diff --git a/scanners/lib/policy-loader.mjs b/scanners/lib/policy-loader.mjs index 3690bd5..5759c79 100644 --- a/scanners/lib/policy-loader.mjs +++ b/scanners/lib/policy-loader.mjs @@ -89,6 +89,12 @@ const DEFAULT_POLICY = Object.freeze({ enabled_families: ['webshell', 'reverse_shell', 'cryptominer', 'hacktool'], custom_rules_path: null, }, + // AST — Python AST taint scanner (shells out to a parse-only python3 helper). + ast: { + enabled: true, + python_path: 'python3', + timeout_ms: 5000, + }, }); // Cache loaded policy per project root diff --git a/scanners/lib/severity.mjs b/scanners/lib/severity.mjs index 31b9eae..b162a78 100644 --- a/scanners/lib/severity.mjs +++ b/scanners/lib/severity.mjs @@ -145,6 +145,7 @@ export const OWASP_MAP = Object.freeze({ WFL: ['LLM02', 'LLM06'], TRG: ['LLM06'], SIG: ['LLM03', 'LLM02'], + AST: ['LLM01', 'LLM02'], }); /** @@ -166,6 +167,7 @@ export const OWASP_AGENTIC_MAP = Object.freeze({ WFL: ['ASI04'], TRG: [], SIG: ['ASI04'], + AST: [], }); /** @@ -187,6 +189,7 @@ export const OWASP_SKILLS_MAP = Object.freeze({ WFL: [], TRG: ['AST04'], SIG: [], + AST: ['AST02'], }); /** @@ -208,6 +211,7 @@ export const OWASP_MCP_MAP = Object.freeze({ WFL: [], TRG: [], SIG: [], + AST: [], }); /** diff --git a/scanners/scan-orchestrator.mjs b/scanners/scan-orchestrator.mjs index 04d35ac..c081890 100644 --- a/scanners/scan-orchestrator.mjs +++ b/scanners/scan-orchestrator.mjs @@ -111,6 +111,7 @@ import { scan as workflowScan } from './workflow-scanner.mjs'; import { scan as tfaScan } from './toxic-flow-analyzer.mjs'; import { scan as trgScan } from './trigger-scanner.mjs'; import { scan as sigScan } from './signature-scanner.mjs'; +import { scan as astScan } from './ast-taint-scanner.mjs'; const SCANNERS = [ { name: 'unicode', fn: unicodeScan }, @@ -125,6 +126,7 @@ const SCANNERS = [ { name: 'workflow', fn: workflowScan }, { name: 'trg', fn: trgScan }, { name: 'sig', fn: sigScan }, + { name: 'ast', fn: astScan }, { name: 'toxic-flow', fn: tfaScan, requiresPriorResults: true }, ]; diff --git a/tests/scanners/ast-taint-scanner.test.mjs b/tests/scanners/ast-taint-scanner.test.mjs index bae684e..4a88f7b 100644 --- a/tests/scanners/ast-taint-scanner.test.mjs +++ b/tests/scanners/ast-taint-scanner.test.mjs @@ -11,7 +11,7 @@ import { describe, it, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; import { resolve, join } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { existsSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { existsSync, mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { spawnSync } from 'node:child_process'; import { resetCounter } from '../../scanners/lib/output.mjs'; @@ -103,3 +103,52 @@ describe('ast-taint-scanner: malformed input resilience', () => { } }); }); + +// --------------------------------------------------------------------------- +// Wiring — OWASP map + orchestrator registration (Step 6) +// --------------------------------------------------------------------------- + +describe('ast-taint-scanner: OWASP map registration', () => { + it('AST is present in all four OWASP maps with the right codes', async () => { + const { OWASP_MAP, OWASP_AGENTIC_MAP, OWASP_SKILLS_MAP, OWASP_MCP_MAP } = + await import('../../scanners/lib/severity.mjs'); + assert.deepEqual(OWASP_MAP.AST, ['LLM01', 'LLM02']); + assert.deepEqual(OWASP_AGENTIC_MAP.AST, []); + assert.deepEqual(OWASP_SKILLS_MAP.AST, ['AST02']); + assert.deepEqual(OWASP_MCP_MAP.AST, []); + }); +}); + +describe('ast-taint-scanner: orchestrator registration', () => { + it('scan-orchestrator imports and lists the AST scanner', () => { + const orchPath = resolve(__dirname, '../../scanners/scan-orchestrator.mjs'); + const text = readFileSync(orchPath, 'utf8'); + assert.match(text, /import\s+\{\s*scan as astScan\s*\}\s+from\s+['"]\.\/ast-taint-scanner\.mjs['"]/); + assert.match(text, /\{\s*name:\s*'ast',\s*fn:\s*astScan\s*\}/); + }); +}); + +// --------------------------------------------------------------------------- +// Policy — enabled:false short-circuits to skipped (Step 6) +// --------------------------------------------------------------------------- + +describe('ast-taint-scanner: policy disable', () => { + it('enabled:false in policy.json short-circuits to skipped', async () => { + const dir = mkdtempSync(join(tmpdir(), 'ast-off-')); + try { + mkdirSync(join(dir, '.llm-security'), { recursive: true }); + writeFileSync( + join(dir, '.llm-security', 'policy.json'), + JSON.stringify({ ast: { enabled: false } }), + ); + writeFileSync(join(dir, 'a.py'), 'import os\nk = os.environ["X"]\neval(k)\n'); + resetCounter(); + const discovery = await discoverFiles(dir); + const result = await scan(dir, discovery); + assert.equal(result.status, 'skipped'); + assert.equal(result.findings.length, 0); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +});