feat: add security sink detection patterns for JavaScript/TypeScript

- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations).
- Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns.
- Added `package-lock.json` for dependency management.
This commit is contained in:
StellaOps Bot
2025-12-22 23:21:21 +02:00
parent 3ba7157b00
commit 5146204f1b
529 changed files with 73579 additions and 5985 deletions

View File

@@ -9,6 +9,10 @@ import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
import { join, extname, relative, dirname } from 'path';
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import { buildSinkLookup, matchSink } from './sink-detect.js';
// Pre-build sink lookup for fast detection
const sinkLookup = buildSinkLookup();
/**
* Main entry point
@@ -72,16 +76,18 @@ async function analyzeProject(projectPath) {
const nodes = [];
const edges = [];
const entrypoints = [];
const sinks = [];
for (const file of sourceFiles) {
try {
const content = readFileSync(file, 'utf-8');
const relativePath = relative(projectPath, file);
const result = analyzeFile(content, relativePath, packageInfo.name);
nodes.push(...result.nodes);
edges.push(...result.edges);
entrypoints.push(...result.entrypoints);
sinks.push(...result.sinks);
} catch (error) {
// Skip files that can't be parsed
console.error(`Warning: Could not parse ${file}: ${error.message}`);
@@ -93,7 +99,8 @@ async function analyzeProject(projectPath) {
version: packageInfo.version,
nodes: deduplicateNodes(nodes),
edges: deduplicateEdges(edges),
entrypoints
entrypoints,
sinks: deduplicateSinks(sinks)
};
}
@@ -142,6 +149,7 @@ function analyzeFile(content, relativePath, packageName) {
const nodes = [];
const edges = [];
const entrypoints = [];
const sinks = [];
const moduleBase = relativePath.replace(/\.[^.]+$/, '').replace(/\\/g, '/');
// Parse with Babel
@@ -273,13 +281,16 @@ function analyzeFile(content, relativePath, packageName) {
const callee = path.node.callee;
let targetId = null;
let objName = null;
let methodName = null;
if (callee.type === 'Identifier') {
targetId = `js:${packageName}/${moduleBase}.${callee.name}`;
methodName = callee.name;
} else if (callee.type === 'MemberExpression') {
const objName = callee.object?.name || 'unknown';
const propName = callee.property?.name || 'unknown';
targetId = `js:external/${objName}.${propName}`;
objName = callee.object?.name || 'unknown';
methodName = callee.property?.name || 'unknown';
targetId = `js:external/${objName}.${methodName}`;
}
if (targetId) {
@@ -294,12 +305,29 @@ function analyzeFile(content, relativePath, packageName) {
});
}
// Detect security sinks
if (methodName) {
const sinkMatch = matchSink(objName || methodName, methodName, sinkLookup);
if (sinkMatch) {
sinks.push({
caller: currentFunction,
category: sinkMatch.category,
method: `${objName ? objName + '.' : ''}${methodName}`,
site: {
file: relativePath,
line: path.node.loc?.start.line || 0,
column: path.node.loc?.start.column || 0
}
});
}
}
// Detect Express/Fastify route registration
detectRouteRegistration(path, entrypoints, packageName, moduleBase, relativePath);
}
});
return { nodes, edges, entrypoints };
return { nodes, edges, entrypoints, sinks };
}
/**
@@ -418,7 +446,7 @@ function deduplicateNodes(nodes) {
/**
* Remove duplicate edges
* @param {any[]} edges
* @param {any[]} edges
* @returns {any[]}
*/
function deduplicateEdges(edges) {
@@ -431,5 +459,20 @@ function deduplicateEdges(edges) {
});
}
/**
* Remove duplicate sinks
* @param {any[]} sinks
* @returns {any[]}
*/
function deduplicateSinks(sinks) {
const seen = new Set();
return sinks.filter(s => {
const key = `${s.caller}|${s.category}|${s.method}|${s.site.file}:${s.site.line}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
// Run
main().catch(console.error);