fix: prettier formatting + remove unused import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test 2026-03-28 21:35:01 +01:00
parent dc16d4473e
commit e1fc420a77
4 changed files with 482 additions and 101 deletions

View file

@ -3221,16 +3221,21 @@ export class LocalBackend {
};
}
private async sourceSinkScan(repo: RepoHandle, params: {
max_depth?: number;
owasp?: string;
source_category?: string;
}): Promise<any> {
private async sourceSinkScan(
repo: RepoHandle,
params: {
max_depth?: number;
owasp?: string;
source_category?: string;
},
): Promise<any> {
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<string, string[]>();
for (const row of callsResult) {

View file

@ -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<T extends { pattern: string }>(entries: T[]): CompiledPattern<T>[] {
return entries.map(entry => ({
return entries.map((entry) => ({
regex: new RegExp(escapeRegex(entry.pattern), 'i'),
entry,
}));
@ -269,32 +584,44 @@ function matchPatterns<T extends { languages?: string[] }>(
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<SourceEntry>[]): boolean {
return (customPatterns ?? SOURCE_REGEXES).some(p => patternMatches(p, content, language));
export function isSourceAdjacent(
_functionName: string,
content: string,
language?: string,
customPatterns?: CompiledPattern<SourceEntry>[],
): 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<SinkEntry>[]): boolean {
return (customPatterns ?? SINK_REGEXES).some(p => patternMatches(p, content, language));
export function isSinkAdjacent(
_functionName: string,
content: string,
language?: string,
customPatterns?: CompiledPattern<SinkEntry>[],
): 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>[]): SinkEntry[] {
export function getMatchingSinks(
content: string,
language?: string,
customPatterns?: CompiledPattern<SinkEntry>[],
): 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>[]): SourceEntry[] {
export function getMatchingSources(
content: string,
language?: string,
customPatterns?: CompiledPattern<SourceEntry>[],
): SourceEntry[] {
return matchPatterns(customPatterns ?? SOURCE_REGEXES, content, language);
}

View file

@ -54,7 +54,7 @@ export function buildSourceSinkPaths(
callsGraph: Map<string, string[]>,
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) {

View file

@ -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']],