diff --git a/frontends/nextjs/src/lib/security/scanner/__tests__/security-scanner.detection.test.ts b/frontends/nextjs/src/lib/security/scanner/__tests__/security-scanner.detection.test.ts
new file mode 100644
index 000000000..a910bea9d
--- /dev/null
+++ b/frontends/nextjs/src/lib/security/scanner/__tests__/security-scanner.detection.test.ts
@@ -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: '
',
+ expectedSeverity: 'critical',
+ expectedSafe: false,
+ },
+ {
+ name: 'flag inline handlers as high',
+ html: '',
+ expectedSeverity: 'high',
+ expectedSafe: false,
+ },
+ {
+ name: 'return safe for plain markup',
+ html: 'Safe
',
+ 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: 'Click
x',
+ type: 'text' as const,
+ shouldExclude: ['',
+ type: 'html' as const,
+ shouldExclude: ['data:text/html', '',
+ 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)
+ })
+ })
+})
diff --git a/frontends/nextjs/src/lib/security/scanner/__tests__/security-scanner.reporting.test.ts b/frontends/nextjs/src/lib/security/scanner/__tests__/security-scanner.reporting.test.ts
new file mode 100644
index 000000000..42f9a2dd2
--- /dev/null
+++ b/frontends/nextjs/src/lib/security/scanner/__tests__/security-scanner.reporting.test.ts
@@ -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)
+ })
+ })
+})
diff --git a/frontends/nextjs/src/lib/security/scanner/security-scanner.test.ts b/frontends/nextjs/src/lib/security/scanner/security-scanner.test.ts
index 142a8997b..0e9fcbd80 100644
--- a/frontends/nextjs/src/lib/security/scanner/security-scanner.test.ts
+++ b/frontends/nextjs/src/lib/security/scanner/security-scanner.test.ts
@@ -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: '',
- expectedSeverity: 'critical',
- expectedSafe: false,
- },
- {
- name: 'flag inline handlers as high',
- html: '',
- expectedSeverity: 'high',
- expectedSafe: false,
- },
- {
- name: 'return safe for plain markup',
- html: 'Safe
',
- 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: 'Click
x',
- type: 'text' as const,
- shouldExclude: ['',
- type: 'html' as const,
- shouldExclude: ['data:text/html', '',
- 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'