feat(lua): add Lua execution context and security scanning functionality

- Implemented tableToJS function to convert Lua tables to JavaScript arrays/objects.
- Created executeLuaCode function to execute Lua code with a given context and log messages.
- Added setupContextAPI function to set up logging and printing functions in Lua.
- Defined types for Lua execution context and results.
- Introduced SecurityScanner class to wrap security scanning operations for JavaScript, Lua, JSON, and HTML.
- Established access rules for secure database operations and implemented rate limiting and input sanitization functions.
This commit is contained in:
2025-12-25 18:04:04 +00:00
parent 089c93e649
commit 8d5efd2b17
34 changed files with 1100 additions and 551 deletions

View File

@@ -0,0 +1,26 @@
/**
* Get App Config
* Retrieves the application configuration from database
*/
import { prisma } from '../../prisma'
import type { AppConfiguration } from '../../../types/level-types'
/**
* Get the application configuration
* @returns AppConfiguration or null if not found
*/
export const getAppConfig = async (): Promise<AppConfiguration | null> => {
const config = await prisma.appConfiguration.findFirst()
if (!config) return null
return {
id: config.id,
name: config.name,
schemas: JSON.parse(config.schemas),
workflows: JSON.parse(config.workflows),
luaScripts: JSON.parse(config.luaScripts),
pages: JSON.parse(config.pages),
theme: JSON.parse(config.theme),
}
}

View File

@@ -0,0 +1,7 @@
/**
* App Config Index
* Exports all app configuration functions
*/
export { getAppConfig } from './get-app-config'
export { setAppConfig } from './set-app-config'

View File

@@ -0,0 +1,26 @@
/**
* Set App Config
* Saves the application configuration to database
*/
import { prisma } from '../../prisma'
import type { AppConfiguration } from '../../../types/level-types'
/**
* Set the application configuration
* @param config - The configuration to save
*/
export const setAppConfig = async (config: AppConfiguration): Promise<void> => {
await prisma.appConfiguration.deleteMany()
await prisma.appConfiguration.create({
data: {
id: config.id,
name: config.name,
schemas: JSON.stringify(config.schemas),
workflows: JSON.stringify(config.workflows),
luaScripts: JSON.stringify(config.luaScripts),
pages: JSON.stringify(config.pages),
theme: JSON.stringify(config.theme),
},
})
}

View File

@@ -0,0 +1,24 @@
/**
* Add Comment
* Adds a new comment to database
*/
import { prisma } from '../../prisma'
import type { Comment } from '../../../types/level-types'
/**
* Add a new comment
* @param comment - Comment to add
*/
export const addComment = async (comment: Comment): Promise<void> => {
await prisma.comment.create({
data: {
id: comment.id,
userId: comment.userId,
content: comment.content,
createdAt: BigInt(comment.createdAt),
updatedAt: comment.updatedAt ? BigInt(comment.updatedAt) : null,
parentId: comment.parentId,
},
})
}

View File

@@ -0,0 +1,14 @@
/**
* Delete Comment
* Deletes a comment from database
*/
import { prisma } from '../../prisma'
/**
* Delete a comment
* @param commentId - ID of comment to delete
*/
export const deleteComment = async (commentId: string): Promise<void> => {
await prisma.comment.delete({ where: { id: commentId } })
}

View File

@@ -0,0 +1,23 @@
/**
* Get Comments
* Retrieves all comments from database
*/
import { prisma } from '../../prisma'
import type { Comment } from '../../../types/level-types'
/**
* Get all comments
* @returns Array of comments
*/
export const getComments = async (): Promise<Comment[]> => {
const comments = await prisma.comment.findMany()
return comments.map(c => ({
id: c.id,
userId: c.userId,
content: c.content,
createdAt: Number(c.createdAt),
updatedAt: c.updatedAt ? Number(c.updatedAt) : undefined,
parentId: c.parentId || undefined,
}))
}

View File

@@ -0,0 +1,10 @@
/**
* Comments Index
* Exports all comment functions
*/
export { getComments } from './get-comments'
export { setComments } from './set-comments'
export { addComment } from './add-comment'
export { updateComment } from './update-comment'
export { deleteComment } from './delete-comment'

View File

@@ -0,0 +1,27 @@
/**
* Set Comments
* Replaces all comments in database
*/
import { prisma } from '../../prisma'
import type { Comment } from '../../../types/level-types'
/**
* Set all comments (replaces existing)
* @param comments - Array of comments to save
*/
export const setComments = async (comments: Comment[]): Promise<void> => {
await prisma.comment.deleteMany()
for (const comment of comments) {
await prisma.comment.create({
data: {
id: comment.id,
userId: comment.userId,
content: comment.content,
createdAt: BigInt(comment.createdAt),
updatedAt: comment.updatedAt ? BigInt(comment.updatedAt) : null,
parentId: comment.parentId,
},
})
}
}

View File

@@ -0,0 +1,23 @@
/**
* Update Comment
* Updates an existing comment
*/
import { prisma } from '../../prisma'
import type { Comment } from '../../../types/level-types'
/**
* Update a comment
* @param commentId - ID of comment to update
* @param updates - Partial comment with updates
*/
export const updateComment = async (commentId: string, updates: Partial<Comment>): Promise<void> => {
const data: any = {}
if (updates.content !== undefined) data.content = updates.content
if (updates.updatedAt !== undefined) data.updatedAt = BigInt(updates.updatedAt)
await prisma.comment.update({
where: { id: commentId },
data,
})
}

View File

@@ -0,0 +1,24 @@
/**
* Add Component Node
* Adds a new component node to hierarchy
*/
import { prisma } from '../../prisma'
import type { ComponentNode } from './types'
/**
* Add a component node
* @param node - Component node to add
*/
export const addComponentNode = async (node: ComponentNode): Promise<void> => {
await prisma.componentNode.create({
data: {
id: node.id,
type: node.type,
parentId: node.parentId,
childIds: JSON.stringify(node.childIds),
order: node.order,
pageId: node.pageId,
},
})
}

View File

@@ -0,0 +1,14 @@
/**
* Delete Component Node
* Deletes a component node from hierarchy
*/
import { prisma } from '../../prisma'
/**
* Delete a component node
* @param nodeId - ID of node to delete
*/
export const deleteComponentNode = async (nodeId: string): Promise<void> => {
await prisma.componentNode.delete({ where: { id: nodeId } })
}

View File

@@ -0,0 +1,27 @@
/**
* Get Component Configs
* Retrieves component configurations from database
*/
import { prisma } from '../../prisma'
import type { ComponentConfig } from './types'
/**
* Get all component configs
* @returns Record of component configs by ID
*/
export const getComponentConfigs = async (): Promise<Record<string, ComponentConfig>> => {
const configs = await prisma.componentConfig.findMany()
const result: Record<string, ComponentConfig> = {}
for (const config of configs) {
result[config.id] = {
id: config.id,
componentId: config.componentId,
props: JSON.parse(config.props),
styles: JSON.parse(config.styles),
events: JSON.parse(config.events),
conditionalRendering: config.conditionalRendering ? JSON.parse(config.conditionalRendering) : undefined,
}
}
return result
}

View File

@@ -0,0 +1,27 @@
/**
* Get Component Hierarchy
* Retrieves component hierarchy from database
*/
import { prisma } from '../../prisma'
import type { ComponentNode } from './types'
/**
* Get the component hierarchy
* @returns Record of component nodes by ID
*/
export const getComponentHierarchy = async (): Promise<Record<string, ComponentNode>> => {
const nodes = await prisma.componentNode.findMany()
const result: Record<string, ComponentNode> = {}
for (const node of nodes) {
result[node.id] = {
id: node.id,
type: node.type,
parentId: node.parentId || undefined,
childIds: JSON.parse(node.childIds),
order: node.order,
pageId: node.pageId,
}
}
return result
}

View File

@@ -0,0 +1,27 @@
/**
* Set Component Hierarchy
* Replaces all component hierarchy in database
*/
import { prisma } from '../../prisma'
import type { ComponentNode } from './types'
/**
* Set the component hierarchy (replaces existing)
* @param hierarchy - Record of component nodes by ID
*/
export const setComponentHierarchy = async (hierarchy: Record<string, ComponentNode>): Promise<void> => {
await prisma.componentNode.deleteMany()
for (const node of Object.values(hierarchy)) {
await prisma.componentNode.create({
data: {
id: node.id,
type: node.type,
parentId: node.parentId,
childIds: JSON.stringify(node.childIds),
order: node.order,
pageId: node.pageId,
},
})
}
}

View File

@@ -0,0 +1,25 @@
/**
* Component Types
* Shared types for component hierarchy and config
*/
export interface ComponentNode {
id: string
type: string
parentId?: string
childIds: string[]
order: number
pageId: string
}
export interface ComponentConfig {
id: string
componentId: string
props: Record<string, any>
styles: Record<string, any>
events: Record<string, string>
conditionalRendering?: {
condition: string
luaScriptId?: string
}
}

View File

@@ -0,0 +1,26 @@
/**
* Update Component Node
* Updates an existing component node
*/
import { prisma } from '../../prisma'
import type { ComponentNode } from './types'
/**
* Update a component node
* @param nodeId - ID of node to update
* @param updates - Partial node with updates
*/
export const updateComponentNode = async (nodeId: string, updates: Partial<ComponentNode>): Promise<void> => {
const data: any = {}
if (updates.type !== undefined) data.type = updates.type
if (updates.parentId !== undefined) data.parentId = updates.parentId
if (updates.childIds !== undefined) data.childIds = JSON.stringify(updates.childIds)
if (updates.order !== undefined) data.order = updates.order
if (updates.pageId !== undefined) data.pageId = updates.pageId
await prisma.componentNode.update({
where: { id: nodeId },
data,
})
}

View File

@@ -0,0 +1,77 @@
/**
* LuaEngine - Class wrapper for Lua execution operations
*
* This class serves as a container for lambda functions related to Lua execution.
* Each method delegates to an individual function file in the functions/ directory.
*
* Pattern: "class is container for lambdas"
* - Each lambda is defined in its own file under functions/
* - This class wraps them for convenient stateful Lua execution
* - Maintains Lua state across multiple executions
*/
import * as fengari from 'fengari-web'
import type { LuaExecutionContext, LuaExecutionResult } from './functions/types'
import { setupContextAPI } from './functions/setup/setup-context-api'
import { executeLuaCode } from './functions/execution/execute-lua-code'
const lua = fengari.lua
const lauxlib = fengari.lauxlib
const lualib = fengari.lualib
// Re-export types
export type { LuaExecutionContext, LuaExecutionResult }
/**
* LuaEngine class wraps individual Lua execution lambdas
*/
export class LuaEngine {
private L: any
private logs: string[] = []
constructor() {
this.L = lauxlib.luaL_newstate()
lualib.luaL_openlibs(this.L)
setupContextAPI(this.L, this.logs)
}
/**
* Execute Lua code with a context
* @param code - Lua code to execute
* @param context - Execution context
* @returns Execution result
*/
async execute(code: string, context: LuaExecutionContext = {}): Promise<LuaExecutionResult> {
this.logs = []
return executeLuaCode(this.L, code, context, this.logs)
}
/**
* Destroy the Lua state
*/
destroy(): void {
if (this.L) {
lua.lua_close(this.L)
}
}
}
/**
* Factory function to create a new LuaEngine instance
* @returns New LuaEngine instance
*/
export const createLuaEngine = (): LuaEngine => {
return new LuaEngine()
}
// Re-export individual functions for direct imports
export {
setupContextAPI,
executeLuaCode
} from './functions'
export {
pushToLua,
fromLua,
tableToJS
} from './functions/converters'

View File

@@ -0,0 +1,34 @@
/**
* From Lua
* Converts Lua stack values to JavaScript values
*/
import * as fengari from 'fengari-web'
import { tableToJS } from './table-to-js'
const lua = fengari.lua
/**
* Convert a Lua stack value to JavaScript
* @param L - Lua state
* @param index - Stack index (default: -1 for top of stack)
* @returns JavaScript value
*/
export const fromLua = (L: any, index: number = -1): any => {
const type = lua.lua_type(L, index)
switch (type) {
case lua.LUA_TNIL:
return null
case lua.LUA_TBOOLEAN:
return lua.lua_toboolean(L, index)
case lua.LUA_TNUMBER:
return lua.lua_tonumber(L, index)
case lua.LUA_TSTRING:
return lua.lua_tojsstring(L, index)
case lua.LUA_TTABLE:
return tableToJS(L, index)
default:
return null
}
}

View File

@@ -0,0 +1,8 @@
/**
* Converters Index
* Exports all Lua/JS conversion functions
*/
export { pushToLua } from './push-to-lua'
export { fromLua } from './from-lua'
export { tableToJS } from './table-to-js'

View File

@@ -0,0 +1,41 @@
/**
* Push to Lua
* Converts JavaScript values to Lua stack values
*/
import * as fengari from 'fengari-web'
const lua = fengari.lua
/**
* Push a JavaScript value onto the Lua stack
* @param L - Lua state
* @param value - JavaScript value to push
*/
export const pushToLua = (L: any, value: any): void => {
if (value === null || value === undefined) {
lua.lua_pushnil(L)
} else if (typeof value === 'boolean') {
lua.lua_pushboolean(L, value)
} else if (typeof value === 'number') {
lua.lua_pushnumber(L, value)
} else if (typeof value === 'string') {
lua.lua_pushstring(L, fengari.to_luastring(value))
} else if (Array.isArray(value)) {
lua.lua_createtable(L, value.length, 0)
value.forEach((item, index) => {
lua.lua_pushinteger(L, index + 1)
pushToLua(L, item)
lua.lua_settable(L, -3)
})
} else if (typeof value === 'object') {
lua.lua_createtable(L, 0, Object.keys(value).length)
for (const [key, val] of Object.entries(value)) {
lua.lua_pushstring(L, fengari.to_luastring(key))
pushToLua(L, val)
lua.lua_settable(L, -3)
}
} else {
lua.lua_pushnil(L)
}
}

View File

@@ -0,0 +1,70 @@
/**
* Table to JS
* Converts Lua tables to JavaScript arrays/objects
*/
import * as fengari from 'fengari-web'
const lua = fengari.lua
/**
* Convert a Lua table to JavaScript array or object
* @param L - Lua state
* @param index - Stack index of the table
* @returns JavaScript array or object
*/
export const tableToJS = (L: any, index: number): any => {
const result: any = {}
let isArray = true
let arrayIndex = 1
lua.lua_pushnil(L)
while (lua.lua_next(L, index < 0 ? index - 1 : index) !== 0) {
const keyType = lua.lua_type(L, -2)
if (keyType === lua.LUA_TNUMBER) {
const key = lua.lua_tonumber(L, -2)
if (key !== arrayIndex) {
isArray = false
}
arrayIndex++
} else {
isArray = false
}
const key = fromLuaValue(L, -2)
const value = fromLuaValue(L, -1)
result[key] = value
lua.lua_pop(L, 1)
}
if (isArray && arrayIndex > 1) {
return Object.values(result)
}
return result
}
/**
* Helper to convert single Lua value (avoids circular dependency)
*/
const fromLuaValue = (L: any, index: number): any => {
const type = lua.lua_type(L, index)
switch (type) {
case lua.LUA_TNIL:
return null
case lua.LUA_TBOOLEAN:
return lua.lua_toboolean(L, index)
case lua.LUA_TNUMBER:
return lua.lua_tonumber(L, index)
case lua.LUA_TSTRING:
return lua.lua_tojsstring(L, index)
case lua.LUA_TTABLE:
return tableToJS(L, index)
default:
return null
}
}

View File

@@ -0,0 +1,108 @@
/**
* Execute Lua Code
* Executes Lua code with a given context
*/
import * as fengari from 'fengari-web'
import type { LuaExecutionContext, LuaExecutionResult } from '../types'
import { pushToLua } from '../converters/push-to-lua'
import { fromLua } from '../converters/from-lua'
const lua = fengari.lua
const lauxlib = fengari.lauxlib
/**
* Execute Lua code with a context
* @param L - Lua state
* @param code - Lua code to execute
* @param context - Execution context with data, user, kv
* @param logs - Array to collect log messages
* @returns Execution result
*/
export const executeLuaCode = async (
L: any,
code: string,
context: LuaExecutionContext,
logs: string[]
): Promise<LuaExecutionResult> => {
try {
// Create context table
lua.lua_createtable(L, 0, 3)
if (context.data !== undefined) {
lua.lua_pushstring(L, fengari.to_luastring('data'))
pushToLua(L, context.data)
lua.lua_settable(L, -3)
}
if (context.user !== undefined) {
lua.lua_pushstring(L, fengari.to_luastring('user'))
pushToLua(L, context.user)
lua.lua_settable(L, -3)
}
const kvMethods: any = {}
if (context.kv) {
kvMethods.get = context.kv.get
kvMethods.set = context.kv.set
}
lua.lua_pushstring(L, fengari.to_luastring('kv'))
pushToLua(L, kvMethods)
lua.lua_settable(L, -3)
lua.lua_setglobal(L, fengari.to_luastring('context'))
// Load and execute code
const loadResult = lauxlib.luaL_loadstring(L, fengari.to_luastring(code))
if (loadResult !== lua.LUA_OK) {
const errorMsg = lua.lua_tojsstring(L, -1)
lua.lua_pop(L, 1)
return {
success: false,
error: `Syntax error: ${errorMsg}`,
logs,
}
}
const execResult = lua.lua_pcall(L, 0, lua.LUA_MULTRET, 0)
if (execResult !== lua.LUA_OK) {
const errorMsg = lua.lua_tojsstring(L, -1)
lua.lua_pop(L, 1)
return {
success: false,
error: `Runtime error: ${errorMsg}`,
logs,
}
}
// Get results
const nresults = lua.lua_gettop(L)
let result: any = null
if (nresults > 0) {
if (nresults === 1) {
result = fromLua(L, -1)
} else {
result = []
for (let i = 1; i <= nresults; i++) {
result.push(fromLua(L, -nresults + i - 1))
}
}
lua.lua_pop(L, nresults)
}
return {
success: true,
result,
logs,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
logs,
}
}
}

View File

@@ -0,0 +1,6 @@
/**
* Execution Index
* Exports all execution functions
*/
export { executeLuaCode } from './execute-lua-code'

View File

@@ -1,3 +1,23 @@
/**
* Lua Functions Index
* Exports all Lua engine functions and snippet utilities
*/
// Types
export type { LuaExecutionContext, LuaExecutionResult } from './types'
// Converters
export { pushToLua } from './converters/push-to-lua'
export { fromLua } from './converters/from-lua'
export { tableToJS } from './converters/table-to-js'
// Setup
export { setupContextAPI } from './setup/setup-context-api'
// Execution
export { executeLuaCode } from './execution/execute-lua-code'
// Snippet utilities
export { getSnippetsByCategory } from './get-snippets-by-category'
export { searchSnippets } from './search-snippets'
export { getSnippetById } from './get-snippet-by-id'

View File

@@ -0,0 +1,6 @@
/**
* Setup Index
* Exports all setup functions
*/
export { setupContextAPI } from './setup-context-api'

View File

@@ -0,0 +1,63 @@
/**
* Setup Context API
* Sets up the Lua execution context with log/print functions
*/
import * as fengari from 'fengari-web'
const lua = fengari.lua
/**
* Setup the context API functions (log, print) in Lua state
* @param L - Lua state
* @param logs - Array to collect log messages
*/
export const setupContextAPI = (L: any, logs: string[]): void => {
// Create log function
const logFunction = function(LState: any) {
const nargs = lua.lua_gettop(LState)
const messages: string[] = []
for (let i = 1; i <= nargs; i++) {
if (lua.lua_isstring(LState, i)) {
messages.push(lua.lua_tojsstring(LState, i))
} else if (lua.lua_isnumber(LState, i)) {
messages.push(String(lua.lua_tonumber(LState, i)))
} else if (lua.lua_isboolean(LState, i)) {
messages.push(String(lua.lua_toboolean(LState, i)))
} else {
messages.push(lua.lua_typename(LState, lua.lua_type(LState, i)))
}
}
logs.push(messages.join(' '))
return 0
}
lua.lua_pushcfunction(L, logFunction)
lua.lua_setglobal(L, fengari.to_luastring('log'))
// Create print function (same behavior but tab-separated)
const printFunction = function(LState: any) {
const nargs = lua.lua_gettop(LState)
const messages: string[] = []
for (let i = 1; i <= nargs; i++) {
if (lua.lua_isstring(LState, i)) {
messages.push(lua.lua_tojsstring(LState, i))
} else if (lua.lua_isnumber(LState, i)) {
messages.push(String(lua.lua_tonumber(LState, i)))
} else if (lua.lua_isboolean(LState, i)) {
messages.push(String(lua.lua_toboolean(LState, i)))
} else {
messages.push(lua.lua_typename(LState, lua.lua_type(LState, i)))
}
}
logs.push(messages.join('\t'))
return 0
}
lua.lua_pushcfunction(L, printFunction)
lua.lua_setglobal(L, fengari.to_luastring('print'))
}

View File

@@ -0,0 +1,21 @@
/**
* Lua Engine Types
* Shared type definitions for Lua execution
*/
export interface LuaExecutionContext {
data?: any
user?: any
kv?: {
get: (key: string) => Promise<any>
set: (key: string, value: any) => Promise<void>
}
log?: (...args: any[]) => void
}
export interface LuaExecutionResult {
success: boolean
result?: any
error?: string
logs: string[]
}

View File

@@ -0,0 +1,60 @@
/**
* SecurityScanner - Class wrapper for security scanning operations
*
* This class serves as a container for lambda functions related to security scanning.
* Each method delegates to an individual function file in the functions/ directory.
*
* Pattern: "class is container for lambdas"
* - Each lambda is defined in its own file under functions/
* - This class wraps them for convenient namespaced access
* - Can be used as SecurityScanner.methodName() or import individual functions
*/
import type { SecurityScanResult, SecurityIssue, SecurityPattern } from './functions/types'
// Import individual lambdas
import { scanJavaScript } from './functions/scanners/scan-javascript'
import { scanLua } from './functions/scanners/scan-lua'
import { scanJSON } from './functions/scanners/scan-json'
import { scanHTML } from './functions/scanners/scan-html'
import { sanitizeInput } from './functions/scanners/sanitize-input'
import { getSeverityColor } from './functions/helpers/get-severity-color'
import { getSeverityIcon } from './functions/helpers/get-severity-icon'
// Re-export types for convenience
export type { SecurityScanResult, SecurityIssue, SecurityPattern }
/**
* SecurityScanner class wraps individual scanner lambdas
*/
export class SecurityScanner {
/** Scan JavaScript code for security vulnerabilities */
scanJavaScript = scanJavaScript
/** Scan Lua code for security vulnerabilities */
scanLua = scanLua
/** Scan JSON for security vulnerabilities */
scanJSON = scanJSON
/** Scan HTML for security vulnerabilities */
scanHTML = scanHTML
/** Sanitize input based on content type */
sanitizeInput = sanitizeInput
}
// Export singleton instance for convenience
export const securityScanner = new SecurityScanner()
// Export helper functions directly
export { getSeverityColor, getSeverityIcon }
// Re-export all individual functions for direct imports
export {
scanJavaScript,
scanLua,
scanJSON,
scanHTML,
sanitizeInput
}

View File

@@ -0,0 +1,43 @@
import type { AccessRule } from './types'
export const ACCESS_RULES: AccessRule[] = [
{ resource: 'user', operation: 'READ', allowedRoles: ['user', 'admin', 'god', 'supergod'] },
{ resource: 'user', operation: 'CREATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'user', operation: 'UPDATE', allowedRoles: ['admin', 'god', 'supergod'] },
{ resource: 'user', operation: 'DELETE', allowedRoles: ['god', 'supergod'] },
{ resource: 'workflow', operation: 'READ', allowedRoles: ['admin', 'god', 'supergod'] },
{ resource: 'workflow', operation: 'CREATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'workflow', operation: 'UPDATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'workflow', operation: 'DELETE', allowedRoles: ['god', 'supergod'] },
{ resource: 'luaScript', operation: 'READ', allowedRoles: ['god', 'supergod'] },
{ resource: 'luaScript', operation: 'CREATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'luaScript', operation: 'UPDATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'luaScript', operation: 'DELETE', allowedRoles: ['god', 'supergod'] },
{ resource: 'pageConfig', operation: 'READ', allowedRoles: ['user', 'admin', 'god', 'supergod'] },
{ resource: 'pageConfig', operation: 'CREATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'pageConfig', operation: 'UPDATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'pageConfig', operation: 'DELETE', allowedRoles: ['god', 'supergod'] },
{ resource: 'modelSchema', operation: 'READ', allowedRoles: ['admin', 'god', 'supergod'] },
{ resource: 'modelSchema', operation: 'CREATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'modelSchema', operation: 'UPDATE', allowedRoles: ['god', 'supergod'] },
{ resource: 'modelSchema', operation: 'DELETE', allowedRoles: ['god', 'supergod'] },
{ resource: 'comment', operation: 'READ', allowedRoles: ['user', 'admin', 'god', 'supergod'] },
{ resource: 'comment', operation: 'CREATE', allowedRoles: ['user', 'admin', 'god', 'supergod'] },
{ resource: 'comment', operation: 'UPDATE', allowedRoles: ['user', 'admin', 'god', 'supergod'] },
{ resource: 'comment', operation: 'DELETE', allowedRoles: ['admin', 'god', 'supergod'] },
{ resource: 'smtpConfig', operation: 'READ', allowedRoles: ['god', 'supergod'] },
{ resource: 'smtpConfig', operation: 'UPDATE', allowedRoles: ['supergod'] },
{ resource: 'credential', operation: 'UPDATE', allowedRoles: ['user', 'admin', 'god', 'supergod'] },
{ resource: 'tenant', operation: 'READ', allowedRoles: ['god', 'supergod'] },
{ resource: 'tenant', operation: 'CREATE', allowedRoles: ['supergod'] },
{ resource: 'tenant', operation: 'UPDATE', allowedRoles: ['supergod'] },
{ resource: 'tenant', operation: 'DELETE', allowedRoles: ['supergod'] },
]

View File

@@ -0,0 +1,30 @@
import type { SecurityContext, OperationType, ResourceType } from './types'
import { ACCESS_RULES } from './access-rules'
/**
* Check if user has access to perform operation on resource
*/
export async function checkAccess(
ctx: SecurityContext,
resource: ResourceType,
operation: OperationType,
resourceId?: string
): Promise<boolean> {
const rule = ACCESS_RULES.find(
r => r.resource === resource && r.operation === operation
)
if (!rule) {
return false
}
if (!rule.allowedRoles.includes(ctx.user.role)) {
return false
}
if (rule.customCheck) {
return await rule.customCheck(ctx, resourceId)
}
return true
}

View File

@@ -0,0 +1,38 @@
const RATE_LIMIT_WINDOW = 60000
const MAX_REQUESTS_PER_WINDOW = 100
const rateLimitMap = new Map<string, number[]>()
/**
* Check if user is within rate limits
*/
export function checkRateLimit(userId: string): boolean {
const now = Date.now()
const userRequests = rateLimitMap.get(userId) || []
const recentRequests = userRequests.filter(
timestamp => now - timestamp < RATE_LIMIT_WINDOW
)
if (recentRequests.length >= MAX_REQUESTS_PER_WINDOW) {
return false
}
recentRequests.push(now)
rateLimitMap.set(userId, recentRequests)
return true
}
/**
* Clear rate limit for a user (useful for testing)
*/
export function clearRateLimit(userId: string): void {
rateLimitMap.delete(userId)
}
/**
* Clear all rate limits (useful for testing)
*/
export function clearAllRateLimits(): void {
rateLimitMap.clear()
}

View File

@@ -0,0 +1,19 @@
/**
* Sanitize user input to prevent XSS and injection attacks
*/
export function sanitizeInput<T>(input: T): T {
if (typeof input === 'string') {
return input.replace(/[<>'"]/g, '') as T
}
if (Array.isArray(input)) {
return input.map(item => sanitizeInput(item)) as T
}
if (typeof input === 'object' && input !== null) {
const sanitized: Record<string, unknown> = {}
for (const key in input) {
sanitized[key] = sanitizeInput((input as Record<string, unknown>)[key])
}
return sanitized as T
}
return input
}

View File

@@ -0,0 +1,33 @@
import type { User } from '../../types/level-types'
export type OperationType = 'CREATE' | 'READ' | 'UPDATE' | 'DELETE'
export type ResourceType = 'user' | 'workflow' | 'luaScript' | 'pageConfig' |
'modelSchema' | 'comment' | 'componentNode' | 'componentConfig' | 'cssCategory' |
'dropdownConfig' | 'tenant' | 'powerTransfer' | 'smtpConfig' | 'credential'
export interface AuditLog {
id: string
timestamp: number
userId: string
username: string
operation: OperationType
resource: ResourceType
resourceId: string
success: boolean
errorMessage?: string
ipAddress?: string
metadata?: Record<string, any>
}
export interface SecurityContext {
user: User
ipAddress?: string
requestId?: string
}
export interface AccessRule {
resource: ResourceType
operation: OperationType
allowedRoles: string[]
customCheck?: (ctx: SecurityContext, resourceId?: string) => Promise<boolean>
}

View File

@@ -1,565 +1,87 @@
export interface SecurityScanResult {
safe: boolean
severity: 'safe' | 'low' | 'medium' | 'high' | 'critical'
issues: SecurityIssue[]
sanitizedCode?: string
}
/**
* Security Scanner
*
* Wraps individual scan functions into a unified class interface.
* Each scanning function is implemented as 1 lambda/function per file
* in the functions/ directory.
*
* Pattern: Lambda wrapper class - keeps this file short while maintaining
* a convenient class-based API.
*/
export interface SecurityIssue {
type: 'malicious' | 'suspicious' | 'dangerous' | 'warning'
severity: 'low' | 'medium' | 'high' | 'critical'
message: string
pattern: string
line?: number
recommendation?: string
}
// Types
export type { SecurityScanResult, SecurityIssue, SecurityPattern } from './functions/types'
const MALICIOUS_PATTERNS = [
{
pattern: /eval\s*\(/gi,
type: 'dangerous' as const,
severity: 'critical' as const,
message: 'Use of eval() detected - can execute arbitrary code',
recommendation: 'Use safe alternatives like JSON.parse() or Function constructor with strict validation'
},
{
pattern: /Function\s*\(/gi,
type: 'dangerous' as const,
severity: 'high' as const,
message: 'Dynamic Function constructor detected',
recommendation: 'Avoid dynamic code generation or use with extreme caution'
},
{
pattern: /innerHTML\s*=/gi,
type: 'dangerous' as const,
severity: 'high' as const,
message: 'innerHTML assignment detected - XSS vulnerability risk',
recommendation: 'Use textContent, createElement, or React JSX instead'
},
{
pattern: /dangerouslySetInnerHTML/gi,
type: 'dangerous' as const,
severity: 'high' as const,
message: 'dangerouslySetInnerHTML detected - XSS vulnerability risk',
recommendation: 'Sanitize HTML content or use safe alternatives'
},
{
pattern: /document\.write\s*\(/gi,
type: 'dangerous' as const,
severity: 'medium' as const,
message: 'document.write() detected - can cause security issues',
recommendation: 'Use DOM manipulation methods instead'
},
{
pattern: /\.call\s*\(\s*window/gi,
type: 'suspicious' as const,
severity: 'medium' as const,
message: 'Calling functions with window context',
recommendation: 'Be careful with context manipulation'
},
{
pattern: /\.apply\s*\(\s*window/gi,
type: 'suspicious' as const,
severity: 'medium' as const,
message: 'Applying functions with window context',
recommendation: 'Be careful with context manipulation'
},
{
pattern: /__proto__/gi,
type: 'dangerous' as const,
severity: 'critical' as const,
message: 'Prototype pollution attempt detected',
recommendation: 'Never manipulate __proto__ directly'
},
{
pattern: /constructor\s*\[\s*['"]prototype['"]\s*\]/gi,
type: 'dangerous' as const,
severity: 'critical' as const,
message: 'Prototype manipulation detected',
recommendation: 'Use Object.create() or proper class syntax'
},
{
pattern: /import\s+.*\s+from\s+['"]https?:/gi,
type: 'dangerous' as const,
severity: 'critical' as const,
message: 'Remote code import detected',
recommendation: 'Only import from trusted, local sources'
},
{
pattern: /<script[^>]*>/gi,
type: 'dangerous' as const,
severity: 'critical' as const,
message: 'Script tag injection detected',
recommendation: 'Never inject script tags dynamically'
},
{
pattern: /on(click|load|error|mouseover|mouseout|focus|blur)\s*=/gi,
type: 'suspicious' as const,
severity: 'medium' as const,
message: 'Inline event handler detected',
recommendation: 'Use addEventListener or React event handlers'
},
{
pattern: /javascript:\s*/gi,
type: 'dangerous' as const,
severity: 'high' as const,
message: 'javascript: protocol detected',
recommendation: 'Never use javascript: protocol in URLs'
},
{
pattern: /data:\s*text\/html/gi,
type: 'dangerous' as const,
severity: 'high' as const,
message: 'Data URI with HTML detected',
recommendation: 'Avoid data URIs with executable content'
},
{
pattern: /setTimeout\s*\(\s*['"`]/gi,
type: 'dangerous' as const,
severity: 'high' as const,
message: 'setTimeout with string argument detected',
recommendation: 'Use setTimeout with function reference instead'
},
{
pattern: /setInterval\s*\(\s*['"`]/gi,
type: 'dangerous' as const,
severity: 'high' as const,
message: 'setInterval with string argument detected',
recommendation: 'Use setInterval with function reference instead'
},
{
pattern: /localStorage|sessionStorage/gi,
type: 'warning' as const,
severity: 'low' as const,
message: 'Local/session storage usage detected',
recommendation: 'Use useKV hook for persistent data instead'
},
{
pattern: /crypto\.subtle|atob|btoa/gi,
type: 'warning' as const,
severity: 'low' as const,
message: 'Cryptographic operation detected',
recommendation: 'Ensure proper key management and secure practices'
},
{
pattern: /XMLHttpRequest|fetch\s*\(\s*['"`]http/gi,
type: 'warning' as const,
severity: 'medium' as const,
message: 'External HTTP request detected',
recommendation: 'Ensure CORS and security headers are properly configured'
},
{
pattern: /window\.open/gi,
type: 'suspicious' as const,
severity: 'medium' as const,
message: 'window.open detected',
recommendation: 'Be cautious with popup windows'
},
{
pattern: /location\.href\s*=/gi,
type: 'suspicious' as const,
severity: 'medium' as const,
message: 'Direct location manipulation detected',
recommendation: 'Use React Router or validate URLs carefully'
},
{
pattern: /require\s*\(\s*[^'"`]/gi,
type: 'dangerous' as const,
severity: 'high' as const,
message: 'Dynamic require() detected',
recommendation: 'Use static imports only'
},
{
pattern: /\.exec\s*\(|child_process|spawn|fork|execFile/gi,
type: 'malicious' as const,
severity: 'critical' as const,
message: 'System command execution attempt detected',
recommendation: 'This is not allowed in browser environment'
},
{
pattern: /fs\.|path\.|os\./gi,
type: 'malicious' as const,
severity: 'critical' as const,
message: 'Node.js system module usage detected',
recommendation: 'File system access not allowed in browser'
},
{
pattern: /process\.env|process\.exit/gi,
type: 'suspicious' as const,
severity: 'medium' as const,
message: 'Process manipulation detected',
recommendation: 'Not applicable in browser environment'
}
]
// Import individual lambda functions
import { scanJavaScript } from './functions/scanners/scan-javascript'
import { scanLua } from './functions/scanners/scan-lua'
import { scanJSON } from './functions/scanners/scan-json'
import { scanHTML } from './functions/scanners/scan-html'
import { sanitizeInput } from './functions/scanners/sanitize-input'
import { getSeverityColor } from './functions/helpers/get-severity-color'
import { getSeverityIcon } from './functions/helpers/get-severity-icon'
const LUA_MALICIOUS_PATTERNS = [
{
pattern: /os\.(execute|exit|remove|rename|tmpname)/gi,
type: 'malicious' as const,
severity: 'critical' as const,
message: 'Lua OS module system call detected',
recommendation: 'OS module access is disabled for security'
},
{
pattern: /io\.(popen|tmpfile|open|input|output|lines)/gi,
type: 'malicious' as const,
severity: 'critical' as const,
message: 'Lua file I/O operation detected',
recommendation: 'File system access is disabled for security'
},
{
pattern: /loadfile|dofile/gi,
type: 'dangerous' as const,
severity: 'critical' as const,
message: 'Lua file loading function detected',
recommendation: 'File loading is disabled for security'
},
{
pattern: /package\.(loadlib|searchpath|cpath)/gi,
type: 'dangerous' as const,
severity: 'critical' as const,
message: 'Lua dynamic library loading detected',
recommendation: 'Dynamic library loading is disabled'
},
{
pattern: /debug\.(getinfo|setmetatable|getfenv|setfenv)/gi,
type: 'dangerous' as const,
severity: 'high' as const,
message: 'Lua debug module advanced features detected',
recommendation: 'Limited debug functionality available'
},
{
pattern: /loadstring\s*\(/gi,
type: 'dangerous' as const,
severity: 'high' as const,
message: 'Lua dynamic code execution detected',
recommendation: 'Use with extreme caution'
},
{
pattern: /\.\.\s*[[\]]/gi,
type: 'suspicious' as const,
severity: 'medium' as const,
message: 'Potential Lua table manipulation',
recommendation: 'Ensure proper validation'
},
{
pattern: /_G\s*\[/gi,
type: 'suspicious' as const,
severity: 'high' as const,
message: 'Global environment manipulation detected',
recommendation: 'Avoid modifying global environment'
},
{
pattern: /getmetatable|setmetatable/gi,
type: 'suspicious' as const,
severity: 'medium' as const,
message: 'Metatable manipulation detected',
recommendation: 'Use carefully to avoid security issues'
},
{
pattern: /while\s+true\s+do/gi,
type: 'warning' as const,
severity: 'medium' as const,
message: 'Infinite loop detected',
recommendation: 'Ensure proper break conditions exist'
},
{
pattern: /function\s+(\w+)\s*\([^)]*\)\s*\{[^}]*\1\s*\(/gi,
type: 'warning' as const,
severity: 'low' as const,
message: 'Potential recursive function',
recommendation: 'Ensure recursion has proper termination'
}
]
const SQL_INJECTION_PATTERNS = [
{
pattern: /;\s*(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE)\s+/gi,
type: 'malicious' as const,
severity: 'critical' as const,
message: 'SQL injection attempt detected',
recommendation: 'Use parameterized queries'
},
{
pattern: /UNION\s+SELECT/gi,
type: 'malicious' as const,
severity: 'critical' as const,
message: 'SQL UNION injection attempt',
recommendation: 'Use parameterized queries'
},
{
pattern: /'[\s]*OR[\s]*'1'[\s]*=[\s]*'1/gi,
type: 'malicious' as const,
severity: 'critical' as const,
message: 'SQL authentication bypass attempt',
recommendation: 'Never concatenate user input into SQL'
},
{
pattern: /--[\s]*$/gm,
type: 'suspicious' as const,
severity: 'high' as const,
message: 'SQL comment pattern detected',
recommendation: 'May indicate SQL injection attempt'
}
]
import type { SecurityScanResult } from './functions/types'
/**
* SecurityScanner - Wrapper class for security scanning functions
*
* All methods delegate to individual lambda functions in functions/scanners/
* This keeps the main class file small while providing a unified API.
*/
export class SecurityScanner {
scanJavaScript(code: string): SecurityScanResult {
const issues: SecurityIssue[] = []
const lines = code.split('\n')
for (const pattern of MALICIOUS_PATTERNS) {
const matches = code.matchAll(new RegExp(pattern.pattern.source, pattern.pattern.flags))
for (const match of matches) {
const lineNumber = this.getLineNumber(code, match.index || 0)
issues.push({
type: pattern.type,
severity: pattern.severity,
message: pattern.message,
pattern: match[0],
line: lineNumber,
recommendation: pattern.recommendation
})
}
}
for (const pattern of SQL_INJECTION_PATTERNS) {
const matches = code.matchAll(new RegExp(pattern.pattern.source, pattern.pattern.flags))
for (const match of matches) {
const lineNumber = this.getLineNumber(code, match.index || 0)
issues.push({
type: pattern.type,
severity: pattern.severity,
message: pattern.message,
pattern: match[0],
line: lineNumber,
recommendation: pattern.recommendation
})
}
}
const severity = this.calculateOverallSeverity(issues)
const safe = severity === 'safe' || severity === 'low'
return {
safe,
severity,
issues,
sanitizedCode: safe ? code : undefined
}
}
scanLua(code: string): SecurityScanResult {
const issues: SecurityIssue[] = []
for (const pattern of LUA_MALICIOUS_PATTERNS) {
const matches = code.matchAll(new RegExp(pattern.pattern.source, pattern.pattern.flags))
for (const match of matches) {
const lineNumber = this.getLineNumber(code, match.index || 0)
issues.push({
type: pattern.type,
severity: pattern.severity,
message: pattern.message,
pattern: match[0],
line: lineNumber,
recommendation: pattern.recommendation
})
}
}
const severity = this.calculateOverallSeverity(issues)
const safe = severity === 'safe' || severity === 'low'
return {
safe,
severity,
issues,
sanitizedCode: safe ? code : undefined
}
}
scanJSON(jsonString: string): SecurityScanResult {
const issues: SecurityIssue[] = []
try {
JSON.parse(jsonString)
} catch (error) {
issues.push({
type: 'warning',
severity: 'medium',
message: 'Invalid JSON format',
pattern: 'JSON parse error',
recommendation: 'Ensure JSON is properly formatted'
})
}
const protoPollution = /__proto__|constructor\s*\[\s*['"]prototype['"]\s*\]/gi
if (protoPollution.test(jsonString)) {
issues.push({
type: 'malicious',
severity: 'critical',
message: 'Prototype pollution attempt in JSON',
pattern: '__proto__',
recommendation: 'Remove prototype manipulation from JSON'
})
}
if (jsonString.includes('<script')) {
issues.push({
type: 'malicious',
severity: 'critical',
message: 'Script tag in JSON data',
pattern: '<script>',
recommendation: 'Remove all HTML/script content from JSON'
})
}
const severity = this.calculateOverallSeverity(issues)
const safe = severity === 'safe' || severity === 'low'
return {
safe,
severity,
issues,
sanitizedCode: safe ? jsonString : undefined
}
}
scanHTML(html: string): SecurityScanResult {
const issues: SecurityIssue[] = []
const scriptTagPattern = /<script[^>]*>.*?<\/script>/gis
const matches = html.matchAll(scriptTagPattern)
for (const match of matches) {
issues.push({
type: 'dangerous',
severity: 'critical',
message: 'Script tag detected in HTML',
pattern: match[0].substring(0, 50) + '...',
recommendation: 'Remove script tags or use proper React components'
})
}
const inlineEventPattern = /on(click|load|error|mouseover|mouseout|focus|blur|submit)\s*=/gi
const inlineMatches = html.matchAll(inlineEventPattern)
for (const match of inlineMatches) {
issues.push({
type: 'dangerous',
severity: 'high',
message: 'Inline event handler in HTML',
pattern: match[0],
recommendation: 'Use React event handlers instead'
})
}
const javascriptProtocol = /href\s*=\s*['"]javascript:/gi
if (javascriptProtocol.test(html)) {
issues.push({
type: 'dangerous',
severity: 'critical',
message: 'javascript: protocol in href',
pattern: 'javascript:',
recommendation: 'Use proper URLs or event handlers'
})
}
const iframePattern = /<iframe[^>]*>/gi
const iframeMatches = html.matchAll(iframePattern)
for (const match of iframeMatches) {
if (!match[0].includes('sandbox=')) {
issues.push({
type: 'suspicious',
severity: 'medium',
message: 'Iframe without sandbox attribute',
pattern: match[0],
recommendation: 'Add sandbox attribute to iframes for security'
})
}
}
const severity = this.calculateOverallSeverity(issues)
const safe = severity === 'safe' || severity === 'low'
return {
safe,
severity,
issues
}
}
private getLineNumber(code: string, index: number): number {
return code.substring(0, index).split('\n').length
}
private calculateOverallSeverity(issues: SecurityIssue[]): 'safe' | 'low' | 'medium' | 'high' | 'critical' {
if (issues.length === 0) return 'safe'
const hasCritical = issues.some(i => i.severity === 'critical')
const hasHigh = issues.some(i => i.severity === 'high')
const hasMedium = issues.some(i => i.severity === 'medium')
const hasLow = issues.some(i => i.severity === 'low')
if (hasCritical) return 'critical'
if (hasHigh) return 'high'
if (hasMedium) return 'medium'
if (hasLow) return 'low'
return 'safe'
}
sanitizeInput(input: string, type: 'text' | 'html' | 'json' | 'javascript' | 'lua' = 'text'): string {
let sanitized = input
if (type === 'text') {
sanitized = sanitized.replace(/<script[^>]*>.*?<\/script>/gis, '')
sanitized = sanitized.replace(/on\w+\s*=/gi, '')
sanitized = sanitized.replace(/javascript:/gi, '')
}
if (type === 'html') {
sanitized = sanitized.replace(/<script[^>]*>.*?<\/script>/gis, '')
sanitized = sanitized.replace(/on\w+\s*=/gi, '')
sanitized = sanitized.replace(/javascript:/gi, '')
sanitized = sanitized.replace(/data:\s*text\/html/gi, '')
}
if (type === 'json') {
sanitized = sanitized.replace(/__proto__/gi, '_proto_')
sanitized = sanitized.replace(/constructor\s*\[\s*['"]prototype['"]\s*\]/gi, '')
}
return sanitized
}
// Scanner methods - delegate to lambda functions
scanJavaScript = scanJavaScript
scanLua = scanLua
scanJSON = scanJSON
scanHTML = scanHTML
sanitizeInput = sanitizeInput
}
// Default instance for convenience
export const securityScanner = new SecurityScanner()
export function getSeverityColor(severity: string): string {
switch (severity) {
case 'critical':
return 'text-red-600 bg-red-50 border-red-200'
case 'high':
return 'text-orange-600 bg-orange-50 border-orange-200'
case 'medium':
return 'text-yellow-600 bg-yellow-50 border-yellow-200'
case 'low':
return 'text-blue-600 bg-blue-50 border-blue-200'
default:
return 'text-green-600 bg-green-50 border-green-200'
}
// Re-export helper functions
export { getSeverityColor, getSeverityIcon }
// Re-export individual functions for direct use
export {
scanJavaScript,
scanLua,
scanJSON,
scanHTML,
sanitizeInput
}
export function getSeverityIcon(severity: string): string {
switch (severity) {
case 'critical':
return '🚨'
case 'high':
return '⚠️'
case 'medium':
return '⚡'
case 'low':
return ''
/**
* Convenience function to scan code for vulnerabilities
* Automatically detects type based on content or explicit type parameter
*/
export function scanForVulnerabilities(
code: string,
type?: 'javascript' | 'lua' | 'json' | 'html'
): SecurityScanResult {
// Auto-detect type if not provided
if (!type) {
if (code.trim().startsWith('{') || code.trim().startsWith('[')) {
type = 'json'
} else if (code.includes('function') && code.includes('end')) {
type = 'lua'
} else if (code.includes('<') && code.includes('>')) {
type = 'html'
} else {
type = 'javascript'
}
}
switch (type) {
case 'lua':
return scanLua(code)
case 'json':
return scanJSON(code)
case 'html':
return scanHTML(code)
default:
return '✓'
return scanJavaScript(code)
}
}