Merge branch 'main' into codex/add-javascript-injection-and-xss-modules

This commit is contained in:
2025-12-27 18:55:50 +00:00
committed by GitHub
12 changed files with 772 additions and 757 deletions

View File

@@ -1,308 +1,6 @@
import type { SchemaConfig } from '../types/schema-types'
import { defaultApps } from './default/components'
export const defaultSchema: SchemaConfig = {
apps: [
{
name: 'blog',
label: 'Blog',
models: [
{
name: 'post',
label: 'Post',
labelPlural: 'Posts',
icon: 'Article',
listDisplay: ['title', 'author', 'status', 'publishedAt'],
listFilter: ['status', 'author'],
searchFields: ['title', 'content'],
ordering: ['-publishedAt'],
fields: [
{
name: 'id',
type: 'string',
label: 'ID',
required: true,
unique: true,
editable: false,
listDisplay: false,
},
{
name: 'title',
type: 'string',
label: 'Title',
required: true,
validation: {
minLength: 3,
maxLength: 200,
},
listDisplay: true,
searchable: true,
sortable: true,
},
{
name: 'slug',
type: 'string',
label: 'Slug',
required: true,
unique: true,
helpText: 'URL-friendly version of the title',
validation: {
pattern: '^[a-z0-9-]+$',
},
listDisplay: false,
sortable: true,
},
{
name: 'content',
type: 'text',
label: 'Content',
required: true,
helpText: 'Main post content',
listDisplay: false,
searchable: true,
},
{
name: 'excerpt',
type: 'text',
label: 'Excerpt',
required: false,
helpText: ['Short summary of the post', 'Used in list views and previews'],
validation: {
maxLength: 500,
},
listDisplay: false,
},
{
name: 'author',
type: 'relation',
label: 'Author',
required: true,
relatedModel: 'author',
listDisplay: true,
sortable: true,
},
{
name: 'status',
type: 'select',
label: 'Status',
required: true,
default: 'draft',
choices: [
{ value: 'draft', label: 'Draft' },
{ value: 'published', label: 'Published' },
{ value: 'archived', label: 'Archived' },
],
listDisplay: true,
sortable: true,
},
{
name: 'featured',
type: 'boolean',
label: 'Featured',
default: false,
helpText: 'Display on homepage',
listDisplay: true,
},
{
name: 'publishedAt',
type: 'datetime',
label: 'Published At',
required: false,
listDisplay: true,
sortable: true,
},
{
name: 'tags',
type: 'json',
label: 'Tags',
required: false,
helpText: 'JSON array of tag strings',
listDisplay: false,
},
{
name: 'views',
type: 'number',
label: 'Views',
default: 0,
validation: {
min: 0,
},
listDisplay: false,
},
],
},
{
name: 'author',
label: 'Author',
labelPlural: 'Authors',
icon: 'User',
listDisplay: ['name', 'email', 'active', 'createdAt'],
listFilter: ['active'],
searchFields: ['name', 'email'],
ordering: ['name'],
fields: [
{
name: 'id',
type: 'string',
label: 'ID',
required: true,
unique: true,
editable: false,
listDisplay: false,
},
{
name: 'name',
type: 'string',
label: 'Name',
required: true,
validation: {
minLength: 2,
maxLength: 100,
},
listDisplay: true,
searchable: true,
sortable: true,
},
{
name: 'email',
type: 'email',
label: 'Email',
required: true,
unique: true,
listDisplay: true,
searchable: true,
sortable: true,
},
{
name: 'bio',
type: 'text',
label: 'Bio',
required: false,
helpText: 'Author biography',
validation: {
maxLength: 1000,
},
listDisplay: false,
},
{
name: 'website',
type: 'url',
label: 'Website',
required: false,
listDisplay: false,
},
{
name: 'active',
type: 'boolean',
label: 'Active',
default: true,
listDisplay: true,
},
{
name: 'createdAt',
type: 'datetime',
label: 'Created At',
required: true,
editable: false,
listDisplay: true,
sortable: true,
},
],
},
],
},
{
name: 'ecommerce',
label: 'E-Commerce',
models: [
{
name: 'product',
label: 'Product',
labelPlural: 'Products',
icon: 'ShoppingCart',
listDisplay: ['name', 'price', 'stock', 'available'],
listFilter: ['available', 'category'],
searchFields: ['name', 'description'],
ordering: ['name'],
fields: [
{
name: 'id',
type: 'string',
label: 'ID',
required: true,
unique: true,
editable: false,
listDisplay: false,
},
{
name: 'name',
type: 'string',
label: 'Product Name',
required: true,
validation: {
minLength: 3,
maxLength: 200,
},
listDisplay: true,
searchable: true,
sortable: true,
},
{
name: 'description',
type: 'text',
label: 'Description',
required: false,
helpText: 'Product description',
listDisplay: false,
searchable: true,
},
{
name: 'price',
type: 'number',
label: 'Price',
required: true,
validation: {
min: 0,
},
listDisplay: true,
sortable: true,
},
{
name: 'stock',
type: 'number',
label: 'Stock',
required: true,
default: 0,
validation: {
min: 0,
},
listDisplay: true,
sortable: true,
},
{
name: 'category',
type: 'select',
label: 'Category',
required: true,
choices: [
{ value: 'electronics', label: 'Electronics' },
{ value: 'clothing', label: 'Clothing' },
{ value: 'books', label: 'Books' },
{ value: 'home', label: 'Home & Garden' },
{ value: 'toys', label: 'Toys' },
],
listDisplay: false,
sortable: true,
},
{
name: 'available',
type: 'boolean',
label: 'Available',
default: true,
listDisplay: true,
},
],
},
],
},
],
apps: defaultApps,
}

View File

@@ -0,0 +1,54 @@
import type { AppSchema, ModelSchema } from '../../types/schema-types'
import { authorFields, postFields, productFields } from './forms'
export const blogModels: ModelSchema[] = [
{
name: 'post',
label: 'Post',
labelPlural: 'Posts',
icon: 'Article',
listDisplay: ['title', 'author', 'status', 'publishedAt'],
listFilter: ['status', 'author'],
searchFields: ['title', 'content'],
ordering: ['-publishedAt'],
fields: postFields,
},
{
name: 'author',
label: 'Author',
labelPlural: 'Authors',
icon: 'User',
listDisplay: ['name', 'email', 'active', 'createdAt'],
listFilter: ['active'],
searchFields: ['name', 'email'],
ordering: ['name'],
fields: authorFields,
},
]
export const ecommerceModels: ModelSchema[] = [
{
name: 'product',
label: 'Product',
labelPlural: 'Products',
icon: 'ShoppingCart',
listDisplay: ['name', 'price', 'stock', 'available'],
listFilter: ['available', 'category'],
searchFields: ['name', 'description'],
ordering: ['name'],
fields: productFields,
},
]
export const defaultApps: AppSchema[] = [
{
name: 'blog',
label: 'Blog',
models: blogModels,
},
{
name: 'ecommerce',
label: 'E-Commerce',
models: ecommerceModels,
},
]

View File

@@ -0,0 +1,244 @@
import type { FieldSchema } from '../../types/schema-types'
import { authorValidations, postValidations, productValidations } from './validation'
export const postFields: FieldSchema[] = [
{
name: 'id',
type: 'string',
label: 'ID',
required: true,
unique: true,
editable: false,
listDisplay: false,
},
{
name: 'title',
type: 'string',
label: 'Title',
required: true,
validation: postValidations.title,
listDisplay: true,
searchable: true,
sortable: true,
},
{
name: 'slug',
type: 'string',
label: 'Slug',
required: true,
unique: true,
helpText: 'URL-friendly version of the title',
validation: postValidations.slug,
listDisplay: false,
sortable: true,
},
{
name: 'content',
type: 'text',
label: 'Content',
required: true,
helpText: 'Main post content',
listDisplay: false,
searchable: true,
},
{
name: 'excerpt',
type: 'text',
label: 'Excerpt',
required: false,
helpText: ['Short summary of the post', 'Used in list views and previews'],
validation: postValidations.excerpt,
listDisplay: false,
},
{
name: 'author',
type: 'relation',
label: 'Author',
required: true,
relatedModel: 'author',
listDisplay: true,
sortable: true,
},
{
name: 'status',
type: 'select',
label: 'Status',
required: true,
default: 'draft',
choices: [
{ value: 'draft', label: 'Draft' },
{ value: 'published', label: 'Published' },
{ value: 'archived', label: 'Archived' },
],
listDisplay: true,
sortable: true,
},
{
name: 'featured',
type: 'boolean',
label: 'Featured',
default: false,
helpText: 'Display on homepage',
listDisplay: true,
},
{
name: 'publishedAt',
type: 'datetime',
label: 'Published At',
required: false,
listDisplay: true,
sortable: true,
},
{
name: 'tags',
type: 'json',
label: 'Tags',
required: false,
helpText: 'JSON array of tag strings',
listDisplay: false,
},
{
name: 'views',
type: 'number',
label: 'Views',
default: 0,
validation: postValidations.views,
listDisplay: false,
},
]
export const authorFields: FieldSchema[] = [
{
name: 'id',
type: 'string',
label: 'ID',
required: true,
unique: true,
editable: false,
listDisplay: false,
},
{
name: 'name',
type: 'string',
label: 'Name',
required: true,
validation: authorValidations.name,
listDisplay: true,
searchable: true,
sortable: true,
},
{
name: 'email',
type: 'email',
label: 'Email',
required: true,
unique: true,
listDisplay: true,
searchable: true,
sortable: true,
},
{
name: 'bio',
type: 'text',
label: 'Bio',
required: false,
helpText: 'Author biography',
validation: authorValidations.bio,
listDisplay: false,
},
{
name: 'website',
type: 'url',
label: 'Website',
required: false,
listDisplay: false,
},
{
name: 'active',
type: 'boolean',
label: 'Active',
default: true,
listDisplay: true,
},
{
name: 'createdAt',
type: 'datetime',
label: 'Created At',
required: true,
editable: false,
listDisplay: true,
sortable: true,
},
]
export const productFields: FieldSchema[] = [
{
name: 'id',
type: 'string',
label: 'ID',
required: true,
unique: true,
editable: false,
listDisplay: false,
},
{
name: 'name',
type: 'string',
label: 'Product Name',
required: true,
validation: productValidations.name,
listDisplay: true,
searchable: true,
sortable: true,
},
{
name: 'description',
type: 'text',
label: 'Description',
required: false,
helpText: 'Product description',
listDisplay: false,
searchable: true,
},
{
name: 'price',
type: 'number',
label: 'Price',
required: true,
validation: productValidations.price,
listDisplay: true,
sortable: true,
},
{
name: 'stock',
type: 'number',
label: 'Stock',
required: true,
default: 0,
validation: productValidations.stock,
listDisplay: true,
sortable: true,
},
{
name: 'category',
type: 'select',
label: 'Category',
required: true,
choices: [
{ value: 'electronics', label: 'Electronics' },
{ value: 'clothing', label: 'Clothing' },
{ value: 'books', label: 'Books' },
{ value: 'home', label: 'Home & Garden' },
{ value: 'toys', label: 'Toys' },
],
listDisplay: false,
sortable: true,
},
{
name: 'available',
type: 'boolean',
label: 'Available',
default: true,
listDisplay: true,
},
]

View File

@@ -0,0 +1,19 @@
import type { FieldSchema } from '../../types/schema-types'
export const postValidations: Record<string, FieldSchema['validation']> = {
title: { minLength: 3, maxLength: 200 },
slug: { pattern: '^[a-z0-9-]+$' },
excerpt: { maxLength: 500 },
views: { min: 0 },
}
export const authorValidations: Record<string, FieldSchema['validation']> = {
name: { minLength: 2, maxLength: 100 },
bio: { maxLength: 1000 },
}
export const productValidations: Record<string, FieldSchema['validation']> = {
name: { minLength: 3, maxLength: 200 },
price: { min: 0 },
stock: { min: 0 },
}

View File

@@ -1,3 +1,6 @@
// Schema utilities exports
export * from './schema-utils'
export { defaultSchema } from './default-schema'
export * from './default/components'
export * from './default/forms'
export * from './default/validation'

View File

@@ -0,0 +1,234 @@
import { describe, expect, it } from 'vitest'
import { scanForVulnerabilities, securityScanner } from '@/lib/security-scanner'
describe('security-scanner detection', () => {
describe('scanJavaScript', () => {
it.each([
{
name: 'flag eval usage as critical',
code: ['const safe = true;', 'const result = eval("1 + 1")'].join('\n'),
expectedSeverity: 'critical',
expectedSafe: false,
expectedIssueType: 'dangerous',
expectedIssuePattern: 'eval',
expectedLine: 2,
},
{
name: 'warn on localStorage usage but stay safe',
code: 'localStorage.setItem("k", "v")',
expectedSeverity: 'low',
expectedSafe: true,
expectedIssueType: 'warning',
expectedIssuePattern: 'localStorage',
},
{
name: 'return safe for benign code',
code: 'const sum = (a, b) => a + b',
expectedSeverity: 'safe',
expectedSafe: true,
},
])(
'should $name',
({ code, expectedSeverity, expectedSafe, expectedIssueType, expectedIssuePattern, expectedLine }) => {
const result = securityScanner.scanJavaScript(code)
expect(result.severity).toBe(expectedSeverity)
expect(result.safe).toBe(expectedSafe)
if (expectedIssueType || expectedIssuePattern) {
const issue = result.issues.find(item => {
const matchesType = expectedIssueType ? item.type === expectedIssueType : true
const matchesPattern = expectedIssuePattern ? item.pattern.includes(expectedIssuePattern) : true
return matchesType && matchesPattern
})
expect(issue).toBeDefined()
if (expectedLine !== undefined) {
expect(issue?.line).toBe(expectedLine)
}
} else {
expect(result.issues.length).toBe(0)
}
if (expectedSafe) {
expect(result.sanitizedCode).toBe(code)
} else {
expect(result.sanitizedCode).toBeUndefined()
}
}
)
})
describe('scanLua', () => {
it.each([
{
name: 'flag os.execute usage as critical',
code: 'os.execute("rm -rf /")',
expectedSeverity: 'critical',
expectedSafe: false,
expectedIssueType: 'malicious',
expectedIssuePattern: 'os.execute',
},
{
name: 'return safe for simple Lua function',
code: 'function add(a, b) return a + b end',
expectedSeverity: 'safe',
expectedSafe: true,
},
])('should $name', ({ code, expectedSeverity, expectedSafe, expectedIssueType, expectedIssuePattern }) => {
const result = securityScanner.scanLua(code)
expect(result.severity).toBe(expectedSeverity)
expect(result.safe).toBe(expectedSafe)
if (expectedIssueType || expectedIssuePattern) {
const issue = result.issues.find(item => {
const matchesType = expectedIssueType ? item.type === expectedIssueType : true
const matchesPattern = expectedIssuePattern ? item.pattern.includes(expectedIssuePattern) : true
return matchesType && matchesPattern
})
expect(issue).toBeDefined()
} else {
expect(result.issues.length).toBe(0)
}
if (expectedSafe) {
expect(result.sanitizedCode).toBe(code)
} else {
expect(result.sanitizedCode).toBeUndefined()
}
})
})
describe('scanJSON', () => {
it.each([
{
name: 'flag invalid JSON as medium severity',
json: '{"value": }',
expectedSeverity: 'medium',
expectedSafe: false,
expectedIssuePattern: 'JSON parse error',
},
{
name: 'flag prototype pollution in JSON as critical',
json: '{"__proto__": {"polluted": true}}',
expectedSeverity: 'critical',
expectedSafe: false,
expectedIssuePattern: '__proto__',
},
{
name: 'return safe for valid JSON',
json: '{"ok": true}',
expectedSeverity: 'safe',
expectedSafe: true,
},
])('should $name', ({ json, expectedSeverity, expectedSafe, expectedIssuePattern }) => {
const result = securityScanner.scanJSON(json)
expect(result.severity).toBe(expectedSeverity)
expect(result.safe).toBe(expectedSafe)
if (expectedIssuePattern) {
expect(result.issues.some(issue => issue.pattern.includes(expectedIssuePattern))).toBe(true)
} else {
expect(result.issues.length).toBe(0)
}
if (expectedSafe) {
expect(result.sanitizedCode).toBe(json)
} else {
expect(result.sanitizedCode).toBeUndefined()
}
})
})
describe('scanHTML', () => {
it.each([
{
name: 'flag script tags as critical',
html: '<div><script>alert(1)</script></div>',
expectedSeverity: 'critical',
expectedSafe: false,
},
{
name: 'flag inline handlers as high',
html: '<button onclick="alert(1)">Click</button>',
expectedSeverity: 'high',
expectedSafe: false,
},
{
name: 'return safe for plain markup',
html: '<div><span>Safe</span></div>',
expectedSeverity: 'safe',
expectedSafe: true,
},
])('should $name', ({ html, expectedSeverity, expectedSafe }) => {
const result = securityScanner.scanHTML(html)
expect(result.severity).toBe(expectedSeverity)
expect(result.safe).toBe(expectedSafe)
})
})
describe('sanitizeInput', () => {
it.each([
{
name: 'remove script tags and inline handlers from text',
input: '<div onclick="alert(1)">Click</div><script>alert(2)</script><a href="javascript:alert(3)">x</a>',
type: 'text' as const,
shouldExclude: ['<script', 'onclick', 'javascript:'],
},
{
name: 'remove data html URIs from html',
input: '<img src="data:text/html;base64,abc"><script>alert(1)</script>',
type: 'html' as const,
shouldExclude: ['data:text/html', '<script'],
},
{
name: 'neutralize prototype pollution in json',
input: '{"__proto__": {"polluted": true}, "note": "constructor[\\"prototype\\"]"}',
type: 'json' as const,
shouldInclude: ['_proto_'],
shouldExclude: ['__proto__', 'constructor["prototype"]'],
},
])('should $name', ({ input, type, shouldExclude = [], shouldInclude = [] }) => {
const sanitized = securityScanner.sanitizeInput(input, type)
shouldExclude.forEach(value => {
expect(sanitized).not.toContain(value)
})
shouldInclude.forEach(value => {
expect(sanitized).toContain(value)
})
})
})
describe('scanForVulnerabilities', () => {
it.each([
{
name: 'auto-detects JSON and flags prototype pollution',
code: '{"__proto__": {"polluted": true}}',
expectedSeverity: 'critical',
},
{
name: 'auto-detects Lua when function/end present',
code: 'function dangerous() os.execute("rm -rf /") end',
expectedSeverity: 'critical',
},
{
name: 'auto-detects HTML and flags script tags',
code: '<div><script>alert(1)</script></div>',
expectedSeverity: 'critical',
},
{
name: 'falls back to JavaScript scanning',
code: 'const result = eval("1 + 1")',
expectedSeverity: 'critical',
},
{
name: 'honors explicit type parameter',
code: 'return 1',
type: 'lua' as const,
expectedSeverity: 'safe',
},
])('should $name', ({ code, type, expectedSeverity }) => {
const result = scanForVulnerabilities(code, type)
expect(result.severity).toBe(expectedSeverity)
})
})
})

View File

@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest'
import { getSeverityColor, getSeverityIcon } from '@/lib/security-scanner'
describe('security-scanner reporting', () => {
describe('getSeverityColor', () => {
it.each([
{ severity: 'critical', expected: 'error' },
{ severity: 'high', expected: 'warning' },
{ severity: 'medium', expected: 'info' },
{ severity: 'low', expected: 'secondary' },
{ severity: 'safe', expected: 'success' },
])('should map $severity to expected classes', ({ severity, expected }) => {
expect(getSeverityColor(severity)).toBe(expected)
})
})
describe('getSeverityIcon', () => {
it.each([
{ severity: 'critical', expected: '\u{1F6A8}' },
{ severity: 'high', expected: '\u26A0\uFE0F' },
{ severity: 'medium', expected: '\u26A1' },
{ severity: 'low', expected: '\u2139\uFE0F' },
{ severity: 'safe', expected: '\u2713' },
])('should map $severity to expected icon', ({ severity, expected }) => {
expect(getSeverityIcon(severity)).toBe(expected)
})
})
})

View File

@@ -1,257 +1,2 @@
import { describe, it, expect } from 'vitest'
import { securityScanner, scanForVulnerabilities, getSeverityColor, getSeverityIcon } from '@/lib/security-scanner'
describe('security-scanner', () => {
describe('scanJavaScript', () => {
it.each([
{
name: 'flag eval usage as critical',
code: ['const safe = true;', 'const result = eval("1 + 1")'].join('\n'),
expectedSeverity: 'critical',
expectedSafe: false,
expectedIssueType: 'dangerous',
expectedIssuePattern: 'eval',
expectedLine: 2,
},
{
name: 'warn on localStorage usage but stay safe',
code: 'localStorage.setItem("k", "v")',
expectedSeverity: 'low',
expectedSafe: true,
expectedIssueType: 'warning',
expectedIssuePattern: 'localStorage',
},
{
name: 'return safe for benign code',
code: 'const sum = (a, b) => a + b',
expectedSeverity: 'safe',
expectedSafe: true,
},
])(
'should $name',
({ code, expectedSeverity, expectedSafe, expectedIssueType, expectedIssuePattern, expectedLine }) => {
const result = securityScanner.scanJavaScript(code)
expect(result.severity).toBe(expectedSeverity)
expect(result.safe).toBe(expectedSafe)
if (expectedIssueType || expectedIssuePattern) {
const issue = result.issues.find(item => {
const matchesType = expectedIssueType ? item.type === expectedIssueType : true
const matchesPattern = expectedIssuePattern ? item.pattern.includes(expectedIssuePattern) : true
return matchesType && matchesPattern
})
expect(issue).toBeDefined()
if (expectedLine !== undefined) {
expect(issue?.line).toBe(expectedLine)
}
} else {
expect(result.issues.length).toBe(0)
}
if (expectedSafe) {
expect(result.sanitizedCode).toBe(code)
} else {
expect(result.sanitizedCode).toBeUndefined()
}
}
)
})
describe('scanLua', () => {
it.each([
{
name: 'flag os.execute usage as critical',
code: 'os.execute("rm -rf /")',
expectedSeverity: 'critical',
expectedSafe: false,
expectedIssueType: 'malicious',
expectedIssuePattern: 'os.execute',
},
{
name: 'return safe for simple Lua function',
code: 'function add(a, b) return a + b end',
expectedSeverity: 'safe',
expectedSafe: true,
},
])('should $name', ({ code, expectedSeverity, expectedSafe, expectedIssueType, expectedIssuePattern }) => {
const result = securityScanner.scanLua(code)
expect(result.severity).toBe(expectedSeverity)
expect(result.safe).toBe(expectedSafe)
if (expectedIssueType || expectedIssuePattern) {
const issue = result.issues.find(item => {
const matchesType = expectedIssueType ? item.type === expectedIssueType : true
const matchesPattern = expectedIssuePattern ? item.pattern.includes(expectedIssuePattern) : true
return matchesType && matchesPattern
})
expect(issue).toBeDefined()
} else {
expect(result.issues.length).toBe(0)
}
if (expectedSafe) {
expect(result.sanitizedCode).toBe(code)
} else {
expect(result.sanitizedCode).toBeUndefined()
}
})
})
describe('scanJSON', () => {
it.each([
{
name: 'flag invalid JSON as medium severity',
json: '{"value": }',
expectedSeverity: 'medium',
expectedSafe: false,
expectedIssuePattern: 'JSON parse error',
},
{
name: 'flag prototype pollution in JSON as critical',
json: '{"__proto__": {"polluted": true}}',
expectedSeverity: 'critical',
expectedSafe: false,
expectedIssuePattern: '__proto__',
},
{
name: 'return safe for valid JSON',
json: '{"ok": true}',
expectedSeverity: 'safe',
expectedSafe: true,
},
])('should $name', ({ json, expectedSeverity, expectedSafe, expectedIssuePattern }) => {
const result = securityScanner.scanJSON(json)
expect(result.severity).toBe(expectedSeverity)
expect(result.safe).toBe(expectedSafe)
if (expectedIssuePattern) {
expect(result.issues.some(issue => issue.pattern.includes(expectedIssuePattern))).toBe(true)
} else {
expect(result.issues.length).toBe(0)
}
if (expectedSafe) {
expect(result.sanitizedCode).toBe(json)
} else {
expect(result.sanitizedCode).toBeUndefined()
}
})
})
describe('scanHTML', () => {
it.each([
{
name: 'flag script tags as critical',
html: '<div><script>alert(1)</script></div>',
expectedSeverity: 'critical',
expectedSafe: false,
},
{
name: 'flag inline handlers as high',
html: '<button onclick="alert(1)">Click</button>',
expectedSeverity: 'high',
expectedSafe: false,
},
{
name: 'return safe for plain markup',
html: '<div><span>Safe</span></div>',
expectedSeverity: 'safe',
expectedSafe: true,
},
])('should $name', ({ html, expectedSeverity, expectedSafe }) => {
const result = securityScanner.scanHTML(html)
expect(result.severity).toBe(expectedSeverity)
expect(result.safe).toBe(expectedSafe)
})
})
describe('sanitizeInput', () => {
it.each([
{
name: 'remove script tags and inline handlers from text',
input: '<div onclick="alert(1)">Click</div><script>alert(2)</script><a href="javascript:alert(3)">x</a>',
type: 'text' as const,
shouldExclude: ['<script', 'onclick', 'javascript:'],
},
{
name: 'remove data html URIs from html',
input: '<img src="data:text/html;base64,abc"><script>alert(1)</script>',
type: 'html' as const,
shouldExclude: ['data:text/html', '<script'],
},
{
name: 'neutralize prototype pollution in json',
input: '{"__proto__": {"polluted": true}, "note": "constructor[\\"prototype\\"]"}',
type: 'json' as const,
shouldInclude: ['_proto_'],
shouldExclude: ['__proto__', 'constructor["prototype"]'],
},
])('should $name', ({ input, type, shouldExclude = [], shouldInclude = [] }) => {
const sanitized = securityScanner.sanitizeInput(input, type)
shouldExclude.forEach(value => {
expect(sanitized).not.toContain(value)
})
shouldInclude.forEach(value => {
expect(sanitized).toContain(value)
})
})
})
describe('getSeverityColor', () => {
it.each([
{ severity: 'critical', expected: 'error' },
{ severity: 'high', expected: 'warning' },
{ severity: 'medium', expected: 'info' },
{ severity: 'low', expected: 'secondary' },
{ severity: 'safe', expected: 'success' },
])('should map $severity to expected classes', ({ severity, expected }) => {
expect(getSeverityColor(severity)).toBe(expected)
})
})
describe('getSeverityIcon', () => {
it.each([
{ severity: 'critical', expected: '\u{1F6A8}' },
{ severity: 'high', expected: '\u26A0\uFE0F' },
{ severity: 'medium', expected: '\u26A1' },
{ severity: 'low', expected: '\u2139\uFE0F' },
{ severity: 'safe', expected: '\u2713' },
])('should map $severity to expected icon', ({ severity, expected }) => {
expect(getSeverityIcon(severity)).toBe(expected)
})
})
describe('scanForVulnerabilities', () => {
it.each([
{
name: 'auto-detects JSON and flags prototype pollution',
code: '{"__proto__": {"polluted": true}}',
expectedSeverity: 'critical',
},
{
name: 'auto-detects Lua when function/end present',
code: 'function dangerous() os.execute("rm -rf /") end',
expectedSeverity: 'critical',
},
{
name: 'auto-detects HTML and flags script tags',
code: '<div><script>alert(1)</script></div>',
expectedSeverity: 'critical',
},
{
name: 'falls back to JavaScript scanning',
code: 'const result = eval("1 + 1")',
expectedSeverity: 'critical',
},
{
name: 'honors explicit type parameter',
code: 'return 1',
type: 'lua' as const,
expectedSeverity: 'safe',
},
])('should $name', ({ code, type, expectedSeverity }) => {
const result = scanForVulnerabilities(code, type)
expect(result.severity).toBe(expectedSeverity)
})
})
})
import './__tests__/security-scanner.detection.test'
import './__tests__/security-scanner.reporting.test'

View File

@@ -0,0 +1,71 @@
import '@mui/material/styles'
import '@mui/material/Typography'
import '@mui/material/Button'
import '@mui/material/Chip'
import '@mui/material/IconButton'
import '@mui/material/Badge'
import '@mui/material/Alert'
// Typography variants and component overrides
declare module '@mui/material/styles' {
interface TypographyVariants {
code: React.CSSProperties
kbd: React.CSSProperties
label: React.CSSProperties
}
interface TypographyVariantsOptions {
code?: React.CSSProperties
kbd?: React.CSSProperties
label?: React.CSSProperties
}
}
declare module '@mui/material/Typography' {
interface TypographyPropsVariantOverrides {
code: true
kbd: true
label: true
}
}
declare module '@mui/material/Button' {
interface ButtonPropsVariantOverrides {
soft: true
ghost: true
}
interface ButtonPropsColorOverrides {
neutral: true
}
}
declare module '@mui/material/Chip' {
interface ChipPropsVariantOverrides {
soft: true
}
interface ChipPropsColorOverrides {
neutral: true
}
}
declare module '@mui/material/IconButton' {
interface IconButtonPropsColorOverrides {
neutral: true
}
}
declare module '@mui/material/Badge' {
interface BadgePropsColorOverrides {
neutral: true
}
}
declare module '@mui/material/Alert' {
interface AlertPropsVariantOverrides {
soft: true
}
}
export {}

View File

@@ -0,0 +1,70 @@
import '@mui/material/styles'
// Custom theme properties for layout and design tokens
declare module '@mui/material/styles' {
interface Theme {
custom: {
fonts: {
body: string
heading: string
mono: string
}
borderRadius: {
none: number
sm: number
md: number
lg: number
xl: number
full: number
}
contentWidth: {
sm: string
md: string
lg: string
xl: string
full: string
}
sidebar: {
width: number
collapsedWidth: number
}
header: {
height: number
}
}
}
interface ThemeOptions {
custom?: {
fonts?: {
body?: string
heading?: string
mono?: string
}
borderRadius?: {
none?: number
sm?: number
md?: number
lg?: number
xl?: number
full?: number
}
contentWidth?: {
sm?: string
md?: string
lg?: string
xl?: string
full?: string
}
sidebar?: {
width?: number
collapsedWidth?: number
}
header?: {
height?: number
}
}
}
}
export {}

View File

@@ -0,0 +1,38 @@
import '@mui/material/styles'
// Extend palette with custom neutral colors
declare module '@mui/material/styles' {
interface Palette {
neutral: {
50: string
100: string
200: string
300: string
400: string
500: string
600: string
700: string
800: string
900: string
950: string
}
}
interface PaletteOptions {
neutral?: {
50?: string
100?: string
200?: string
300?: string
400?: string
500?: string
600?: string
700?: string
800?: string
900?: string
950?: string
}
}
}
export {}

View File

@@ -1,200 +1,10 @@
/**
* MUI Theme Type Extensions
*
* This file extends Material-UI's theme interface with custom properties.
* All custom design tokens and component variants should be declared here.
*
* This file aggregates the theme augmentation modules to keep the
* main declaration lightweight while still exposing all custom tokens.
*/
import '@mui/material/styles'
import '@mui/material/Typography'
import '@mui/material/Button'
// ============================================================================
// Custom Palette Extensions
// ============================================================================
declare module '@mui/material/styles' {
// Extend palette with custom neutral colors
interface Palette {
neutral: {
50: string
100: string
200: string
300: string
400: string
500: string
600: string
700: string
800: string
900: string
950: string
}
}
interface PaletteOptions {
neutral?: {
50?: string
100?: string
200?: string
300?: string
400?: string
500?: string
600?: string
700?: string
800?: string
900?: string
950?: string
}
}
// Custom typography variants
interface TypographyVariants {
code: React.CSSProperties
kbd: React.CSSProperties
label: React.CSSProperties
}
interface TypographyVariantsOptions {
code?: React.CSSProperties
kbd?: React.CSSProperties
label?: React.CSSProperties
}
// Custom theme properties
interface Theme {
custom: {
fonts: {
body: string
heading: string
mono: string
}
borderRadius: {
none: number
sm: number
md: number
lg: number
xl: number
full: number
}
contentWidth: {
sm: string
md: string
lg: string
xl: string
full: string
}
sidebar: {
width: number
collapsedWidth: number
}
header: {
height: number
}
}
}
interface ThemeOptions {
custom?: {
fonts?: {
body?: string
heading?: string
mono?: string
}
borderRadius?: {
none?: number
sm?: number
md?: number
lg?: number
xl?: number
full?: number
}
contentWidth?: {
sm?: string
md?: string
lg?: string
xl?: string
full?: string
}
sidebar?: {
width?: number
collapsedWidth?: number
}
header?: {
height?: number
}
}
}
}
// ============================================================================
// Typography Variant Mapping
// ============================================================================
declare module '@mui/material/Typography' {
interface TypographyPropsVariantOverrides {
code: true
kbd: true
label: true
}
}
// ============================================================================
// Button Variants & Colors
// ============================================================================
declare module '@mui/material/Button' {
interface ButtonPropsVariantOverrides {
soft: true
ghost: true
}
interface ButtonPropsColorOverrides {
neutral: true
}
}
// ============================================================================
// Chip Variants
// ============================================================================
declare module '@mui/material/Chip' {
interface ChipPropsVariantOverrides {
soft: true
}
interface ChipPropsColorOverrides {
neutral: true
}
}
// ============================================================================
// IconButton Colors
// ============================================================================
declare module '@mui/material/IconButton' {
interface IconButtonPropsColorOverrides {
neutral: true
}
}
// ============================================================================
// Badge Colors
// ============================================================================
declare module '@mui/material/Badge' {
interface BadgePropsColorOverrides {
neutral: true
}
}
// ============================================================================
// Alert Variants
// ============================================================================
declare module '@mui/material/Alert' {
interface AlertPropsVariantOverrides {
soft: true
}
}
export {}
export * from './palette'
export * from './layout'
export * from './components'