diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index 2c1860a72..3dff4e0c9 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -3221,16 +3221,21 @@ export class LocalBackend { }; } - private async sourceSinkScan(repo: RepoHandle, params: { - max_depth?: number; - owasp?: string; - source_category?: string; - }): Promise { + private async sourceSinkScan( + repo: RepoHandle, + params: { + max_depth?: number; + owasp?: string; + source_category?: string; + }, + ): Promise { await this.ensureInitialized(repo.id); const maxDepth = params.max_depth ?? 5; // Step 1: Find all Function and Method nodes with their content. - const nodesResult = await executeQuery(repo.id, ` + const nodesResult = await executeQuery( + repo.id, + ` MATCH (n:Function) WHERE n.id IS NOT NULL RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.content AS content @@ -3238,7 +3243,8 @@ export class LocalBackend { MATCH (n:Method) WHERE n.id IS NOT NULL RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.content AS content - `); + `, + ); // Step 2: Load user-defined catalog extensions (if any) and merge with built-in catalogs const { @@ -3308,10 +3314,13 @@ export class LocalBackend { } // Step 4: Build CALLS adjacency map via Cypher - const callsResult = await executeQuery(repo.id, ` + const callsResult = await executeQuery( + repo.id, + ` MATCH (a)-[r:CodeRelation {type: 'CALLS'}]->(b) RETURN a.id AS sourceId, b.id AS targetId - `); + `, + ); const callsGraph = new Map(); for (const row of callsResult) { diff --git a/gitnexus/src/security/catalogs.ts b/gitnexus/src/security/catalogs.ts index 4663d07be..5496f7eff 100644 --- a/gitnexus/src/security/catalogs.ts +++ b/gitnexus/src/security/catalogs.ts @@ -41,70 +41,242 @@ export interface SinkEntry { export const SOURCE_CATALOG: SourceEntry[] = [ // HTTP request data (Next.js, Express, Koa, Fastify) - { pattern: 'request.json', category: 'user_input', description: 'Next.js request body (Request object)' }, - { pattern: 'req.json', category: 'user_input', description: 'Next.js request body (req shorthand)' }, + { + pattern: 'request.json', + category: 'user_input', + description: 'Next.js request body (Request object)', + }, + { + pattern: 'req.json', + category: 'user_input', + description: 'Next.js request body (req shorthand)', + }, { pattern: 'req.body', category: 'user_input', description: 'Express request body' }, { pattern: 'req.query', category: 'user_input', description: 'Express query parameters' }, { pattern: 'req.params', category: 'user_input', description: 'Express route parameters' }, { pattern: 'req.headers', category: 'user_input', description: 'HTTP request headers' }, - { pattern: 'request.GET', category: 'user_input', languages: ['python'], description: 'Django GET params' }, - { pattern: 'request.POST', category: 'user_input', languages: ['python'], description: 'Django POST data' }, - { pattern: 'request.data', category: 'user_input', languages: ['python'], description: 'DRF request data' }, - { pattern: '$_GET', category: 'user_input', languages: ['php'], description: 'PHP GET superglobal' }, - { pattern: '$_POST', category: 'user_input', languages: ['php'], description: 'PHP POST superglobal' }, - { pattern: '$_REQUEST', category: 'user_input', languages: ['php'], description: 'PHP REQUEST superglobal' }, - { pattern: 'request.form', category: 'user_input', languages: ['python'], description: 'Flask form data' }, - { pattern: 'request.args', category: 'user_input', languages: ['python'], description: 'Flask query args' }, - { pattern: 'nextUrl.searchParams', category: 'user_input', description: 'Next.js URL search params' }, + { + pattern: 'request.GET', + category: 'user_input', + languages: ['python'], + description: 'Django GET params', + }, + { + pattern: 'request.POST', + category: 'user_input', + languages: ['python'], + description: 'Django POST data', + }, + { + pattern: 'request.data', + category: 'user_input', + languages: ['python'], + description: 'DRF request data', + }, + { + pattern: '$_GET', + category: 'user_input', + languages: ['php'], + description: 'PHP GET superglobal', + }, + { + pattern: '$_POST', + category: 'user_input', + languages: ['php'], + description: 'PHP POST superglobal', + }, + { + pattern: '$_REQUEST', + category: 'user_input', + languages: ['php'], + description: 'PHP REQUEST superglobal', + }, + { + pattern: 'request.form', + category: 'user_input', + languages: ['python'], + description: 'Flask form data', + }, + { + pattern: 'request.args', + category: 'user_input', + languages: ['python'], + description: 'Flask query args', + }, + { + pattern: 'nextUrl.searchParams', + category: 'user_input', + description: 'Next.js URL search params', + }, // Go (net/http) - { pattern: 'r.Body', category: 'user_input', languages: ['go'], description: 'Go HTTP request body' }, - { pattern: 'r.URL.Query()', category: 'user_input', languages: ['go'], description: 'Go URL query parameters' }, - { pattern: 'r.FormValue', category: 'user_input', languages: ['go'], description: 'Go form value' }, - { pattern: 'r.Header.Get', category: 'user_input', languages: ['go'], description: 'Go request header' }, + { + pattern: 'r.Body', + category: 'user_input', + languages: ['go'], + description: 'Go HTTP request body', + }, + { + pattern: 'r.URL.Query()', + category: 'user_input', + languages: ['go'], + description: 'Go URL query parameters', + }, + { + pattern: 'r.FormValue', + category: 'user_input', + languages: ['go'], + description: 'Go form value', + }, + { + pattern: 'r.Header.Get', + category: 'user_input', + languages: ['go'], + description: 'Go request header', + }, // Rust / Actix-web - { pattern: 'web::Json', category: 'user_input', languages: ['rust'], description: 'Actix-web JSON extractor' }, - { pattern: 'web::Query', category: 'user_input', languages: ['rust'], description: 'Actix-web query extractor' }, - { pattern: 'web::Path', category: 'user_input', languages: ['rust'], description: 'Actix-web path extractor' }, + { + pattern: 'web::Json', + category: 'user_input', + languages: ['rust'], + description: 'Actix-web JSON extractor', + }, + { + pattern: 'web::Query', + category: 'user_input', + languages: ['rust'], + description: 'Actix-web query extractor', + }, + { + pattern: 'web::Path', + category: 'user_input', + languages: ['rust'], + description: 'Actix-web path extractor', + }, // Spring (Java/Kotlin) - { pattern: '@RequestBody', category: 'user_input', languages: ['java', 'kotlin'], description: 'Spring request body annotation' }, - { pattern: '@RequestParam', category: 'user_input', languages: ['java', 'kotlin'], description: 'Spring request parameter annotation' }, - { pattern: '@PathVariable', category: 'user_input', languages: ['java', 'kotlin'], description: 'Spring path variable annotation' }, + { + pattern: '@RequestBody', + category: 'user_input', + languages: ['java', 'kotlin'], + description: 'Spring request body annotation', + }, + { + pattern: '@RequestParam', + category: 'user_input', + languages: ['java', 'kotlin'], + description: 'Spring request parameter annotation', + }, + { + pattern: '@PathVariable', + category: 'user_input', + languages: ['java', 'kotlin'], + description: 'Spring path variable annotation', + }, // Rails (Ruby) - { pattern: 'params[', category: 'user_input', languages: ['ruby'], description: 'Rails params hash access' }, - { pattern: 'request.body', category: 'user_input', languages: ['ruby'], description: 'Rails raw request body' }, + { + pattern: 'params[', + category: 'user_input', + languages: ['ruby'], + description: 'Rails params hash access', + }, + { + pattern: 'request.body', + category: 'user_input', + languages: ['ruby'], + description: 'Rails raw request body', + }, // Kotlin / Ktor - { pattern: 'call.receive', category: 'user_input', languages: ['kotlin'], description: 'Ktor request body receive' }, - { pattern: 'call.parameters', category: 'user_input', languages: ['kotlin'], description: 'Ktor request parameters' }, + { + pattern: 'call.receive', + category: 'user_input', + languages: ['kotlin'], + description: 'Ktor request body receive', + }, + { + pattern: 'call.parameters', + category: 'user_input', + languages: ['kotlin'], + description: 'Ktor request parameters', + }, // FastAPI (Python) - { pattern: 'async def endpoint', category: 'user_input', languages: ['python'], description: 'FastAPI auto-injected endpoint parameter' }, + { + pattern: 'async def endpoint', + category: 'user_input', + languages: ['python'], + description: 'FastAPI auto-injected endpoint parameter', + }, // Environment { pattern: 'process.env', category: 'environment', description: 'Node.js env variable' }, - { pattern: 'os.environ', category: 'environment', languages: ['python'], description: 'Python env variable' }, - { pattern: 'getenv', category: 'environment', languages: ['php'], description: 'PHP env variable' }, - { pattern: 'os.Getenv', category: 'environment', languages: ['go'], description: 'Go env variable' }, - { pattern: 'std::env::var', category: 'environment', languages: ['rust'], description: 'Rust env variable' }, - { pattern: 'System.getenv', category: 'environment', languages: ['java', 'kotlin'], description: 'Java/Kotlin env variable' }, - { pattern: 'ENV[', category: 'environment', languages: ['ruby'], description: 'Ruby env variable' }, + { + pattern: 'os.environ', + category: 'environment', + languages: ['python'], + description: 'Python env variable', + }, + { + pattern: 'getenv', + category: 'environment', + languages: ['php'], + description: 'PHP env variable', + }, + { + pattern: 'os.Getenv', + category: 'environment', + languages: ['go'], + description: 'Go env variable', + }, + { + pattern: 'std::env::var', + category: 'environment', + languages: ['rust'], + description: 'Rust env variable', + }, + { + pattern: 'System.getenv', + category: 'environment', + languages: ['java', 'kotlin'], + description: 'Java/Kotlin env variable', + }, + { + pattern: 'ENV[', + category: 'environment', + languages: ['ruby'], + description: 'Ruby env variable', + }, // File reads { pattern: 'readFile', category: 'file_read', description: 'File read operation' }, { pattern: 'readFileSync', category: 'file_read', description: 'Sync file read' }, { pattern: 'os.ReadFile', category: 'file_read', languages: ['go'], description: 'Go file read' }, - { pattern: 'std::fs::read', category: 'file_read', languages: ['rust'], description: 'Rust file read' }, + { + pattern: 'std::fs::read', + category: 'file_read', + languages: ['rust'], + description: 'Rust file read', + }, // Network input { pattern: 'fetch(', category: 'network', description: 'Fetch API response' }, { pattern: 'axios.get', category: 'network', description: 'Axios HTTP response' }, { pattern: 'axios.post', category: 'network', description: 'Axios HTTP response' }, - { pattern: 'http.Get', category: 'network', languages: ['go'], description: 'Go HTTP client GET' }, - { pattern: 'reqwest::get', category: 'network', languages: ['rust'], description: 'Rust reqwest HTTP GET' }, + { + pattern: 'http.Get', + category: 'network', + languages: ['go'], + description: 'Go HTTP client GET', + }, + { + pattern: 'reqwest::get', + category: 'network', + languages: ['rust'], + description: 'Rust reqwest HTTP GET', + }, ]; // ── Sink Catalog ── @@ -112,52 +284,195 @@ export const SOURCE_CATALOG: SourceEntry[] = [ export const SINK_CATALOG: SinkEntry[] = [ // A03: Injection — SQL { pattern: 'query', owasp: 'A03-injection', severity: 'critical', description: 'Raw SQL query' }, - { pattern: '$queryRaw', owasp: 'A03-injection', severity: 'critical', description: 'Prisma raw query' }, - { pattern: '$executeRaw', owasp: 'A03-injection', severity: 'critical', description: 'Prisma raw execute' }, - { pattern: 'rawQuery', owasp: 'A03-injection', severity: 'critical', description: 'Sequelize raw query' }, + { + pattern: '$queryRaw', + owasp: 'A03-injection', + severity: 'critical', + description: 'Prisma raw query', + }, + { + pattern: '$executeRaw', + owasp: 'A03-injection', + severity: 'critical', + description: 'Prisma raw execute', + }, + { + pattern: 'rawQuery', + owasp: 'A03-injection', + severity: 'critical', + description: 'Sequelize raw query', + }, // A03: Injection — Command - { pattern: 'exec', owasp: 'A03-injection', severity: 'critical', description: 'Command execution' }, - { pattern: 'execSync', owasp: 'A03-injection', severity: 'critical', description: 'Sync command execution' }, + { + pattern: 'exec', + owasp: 'A03-injection', + severity: 'critical', + description: 'Command execution', + }, + { + pattern: 'execSync', + owasp: 'A03-injection', + severity: 'critical', + description: 'Sync command execution', + }, { pattern: 'spawn', owasp: 'A03-injection', severity: 'high', description: 'Process spawn' }, { pattern: 'eval', owasp: 'A03-injection', severity: 'critical', description: 'Code evaluation' }, - { pattern: 'Function(', owasp: 'A03-injection', severity: 'critical', description: 'Dynamic function creation' }, - { pattern: 'subprocess.run', owasp: 'A03-injection', severity: 'critical', languages: ['python'], description: 'Python subprocess' }, - { pattern: 'os.system', owasp: 'A03-injection', severity: 'critical', languages: ['python'], description: 'Python system call' }, - { pattern: 'shell_exec', owasp: 'A03-injection', severity: 'critical', languages: ['php'], description: 'PHP shell exec' }, + { + pattern: 'Function(', + owasp: 'A03-injection', + severity: 'critical', + description: 'Dynamic function creation', + }, + { + pattern: 'subprocess.run', + owasp: 'A03-injection', + severity: 'critical', + languages: ['python'], + description: 'Python subprocess', + }, + { + pattern: 'os.system', + owasp: 'A03-injection', + severity: 'critical', + languages: ['python'], + description: 'Python system call', + }, + { + pattern: 'shell_exec', + owasp: 'A03-injection', + severity: 'critical', + languages: ['php'], + description: 'PHP shell exec', + }, // A03: Injection — Go - { pattern: 'os.exec', owasp: 'A03-injection', severity: 'critical', languages: ['go'], description: 'Go command execution' }, - { pattern: 'sql.Query', owasp: 'A03-injection', severity: 'critical', languages: ['go'], description: 'Go raw SQL query' }, + { + pattern: 'os.exec', + owasp: 'A03-injection', + severity: 'critical', + languages: ['go'], + description: 'Go command execution', + }, + { + pattern: 'sql.Query', + owasp: 'A03-injection', + severity: 'critical', + languages: ['go'], + description: 'Go raw SQL query', + }, // A03: Injection — Rust - { pattern: 'Command::new', owasp: 'A03-injection', severity: 'critical', languages: ['rust'], description: 'Rust command execution' }, - { pattern: 'sqlx::query', owasp: 'A03-injection', severity: 'critical', languages: ['rust'], description: 'Rust sqlx raw query' }, + { + pattern: 'Command::new', + owasp: 'A03-injection', + severity: 'critical', + languages: ['rust'], + description: 'Rust command execution', + }, + { + pattern: 'sqlx::query', + owasp: 'A03-injection', + severity: 'critical', + languages: ['rust'], + description: 'Rust sqlx raw query', + }, // A03: Injection — Spring (Java/Kotlin) - { pattern: 'jdbcTemplate.query', owasp: 'A03-injection', severity: 'critical', languages: ['java', 'kotlin'], description: 'Spring JDBC raw query' }, - { pattern: 'Runtime.exec', owasp: 'A03-injection', severity: 'critical', languages: ['java', 'kotlin'], description: 'Java runtime command execution' }, + { + pattern: 'jdbcTemplate.query', + owasp: 'A03-injection', + severity: 'critical', + languages: ['java', 'kotlin'], + description: 'Spring JDBC raw query', + }, + { + pattern: 'Runtime.exec', + owasp: 'A03-injection', + severity: 'critical', + languages: ['java', 'kotlin'], + description: 'Java runtime command execution', + }, // A03: Injection — Rails (Ruby) - { pattern: 'system(', owasp: 'A03-injection', severity: 'critical', languages: ['ruby'], description: 'Ruby system command execution' }, - { pattern: 'ActiveRecord::Base.connection.execute', owasp: 'A03-injection', severity: 'critical', languages: ['ruby'], description: 'Rails raw SQL execution' }, + { + pattern: 'system(', + owasp: 'A03-injection', + severity: 'critical', + languages: ['ruby'], + description: 'Ruby system command execution', + }, + { + pattern: 'ActiveRecord::Base.connection.execute', + owasp: 'A03-injection', + severity: 'critical', + languages: ['ruby'], + description: 'Rails raw SQL execution', + }, // A07: XSS - { pattern: 'innerHTML', owasp: 'A07-xss', severity: 'high', description: 'Direct HTML injection' }, - { pattern: 'dangerouslySetInnerHTML', owasp: 'A07-xss', severity: 'high', description: 'React unsafe HTML' }, + { + pattern: 'innerHTML', + owasp: 'A07-xss', + severity: 'high', + description: 'Direct HTML injection', + }, + { + pattern: 'dangerouslySetInnerHTML', + owasp: 'A07-xss', + severity: 'high', + description: 'React unsafe HTML', + }, { pattern: 'document.write', owasp: 'A07-xss', severity: 'high', description: 'Document write' }, - { pattern: 'template.HTML', owasp: 'A07-xss', severity: 'high', languages: ['go'], description: 'Go template unescaped HTML' }, + { + pattern: 'template.HTML', + owasp: 'A07-xss', + severity: 'high', + languages: ['go'], + description: 'Go template unescaped HTML', + }, // A10: SSRF - { pattern: 'fetch(', owasp: 'A10-ssrf', severity: 'high', description: 'Server-side fetch with user URL' }, + { + pattern: 'fetch(', + owasp: 'A10-ssrf', + severity: 'high', + description: 'Server-side fetch with user URL', + }, { pattern: 'axios(', owasp: 'A10-ssrf', severity: 'high', description: 'Axios with user URL' }, - { pattern: 'http.get', owasp: 'A10-ssrf', severity: 'high', description: 'HTTP client with user URL' }, - { pattern: 'urllib.request', owasp: 'A10-ssrf', severity: 'high', languages: ['python'], description: 'Python URL request' }, + { + pattern: 'http.get', + owasp: 'A10-ssrf', + severity: 'high', + description: 'HTTP client with user URL', + }, + { + pattern: 'urllib.request', + owasp: 'A10-ssrf', + severity: 'high', + languages: ['python'], + description: 'Python URL request', + }, // Database writes (ORM — not injection per se, but data integrity sinks) - { pattern: 'prisma.', owasp: 'A03-injection', severity: 'medium', description: 'Prisma ORM operation (check for raw queries)' }, - { pattern: '.create(', owasp: 'A03-injection', severity: 'medium', description: 'ORM create operation' }, - { pattern: '.update(', owasp: 'A03-injection', severity: 'medium', description: 'ORM update operation' }, + { + pattern: 'prisma.', + owasp: 'A03-injection', + severity: 'medium', + description: 'Prisma ORM operation (check for raw queries)', + }, + { + pattern: '.create(', + owasp: 'A03-injection', + severity: 'medium', + description: 'ORM create operation', + }, + { + pattern: '.update(', + owasp: 'A03-injection', + severity: 'medium', + description: 'ORM update operation', + }, ]; // ── User-extensible catalog loading ── @@ -237,7 +552,7 @@ export function mergeCatalogs(userConfig: UserSecurityConfig | null): { * Compile an array of catalog entries into regex patterns for matching. */ export function compilePatterns(entries: T[]): CompiledPattern[] { - return entries.map(entry => ({ + return entries.map((entry) => ({ regex: new RegExp(escapeRegex(entry.pattern), 'i'), entry, })); @@ -269,32 +584,44 @@ function matchPatterns( content: string, language?: string, ): T[] { - return patterns - .filter(p => patternMatches(p, content, language)) - .map(({ entry }) => entry); + return patterns.filter((p) => patternMatches(p, content, language)).map(({ entry }) => entry); } /** * Check if a function's content contains source patterns (user input reads). * Optionally accepts custom compiled patterns (e.g. merged with user config). */ -export function isSourceAdjacent(_functionName: string, content: string, language?: string, customPatterns?: CompiledPattern[]): boolean { - return (customPatterns ?? SOURCE_REGEXES).some(p => patternMatches(p, content, language)); +export function isSourceAdjacent( + _functionName: string, + content: string, + language?: string, + customPatterns?: CompiledPattern[], +): boolean { + return (customPatterns ?? SOURCE_REGEXES).some((p) => patternMatches(p, content, language)); } /** * Check if a function's content contains sink patterns (dangerous operations). * Optionally accepts custom compiled patterns (e.g. merged with user config). */ -export function isSinkAdjacent(_functionName: string, content: string, language?: string, customPatterns?: CompiledPattern[]): boolean { - return (customPatterns ?? SINK_REGEXES).some(p => patternMatches(p, content, language)); +export function isSinkAdjacent( + _functionName: string, + content: string, + language?: string, + customPatterns?: CompiledPattern[], +): boolean { + return (customPatterns ?? SINK_REGEXES).some((p) => patternMatches(p, content, language)); } /** * Get matching sink entries for a function's content (for reporting). * Optionally accepts custom compiled patterns (e.g. merged with user config). */ -export function getMatchingSinks(content: string, language?: string, customPatterns?: CompiledPattern[]): SinkEntry[] { +export function getMatchingSinks( + content: string, + language?: string, + customPatterns?: CompiledPattern[], +): SinkEntry[] { return matchPatterns(customPatterns ?? SINK_REGEXES, content, language); } @@ -302,7 +629,11 @@ export function getMatchingSinks(content: string, language?: string, customPatte * Get matching source entries for a function's content (for reporting). * Optionally accepts custom compiled patterns (e.g. merged with user config). */ -export function getMatchingSources(content: string, language?: string, customPatterns?: CompiledPattern[]): SourceEntry[] { +export function getMatchingSources( + content: string, + language?: string, + customPatterns?: CompiledPattern[], +): SourceEntry[] { return matchPatterns(customPatterns ?? SOURCE_REGEXES, content, language); } diff --git a/gitnexus/src/security/source-sink-scanner.ts b/gitnexus/src/security/source-sink-scanner.ts index 490b41750..6816ad738 100644 --- a/gitnexus/src/security/source-sink-scanner.ts +++ b/gitnexus/src/security/source-sink-scanner.ts @@ -54,7 +54,7 @@ export function buildSourceSinkPaths( callsGraph: Map, maxDepth: number = 5, ): SourceSinkPath[] { - const sinkMap = new Map(sinks.map(s => [s.id, s])); + const sinkMap = new Map(sinks.map((s) => [s.id, s])); const results: SourceSinkPath[] = []; for (const source of sources) { diff --git a/gitnexus/test/unit/source-sink-scanner.test.ts b/gitnexus/test/unit/source-sink-scanner.test.ts index b9ece117d..fb0ec2d80 100644 --- a/gitnexus/test/unit/source-sink-scanner.test.ts +++ b/gitnexus/test/unit/source-sink-scanner.test.ts @@ -10,11 +10,11 @@ import { getMatchingSinks, type UserSecurityConfig, } from '../../src/security/catalogs.js'; -import { buildSourceSinkPaths, type SourceSinkPath } from '../../src/security/source-sink-scanner.js'; +import { buildSourceSinkPaths } from '../../src/security/source-sink-scanner.js'; describe('SOURCE_CATALOG', () => { it('contains user input sources', () => { - const names = SOURCE_CATALOG.map(s => s.pattern); + const names = SOURCE_CATALOG.map((s) => s.pattern); expect(names).toContain('request.json'); expect(names).toContain('req.body'); expect(names).toContain('request.GET'); @@ -27,7 +27,7 @@ describe('SOURCE_CATALOG', () => { }); it('contains Go sources', () => { - const patterns = SOURCE_CATALOG.map(s => s.pattern); + const patterns = SOURCE_CATALOG.map((s) => s.pattern); expect(patterns).toContain('r.Body'); expect(patterns).toContain('r.URL.Query()'); expect(patterns).toContain('r.FormValue'); @@ -35,26 +35,26 @@ describe('SOURCE_CATALOG', () => { }); it('contains Rust/Actix sources', () => { - const patterns = SOURCE_CATALOG.map(s => s.pattern); + const patterns = SOURCE_CATALOG.map((s) => s.pattern); expect(patterns).toContain('web::Json'); expect(patterns).toContain('web::Query'); expect(patterns).toContain('web::Path'); }); it('contains Spring annotation sources', () => { - const patterns = SOURCE_CATALOG.map(s => s.pattern); + const patterns = SOURCE_CATALOG.map((s) => s.pattern); expect(patterns).toContain('@RequestBody'); expect(patterns).toContain('@RequestParam'); expect(patterns).toContain('@PathVariable'); }); it('contains Rails sources', () => { - const patterns = SOURCE_CATALOG.map(s => s.pattern); + const patterns = SOURCE_CATALOG.map((s) => s.pattern); expect(patterns).toContain('params['); }); it('contains Ktor sources', () => { - const patterns = SOURCE_CATALOG.map(s => s.pattern); + const patterns = SOURCE_CATALOG.map((s) => s.pattern); expect(patterns).toContain('call.receive'); expect(patterns).toContain('call.parameters'); }); @@ -62,7 +62,7 @@ describe('SOURCE_CATALOG', () => { describe('SINK_CATALOG', () => { it('contains dangerous sinks', () => { - const names = SINK_CATALOG.map(s => s.pattern); + const names = SINK_CATALOG.map((s) => s.pattern); expect(names).toContain('eval'); expect(names).toContain('exec'); expect(names).toContain('innerHTML'); @@ -75,26 +75,26 @@ describe('SINK_CATALOG', () => { }); it('contains Go sinks', () => { - const patterns = SINK_CATALOG.map(s => s.pattern); + const patterns = SINK_CATALOG.map((s) => s.pattern); expect(patterns).toContain('os.exec'); expect(patterns).toContain('sql.Query'); expect(patterns).toContain('template.HTML'); }); it('contains Rust sinks', () => { - const patterns = SINK_CATALOG.map(s => s.pattern); + const patterns = SINK_CATALOG.map((s) => s.pattern); expect(patterns).toContain('Command::new'); expect(patterns).toContain('sqlx::query'); }); it('contains Spring sinks', () => { - const patterns = SINK_CATALOG.map(s => s.pattern); + const patterns = SINK_CATALOG.map((s) => s.pattern); expect(patterns).toContain('jdbcTemplate.query'); expect(patterns).toContain('Runtime.exec'); }); it('contains Rails sinks', () => { - const patterns = SINK_CATALOG.map(s => s.pattern); + const patterns = SINK_CATALOG.map((s) => s.pattern); expect(patterns).toContain('system('); expect(patterns).toContain('ActiveRecord::Base.connection.execute'); }); @@ -170,7 +170,12 @@ describe('mergeCatalogs', () => { it('merges user-defined sinks with built-in catalog', () => { const userConfig: UserSecurityConfig = { sinks: [ - { pattern: 'dangerousOp', owasp: 'A03-injection', severity: 'high', description: 'Custom sink' }, + { + pattern: 'dangerousOp', + owasp: 'A03-injection', + severity: 'high', + description: 'Custom sink', + }, ], }; const result = mergeCatalogs(userConfig); @@ -184,7 +189,12 @@ describe('mergeCatalogs', () => { { pattern: 'myCustomInput', category: 'user_input', description: 'Custom input source' }, ], sinks: [ - { pattern: 'dangerousOp', owasp: 'A03-injection', severity: 'high', description: 'Custom sink' }, + { + pattern: 'dangerousOp', + owasp: 'A03-injection', + severity: 'high', + description: 'Custom sink', + }, ], }; const merged = mergeCatalogs(userConfig); @@ -194,11 +204,11 @@ describe('mergeCatalogs', () => { // User-defined source should be detected const content = `function handle() { const data = myCustomInput(); dangerousOp(data); }`; const matchedSources = getMatchingSources(content, undefined, compiledSources); - expect(matchedSources.some(s => s.pattern === 'myCustomInput')).toBe(true); + expect(matchedSources.some((s) => s.pattern === 'myCustomInput')).toBe(true); // User-defined sink should be detected const matchedSinks = getMatchingSinks(content, undefined, compiledSinks); - expect(matchedSinks.some(s => s.pattern === 'dangerousOp')).toBe(true); + expect(matchedSinks.some((s) => s.pattern === 'dangerousOp')).toBe(true); }); it('does not modify built-in catalog arrays', () => { @@ -215,8 +225,23 @@ describe('mergeCatalogs', () => { describe('buildSourceSinkPaths', () => { it('finds path from source to sink through CALLS chain', () => { - const sources = [{ id: 'func:handlePOST', name: 'handlePOST', filePath: 'route.ts', sourcePatterns: ['req.body'] }]; - const sinks = [{ id: 'func:createGrant', name: 'createGrant', filePath: 'service.ts', sinkPatterns: ['prisma.'], owasp: 'A03-injection' as const }]; + const sources = [ + { + id: 'func:handlePOST', + name: 'handlePOST', + filePath: 'route.ts', + sourcePatterns: ['req.body'], + }, + ]; + const sinks = [ + { + id: 'func:createGrant', + name: 'createGrant', + filePath: 'service.ts', + sinkPatterns: ['prisma.'], + owasp: 'A03-injection' as const, + }, + ]; const callsGraph = new Map([ ['func:handlePOST', ['func:validateInput']], ['func:validateInput', ['func:createGrant']], @@ -232,7 +257,15 @@ describe('buildSourceSinkPaths', () => { it('returns empty when no path exists', () => { const sources = [{ id: 'func:a', name: 'a', filePath: 'a.ts', sourcePatterns: ['req.body'] }]; - const sinks = [{ id: 'func:z', name: 'z', filePath: 'z.ts', sinkPatterns: ['eval'], owasp: 'A03-injection' as const }]; + const sinks = [ + { + id: 'func:z', + name: 'z', + filePath: 'z.ts', + sinkPatterns: ['eval'], + owasp: 'A03-injection' as const, + }, + ]; const callsGraph = new Map([ ['func:a', ['func:b']], // func:b doesn't call func:z @@ -244,7 +277,15 @@ describe('buildSourceSinkPaths', () => { it('respects maxDepth', () => { const sources = [{ id: 'func:a', name: 'a', filePath: 'a.ts', sourcePatterns: ['req.body'] }]; - const sinks = [{ id: 'func:d', name: 'd', filePath: 'd.ts', sinkPatterns: ['eval'], owasp: 'A03-injection' as const }]; + const sinks = [ + { + id: 'func:d', + name: 'd', + filePath: 'd.ts', + sinkPatterns: ['eval'], + owasp: 'A03-injection' as const, + }, + ]; const callsGraph = new Map([ ['func:a', ['func:b']], ['func:b', ['func:c']],