mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-13 03:58:33 +00:00
fix: prettier formatting + remove unused import
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dc16d4473e
commit
e1fc420a77
4 changed files with 482 additions and 101 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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']],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue