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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user