mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
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:
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* App Config Index
|
||||
* Exports all app configuration functions
|
||||
*/
|
||||
|
||||
export { getAppConfig } from './get-app-config'
|
||||
export { setAppConfig } from './set-app-config'
|
||||
@@ -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),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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 } })
|
||||
}
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
10
frontends/nextjs/src/lib/db/functions/comments/index.ts
Normal file
10
frontends/nextjs/src/lib/db/functions/comments/index.ts
Normal 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'
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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 } })
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
25
frontends/nextjs/src/lib/db/functions/components/types.ts
Normal file
25
frontends/nextjs/src/lib/db/functions/components/types.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
77
frontends/nextjs/src/lib/lua/LuaEngine.ts
Normal file
77
frontends/nextjs/src/lib/lua/LuaEngine.ts
Normal 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'
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Execution Index
|
||||
* Exports all execution functions
|
||||
*/
|
||||
|
||||
export { executeLuaCode } from './execute-lua-code'
|
||||
@@ -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'
|
||||
|
||||
6
frontends/nextjs/src/lib/lua/functions/setup/index.ts
Normal file
6
frontends/nextjs/src/lib/lua/functions/setup/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Setup Index
|
||||
* Exports all setup functions
|
||||
*/
|
||||
|
||||
export { setupContextAPI } from './setup-context-api'
|
||||
@@ -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'))
|
||||
}
|
||||
21
frontends/nextjs/src/lib/lua/functions/types.ts
Normal file
21
frontends/nextjs/src/lib/lua/functions/types.ts
Normal 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[]
|
||||
}
|
||||
60
frontends/nextjs/src/lib/security/SecurityScanner.ts
Normal file
60
frontends/nextjs/src/lib/security/SecurityScanner.ts
Normal 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
|
||||
}
|
||||
43
frontends/nextjs/src/lib/security/secure-db/access-rules.ts
Normal file
43
frontends/nextjs/src/lib/security/secure-db/access-rules.ts
Normal 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'] },
|
||||
]
|
||||
30
frontends/nextjs/src/lib/security/secure-db/check-access.ts
Normal file
30
frontends/nextjs/src/lib/security/secure-db/check-access.ts
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
33
frontends/nextjs/src/lib/security/secure-db/types.ts
Normal file
33
frontends/nextjs/src/lib/security/secure-db/types.ts
Normal 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>
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user