fivosense 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/.github/workflows/ci.yml +52 -0
- package/BLUEPRINT.md +215 -0
- package/BUILD_PLAN.md +175 -0
- package/CONTRIBUTING.md +80 -0
- package/DOCS_VERIFICATION.md +232 -0
- package/FINAL_CHECKLIST.md +263 -0
- package/FINAL_SUMMARY.md +238 -0
- package/GITHUB_PUSH.md +64 -0
- package/LICENSE +21 -0
- package/PROGRESS.md +153 -0
- package/README.md +443 -0
- package/RELEASE_READY.md +201 -0
- package/SECURITY.md +211 -0
- package/SECURITY_DEEP_AUDIT.md +331 -0
- package/TODO.md +52 -0
- package/dist/ai/judge.d.ts +36 -0
- package/dist/ai/judge.d.ts.map +1 -0
- package/dist/ai/judge.js +75 -0
- package/dist/ai/judge.js.map +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +39 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/editors/vscode.d.ts +30 -0
- package/dist/editors/vscode.d.ts.map +1 -0
- package/dist/editors/vscode.js +103 -0
- package/dist/editors/vscode.js.map +1 -0
- package/dist/engine/adversary.d.ts +24 -0
- package/dist/engine/adversary.d.ts.map +1 -0
- package/dist/engine/adversary.js +83 -0
- package/dist/engine/adversary.js.map +1 -0
- package/dist/engine/graph.d.ts +38 -0
- package/dist/engine/graph.d.ts.map +1 -0
- package/dist/engine/graph.js +131 -0
- package/dist/engine/graph.js.map +1 -0
- package/dist/engine/reach.d.ts +22 -0
- package/dist/engine/reach.d.ts.map +1 -0
- package/dist/engine/reach.js +107 -0
- package/dist/engine/reach.js.map +1 -0
- package/dist/engine/sinks.d.ts +52 -0
- package/dist/engine/sinks.d.ts.map +1 -0
- package/dist/engine/sinks.js +96 -0
- package/dist/engine/sinks.js.map +1 -0
- package/dist/engine/sources.d.ts +35 -0
- package/dist/engine/sources.d.ts.map +1 -0
- package/dist/engine/sources.js +59 -0
- package/dist/engine/sources.js.map +1 -0
- package/dist/engine/taint.d.ts +37 -0
- package/dist/engine/taint.d.ts.map +1 -0
- package/dist/engine/taint.js +83 -0
- package/dist/engine/taint.js.map +1 -0
- package/dist/engine/verify.d.ts +20 -0
- package/dist/engine/verify.d.ts.map +1 -0
- package/dist/engine/verify.js +65 -0
- package/dist/engine/verify.js.map +1 -0
- package/dist/features/badge.d.ts +20 -0
- package/dist/features/badge.d.ts.map +1 -0
- package/dist/features/badge.js +86 -0
- package/dist/features/badge.js.map +1 -0
- package/dist/features/fix.d.ts +20 -0
- package/dist/features/fix.d.ts.map +1 -0
- package/dist/features/fix.js +115 -0
- package/dist/features/fix.js.map +1 -0
- package/dist/features/roast.d.ts +23 -0
- package/dist/features/roast.d.ts.map +1 -0
- package/dist/features/roast.js +96 -0
- package/dist/features/roast.js.map +1 -0
- package/dist/hooks/agent.d.ts +19 -0
- package/dist/hooks/agent.d.ts.map +1 -0
- package/dist/hooks/agent.js +69 -0
- package/dist/hooks/agent.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +116 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/destructive.d.ts +35 -0
- package/dist/rules/destructive.d.ts.map +1 -0
- package/dist/rules/destructive.js +117 -0
- package/dist/rules/destructive.js.map +1 -0
- package/dist/rules/secrets.d.ts +29 -0
- package/dist/rules/secrets.d.ts.map +1 -0
- package/dist/rules/secrets.js +100 -0
- package/dist/rules/secrets.js.map +1 -0
- package/package.json +56 -0
- package/skill/SKILL.md +86 -0
- package/skill/prompts/path-judge.md +22 -0
- package/src/ai/judge.ts +100 -0
- package/src/cli/index.ts +46 -0
- package/src/editors/vscode.ts +125 -0
- package/src/engine/adversary.ts +100 -0
- package/src/engine/graph.ts +167 -0
- package/src/engine/reach.ts +141 -0
- package/src/engine/sinks.ts +113 -0
- package/src/engine/sources.ts +71 -0
- package/src/engine/taint.ts +117 -0
- package/src/engine/verify.ts +94 -0
- package/src/features/badge.ts +102 -0
- package/src/features/fix.ts +138 -0
- package/src/features/roast.ts +110 -0
- package/src/hooks/agent.ts +84 -0
- package/src/index.ts +147 -0
- package/src/rules/destructive.ts +131 -0
- package/src/rules/secrets.ts +120 -0
- package/test/engine.test.ts +110 -0
- package/test/features.test.ts +131 -0
- package/test/phase3.test.ts +129 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FivoCore Graph Builder
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { parse } from '@babel/parser';
|
|
6
|
+
import * as traverseModule from '@babel/traverse';
|
|
7
|
+
import * as t from '@babel/types';
|
|
8
|
+
import { isSource, SourcePattern } from './sources.js';
|
|
9
|
+
import { isSink, SinkPattern } from './sinks.js';
|
|
10
|
+
|
|
11
|
+
// @ts-ignore - Handle CJS/ESM interop
|
|
12
|
+
const traverse = traverseModule.default ?? traverseModule;
|
|
13
|
+
|
|
14
|
+
export interface DataFlowNode {
|
|
15
|
+
id: string;
|
|
16
|
+
type: 'source' | 'sink' | 'variable' | 'function';
|
|
17
|
+
name: string;
|
|
18
|
+
value?: string;
|
|
19
|
+
loc?: t.SourceLocation | null;
|
|
20
|
+
sourcePattern?: SourcePattern;
|
|
21
|
+
sinkPattern?: SinkPattern;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DataFlowEdge {
|
|
25
|
+
from: string;
|
|
26
|
+
to: string;
|
|
27
|
+
type: 'assignment' | 'call' | 'return' | 'parameter';
|
|
28
|
+
loc?: t.SourceLocation | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TaintPath {
|
|
32
|
+
source: DataFlowNode;
|
|
33
|
+
sink: DataFlowNode;
|
|
34
|
+
path: DataFlowNode[];
|
|
35
|
+
sanitized: boolean;
|
|
36
|
+
confidence: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DataFlowGraph {
|
|
40
|
+
nodes: Map<string, DataFlowNode>;
|
|
41
|
+
edges: DataFlowEdge[];
|
|
42
|
+
taintPaths: TaintPath[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SANITIZERS = new Set([
|
|
46
|
+
'parseInt', 'parseFloat', 'Number', 'escape', 'escapeHtml', 'sanitize',
|
|
47
|
+
'validator.escape', 'validator.trim', 'encodeURIComponent', 'encodeURI',
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
export function buildDataFlowGraph(code: string, filename = 'input.js'): DataFlowGraph {
|
|
51
|
+
const ast = parse(code, {
|
|
52
|
+
sourceType: 'module',
|
|
53
|
+
plugins: ['jsx', 'typescript'],
|
|
54
|
+
errorRecovery: true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const graph: DataFlowGraph = { nodes: new Map(), edges: [], taintPaths: [] };
|
|
58
|
+
const taintedVars = new Map<string, { source: SourcePattern; sanitized: boolean; node: DataFlowNode }>();
|
|
59
|
+
let nodeIdCounter = 0;
|
|
60
|
+
const getNodeId = () => `node_${nodeIdCounter++}`;
|
|
61
|
+
|
|
62
|
+
function addNode(node: DataFlowNode): DataFlowNode {
|
|
63
|
+
graph.nodes.set(node.id, node);
|
|
64
|
+
return node;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function nodeToString(node: t.Node): string {
|
|
68
|
+
if (t.isMemberExpression(node)) {
|
|
69
|
+
const obj = nodeToString(node.object);
|
|
70
|
+
const prop = t.isIdentifier(node.property) ? node.property.name : '';
|
|
71
|
+
return `${obj}.${prop}`;
|
|
72
|
+
}
|
|
73
|
+
return t.isIdentifier(node) ? node.name : t.isStringLiteral(node) ? node.value : '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getCalleeName(node: t.Node): string {
|
|
77
|
+
if (t.isIdentifier(node)) return node.name;
|
|
78
|
+
if (t.isMemberExpression(node)) {
|
|
79
|
+
const obj = t.isIdentifier(node.object) ? node.object.name : '';
|
|
80
|
+
const prop = t.isIdentifier(node.property) ? node.property.name : '';
|
|
81
|
+
return `${obj}.${prop}`;
|
|
82
|
+
}
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function findTaintedVars(node: t.Node): Array<{ varName: string; taint: any }> {
|
|
87
|
+
const results: Array<{ varName: string; taint: any }> = [];
|
|
88
|
+
if (t.isIdentifier(node)) {
|
|
89
|
+
const taint = taintedVars.get(node.name);
|
|
90
|
+
if (taint) results.push({ varName: node.name, taint });
|
|
91
|
+
} else if (t.isTemplateLiteral(node)) {
|
|
92
|
+
node.expressions.forEach(expr => results.push(...findTaintedVars(expr)));
|
|
93
|
+
} else if (t.isBinaryExpression(node)) {
|
|
94
|
+
results.push(...findTaintedVars(node.left), ...findTaintedVars(node.right));
|
|
95
|
+
} else if (t.isCallExpression(node)) {
|
|
96
|
+
node.arguments.forEach(arg => !t.isSpreadElement(arg) && results.push(...findTaintedVars(arg)));
|
|
97
|
+
}
|
|
98
|
+
return results;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
(traverse as any)(ast, {
|
|
102
|
+
VariableDeclarator(path: any) {
|
|
103
|
+
const { id, init } = path.node;
|
|
104
|
+
if (t.isIdentifier(id) && init) {
|
|
105
|
+
const varName = id.name;
|
|
106
|
+
const sourcePattern = isSource(nodeToString(init));
|
|
107
|
+
if (sourcePattern) {
|
|
108
|
+
const node = addNode({
|
|
109
|
+
id: getNodeId(), type: 'source', name: varName, value: nodeToString(init),
|
|
110
|
+
loc: path.node.loc, sourcePattern,
|
|
111
|
+
});
|
|
112
|
+
taintedVars.set(varName, { source: sourcePattern, sanitized: false, node });
|
|
113
|
+
} else {
|
|
114
|
+
const taintedSources = findTaintedVars(init);
|
|
115
|
+
if (taintedSources.length > 0) {
|
|
116
|
+
const firstTaint = taintedSources[0].taint;
|
|
117
|
+
taintedVars.set(varName, { source: firstTaint.source, sanitized: firstTaint.sanitized, node: firstTaint.node });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
CallExpression(path: any) {
|
|
123
|
+
const { callee, arguments: args } = path.node;
|
|
124
|
+
if (SANITIZERS.has(getCalleeName(callee))) {
|
|
125
|
+
args.forEach((arg: any) => {
|
|
126
|
+
if (t.isIdentifier(arg)) {
|
|
127
|
+
const taint = taintedVars.get(arg.name);
|
|
128
|
+
if (taint) taint.sanitized = true;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const sinkPattern = isSink(nodeToString(callee));
|
|
134
|
+
if (sinkPattern) {
|
|
135
|
+
const sinkNode = addNode({
|
|
136
|
+
id: getNodeId(), type: 'sink', name: getCalleeName(callee),
|
|
137
|
+
loc: path.node.loc, sinkPattern,
|
|
138
|
+
});
|
|
139
|
+
args.forEach((arg: any) => {
|
|
140
|
+
if (t.isSpreadElement(arg)) return;
|
|
141
|
+
findTaintedVars(arg).forEach(({ taint }) => {
|
|
142
|
+
graph.taintPaths.push({
|
|
143
|
+
source: taint.node, sink: sinkNode, path: [taint.node, sinkNode],
|
|
144
|
+
sanitized: taint.sanitized, confidence: taint.sanitized ? 0.3 : 0.9,
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return graph;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function getVulnerablePaths(graph: DataFlowGraph): TaintPath[] {
|
|
156
|
+
return graph.taintPaths.filter(p => !p.sanitized);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function getPathsBySeverity(graph: DataFlowGraph, severity: 'critical' | 'high' | 'medium'): TaintPath[] {
|
|
160
|
+
return graph.taintPaths.filter(p => p.sink.sinkPattern?.severity === severity);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function formatTaintPath(path: TaintPath): string {
|
|
164
|
+
const sourceStr = `${path.source.value} (${path.source.sourcePattern?.description})`;
|
|
165
|
+
const sinkStr = `${path.sink.name} (${path.sink.sinkPattern?.description})`;
|
|
166
|
+
return `${sourceStr} → ${sinkStr}`;
|
|
167
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reachability Analysis - Filters code to only entry-point reachable paths
|
|
3
|
+
* This reduces analysis surface by ~97% (OpenAnt research)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parse } from '@babel/parser';
|
|
7
|
+
import * as traverseModule from '@babel/traverse';
|
|
8
|
+
import * as t from '@babel/types';
|
|
9
|
+
|
|
10
|
+
// @ts-ignore
|
|
11
|
+
const traverse = traverseModule.default ?? traverseModule;
|
|
12
|
+
|
|
13
|
+
export interface ReachabilityResult {
|
|
14
|
+
reachableFunctions: Set<string>;
|
|
15
|
+
reachableLines: Set<number>;
|
|
16
|
+
entryPoints: string[];
|
|
17
|
+
totalFunctions: number;
|
|
18
|
+
reachableFunctionCount: number;
|
|
19
|
+
reductionPercent: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Common entry points in web applications
|
|
24
|
+
*/
|
|
25
|
+
const ENTRY_POINT_PATTERNS = [
|
|
26
|
+
/^app\.(get|post|put|delete|patch)/,
|
|
27
|
+
/^router\.(get|post|put|delete|patch)/,
|
|
28
|
+
/^exports\./,
|
|
29
|
+
/^module\.exports/,
|
|
30
|
+
/^export /,
|
|
31
|
+
/addEventListener/,
|
|
32
|
+
/^on[A-Z]/,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Analyze reachability from entry points
|
|
37
|
+
*/
|
|
38
|
+
export function analyzeReachability(code: string): ReachabilityResult {
|
|
39
|
+
const ast = parse(code, {
|
|
40
|
+
sourceType: 'module',
|
|
41
|
+
plugins: ['jsx', 'typescript'],
|
|
42
|
+
errorRecovery: true,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const entryPoints: string[] = [];
|
|
46
|
+
const allFunctions = new Set<string>();
|
|
47
|
+
const reachableFunctions = new Set<string>();
|
|
48
|
+
const reachableLines = new Set<number>();
|
|
49
|
+
const functionCalls = new Map<string, Set<string>>();
|
|
50
|
+
|
|
51
|
+
(traverse as any)(ast, {
|
|
52
|
+
FunctionDeclaration(path: any) {
|
|
53
|
+
const name = path.node.id?.name;
|
|
54
|
+
if (name) {
|
|
55
|
+
allFunctions.add(name);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
CallExpression(path: any) {
|
|
60
|
+
const callee = getCalleeName(path.node.callee);
|
|
61
|
+
|
|
62
|
+
if (isEntryPoint(callee)) {
|
|
63
|
+
entryPoints.push(callee);
|
|
64
|
+
|
|
65
|
+
const callback = path.node.arguments[1] || path.node.arguments[0];
|
|
66
|
+
if (t.isFunctionExpression(callback) || t.isArrowFunctionExpression(callback)) {
|
|
67
|
+
markReachable(callback, reachableFunctions, reachableLines);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const visited = new Set<string>();
|
|
74
|
+
const queue = [...entryPoints];
|
|
75
|
+
|
|
76
|
+
while (queue.length > 0) {
|
|
77
|
+
const current = queue.shift()!;
|
|
78
|
+
if (visited.has(current)) continue;
|
|
79
|
+
visited.add(current);
|
|
80
|
+
|
|
81
|
+
reachableFunctions.add(current);
|
|
82
|
+
|
|
83
|
+
const callees = functionCalls.get(current) || new Set();
|
|
84
|
+
for (const callee of callees) {
|
|
85
|
+
if (!visited.has(callee)) {
|
|
86
|
+
queue.push(callee);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const totalFuncs = allFunctions.size || 1;
|
|
92
|
+
const reachableFuncs = reachableFunctions.size;
|
|
93
|
+
const reduction = ((totalFuncs - reachableFuncs) / totalFuncs) * 100;
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
reachableFunctions,
|
|
97
|
+
reachableLines,
|
|
98
|
+
entryPoints,
|
|
99
|
+
totalFunctions: totalFuncs,
|
|
100
|
+
reachableFunctionCount: reachableFuncs,
|
|
101
|
+
reductionPercent: reduction,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isEntryPoint(callee: string): boolean {
|
|
106
|
+
return ENTRY_POINT_PATTERNS.some(pattern => pattern.test(callee));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getCalleeName(node: t.Node): string {
|
|
110
|
+
if (t.isIdentifier(node)) return node.name;
|
|
111
|
+
if (t.isMemberExpression(node)) {
|
|
112
|
+
const obj = t.isIdentifier(node.object) ? node.object.name : '';
|
|
113
|
+
const prop = t.isIdentifier(node.property) ? node.property.name : '';
|
|
114
|
+
return `${obj}.${prop}`;
|
|
115
|
+
}
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function markReachable(
|
|
120
|
+
node: t.Node,
|
|
121
|
+
reachableFunctions: Set<string>,
|
|
122
|
+
reachableLines: Set<number>
|
|
123
|
+
): void {
|
|
124
|
+
if (t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) {
|
|
125
|
+
if (node.loc) {
|
|
126
|
+
for (let i = node.loc.start.line; i <= node.loc.end.line; i++) {
|
|
127
|
+
reachableLines.add(i);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function filterReachablePaths<T extends { location: { line: number } }>(
|
|
134
|
+
paths: T[],
|
|
135
|
+
reachability: ReachabilityResult
|
|
136
|
+
): T[] {
|
|
137
|
+
return paths.filter(path =>
|
|
138
|
+
reachability.reachableLines.size === 0 ||
|
|
139
|
+
reachability.reachableLines.has(path.location.line)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog of dangerous sinks (vulnerability endpoints)
|
|
3
|
+
* These are operations that can cause security issues if fed untrusted data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface SinkPattern {
|
|
7
|
+
pattern: string;
|
|
8
|
+
category: 'sql' | 'nosql' | 'command' | 'code' | 'xss' | 'path' | 'xxe';
|
|
9
|
+
description: string;
|
|
10
|
+
severity: 'critical' | 'high' | 'medium';
|
|
11
|
+
cwe?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* SQL injection sinks
|
|
16
|
+
*/
|
|
17
|
+
export const SQL_SINKS: SinkPattern[] = [
|
|
18
|
+
{ pattern: 'db.execute', category: 'sql', description: 'SQL execution', severity: 'critical', cwe: 'CWE-89' },
|
|
19
|
+
{ pattern: 'db.query', category: 'sql', description: 'SQL query', severity: 'critical', cwe: 'CWE-89' },
|
|
20
|
+
{ pattern: 'connection.query', category: 'sql', description: 'MySQL query', severity: 'critical', cwe: 'CWE-89' },
|
|
21
|
+
{ pattern: 'pool.query', category: 'sql', description: 'Connection pool query', severity: 'critical', cwe: 'CWE-89' },
|
|
22
|
+
{ pattern: 'executeQuery', category: 'sql', description: 'Generic SQL exec', severity: 'critical', cwe: 'CWE-89' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* NoSQL injection sinks
|
|
27
|
+
*/
|
|
28
|
+
export const NOSQL_SINKS: SinkPattern[] = [
|
|
29
|
+
{ pattern: 'find', category: 'nosql', description: 'MongoDB find', severity: 'high', cwe: 'CWE-943' },
|
|
30
|
+
{ pattern: 'findOne', category: 'nosql', description: 'MongoDB findOne', severity: 'high', cwe: 'CWE-943' },
|
|
31
|
+
{ pattern: 'updateOne', category: 'nosql', description: 'MongoDB update', severity: 'high', cwe: 'CWE-943' },
|
|
32
|
+
{ pattern: 'deleteOne', category: 'nosql', description: 'MongoDB delete', severity: 'high', cwe: 'CWE-943' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Command injection sinks
|
|
37
|
+
*/
|
|
38
|
+
export const COMMAND_SINKS: SinkPattern[] = [
|
|
39
|
+
{ pattern: 'exec', category: 'command', description: 'Command execution', severity: 'critical', cwe: 'CWE-78' },
|
|
40
|
+
{ pattern: 'execSync', category: 'command', description: 'Sync command exec', severity: 'critical', cwe: 'CWE-78' },
|
|
41
|
+
{ pattern: 'spawn', category: 'command', description: 'Process spawn', severity: 'critical', cwe: 'CWE-78' },
|
|
42
|
+
{ pattern: 'spawnSync', category: 'command', description: 'Sync process spawn', severity: 'critical', cwe: 'CWE-78' },
|
|
43
|
+
{ pattern: 'execFile', category: 'command', description: 'File execution', severity: 'critical', cwe: 'CWE-78' },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Code injection sinks
|
|
48
|
+
*/
|
|
49
|
+
export const CODE_SINKS: SinkPattern[] = [
|
|
50
|
+
{ pattern: 'eval', category: 'code', description: 'Code evaluation', severity: 'critical', cwe: 'CWE-94' },
|
|
51
|
+
{ pattern: 'Function', category: 'code', description: 'Dynamic function creation', severity: 'critical', cwe: 'CWE-94' },
|
|
52
|
+
{ pattern: 'setTimeout', category: 'code', description: 'Delayed code exec', severity: 'high', cwe: 'CWE-94' },
|
|
53
|
+
{ pattern: 'setInterval', category: 'code', description: 'Repeated code exec', severity: 'high', cwe: 'CWE-94' },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* XSS sinks
|
|
58
|
+
*/
|
|
59
|
+
export const XSS_SINKS: SinkPattern[] = [
|
|
60
|
+
{ pattern: 'res.send', category: 'xss', description: 'HTTP response', severity: 'high', cwe: 'CWE-79' },
|
|
61
|
+
{ pattern: 'res.write', category: 'xss', description: 'HTTP write', severity: 'high', cwe: 'CWE-79' },
|
|
62
|
+
{ pattern: 'innerHTML', category: 'xss', description: 'DOM innerHTML', severity: 'critical', cwe: 'CWE-79' },
|
|
63
|
+
{ pattern: 'outerHTML', category: 'xss', description: 'DOM outerHTML', severity: 'critical', cwe: 'CWE-79' },
|
|
64
|
+
{ pattern: 'document.write', category: 'xss', description: 'Document write', severity: 'critical', cwe: 'CWE-79' },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Path traversal sinks
|
|
69
|
+
*/
|
|
70
|
+
export const PATH_SINKS: SinkPattern[] = [
|
|
71
|
+
{ pattern: 'fs.readFile', category: 'path', description: 'File read', severity: 'high', cwe: 'CWE-22' },
|
|
72
|
+
{ pattern: 'fs.writeFile', category: 'path', description: 'File write', severity: 'critical', cwe: 'CWE-22' },
|
|
73
|
+
{ pattern: 'fs.unlink', category: 'path', description: 'File delete', severity: 'critical', cwe: 'CWE-22' },
|
|
74
|
+
{ pattern: 'fs.readFileSync', category: 'path', description: 'Sync file read', severity: 'high', cwe: 'CWE-22' },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* All sinks combined
|
|
79
|
+
*/
|
|
80
|
+
export const ALL_SINKS = [
|
|
81
|
+
...SQL_SINKS,
|
|
82
|
+
...NOSQL_SINKS,
|
|
83
|
+
...COMMAND_SINKS,
|
|
84
|
+
...CODE_SINKS,
|
|
85
|
+
...XSS_SINKS,
|
|
86
|
+
...PATH_SINKS,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if a code string matches any sink pattern
|
|
91
|
+
*/
|
|
92
|
+
export function isSink(code: string): SinkPattern | null {
|
|
93
|
+
for (const sink of ALL_SINKS) {
|
|
94
|
+
if (code.includes(sink.pattern)) {
|
|
95
|
+
return sink;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get all sinks matching a category
|
|
103
|
+
*/
|
|
104
|
+
export function getSinksByCategory(category: SinkPattern['category']): SinkPattern[] {
|
|
105
|
+
return ALL_SINKS.filter(s => s.category === category);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get sinks by severity
|
|
110
|
+
*/
|
|
111
|
+
export function getSinksBySeverity(severity: SinkPattern['severity']): SinkPattern[] {
|
|
112
|
+
return ALL_SINKS.filter(s => s.severity === severity);
|
|
113
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog of untrusted input sources (taint origins)
|
|
3
|
+
* These represent user-controlled data that could be malicious
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface SourcePattern {
|
|
7
|
+
pattern: string;
|
|
8
|
+
category: 'http' | 'file' | 'env' | 'cli' | 'external';
|
|
9
|
+
description: string;
|
|
10
|
+
severity: 'critical' | 'high' | 'medium';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* HTTP/API sources - user input from web requests
|
|
15
|
+
*/
|
|
16
|
+
export const HTTP_SOURCES: SourcePattern[] = [
|
|
17
|
+
{ pattern: 'req.query', category: 'http', description: 'URL query parameters', severity: 'critical' },
|
|
18
|
+
{ pattern: 'req.params', category: 'http', description: 'Route parameters', severity: 'critical' },
|
|
19
|
+
{ pattern: 'req.body', category: 'http', description: 'Request body', severity: 'critical' },
|
|
20
|
+
{ pattern: 'req.headers', category: 'http', description: 'HTTP headers', severity: 'high' },
|
|
21
|
+
{ pattern: 'req.cookies', category: 'http', description: 'Cookies', severity: 'high' },
|
|
22
|
+
{ pattern: 'request.query', category: 'http', description: 'Query string', severity: 'critical' },
|
|
23
|
+
{ pattern: 'request.body', category: 'http', description: 'Request body', severity: 'critical' },
|
|
24
|
+
{ pattern: 'ctx.request.body', category: 'http', description: 'Koa/context body', severity: 'critical' },
|
|
25
|
+
{ pattern: 'ctx.query', category: 'http', description: 'Koa query', severity: 'critical' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* File system sources - external file content
|
|
30
|
+
*/
|
|
31
|
+
export const FILE_SOURCES: SourcePattern[] = [
|
|
32
|
+
{ pattern: 'fs.readFileSync', category: 'file', description: 'File content', severity: 'high' },
|
|
33
|
+
{ pattern: 'fs.readFile', category: 'file', description: 'File content async', severity: 'high' },
|
|
34
|
+
{ pattern: 'readFileSync', category: 'file', description: 'File read', severity: 'high' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Environment/config sources - potentially untrusted config
|
|
39
|
+
*/
|
|
40
|
+
export const ENV_SOURCES: SourcePattern[] = [
|
|
41
|
+
{ pattern: 'process.env', category: 'env', description: 'Environment variables', severity: 'medium' },
|
|
42
|
+
{ pattern: 'process.argv', category: 'cli', description: 'Command-line arguments', severity: 'high' },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* All sources combined
|
|
47
|
+
*/
|
|
48
|
+
export const ALL_SOURCES = [
|
|
49
|
+
...HTTP_SOURCES,
|
|
50
|
+
...FILE_SOURCES,
|
|
51
|
+
...ENV_SOURCES,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if a code string matches any source pattern
|
|
56
|
+
*/
|
|
57
|
+
export function isSource(code: string): SourcePattern | null {
|
|
58
|
+
for (const source of ALL_SOURCES) {
|
|
59
|
+
if (code.includes(source.pattern)) {
|
|
60
|
+
return source;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get all sources matching a category
|
|
68
|
+
*/
|
|
69
|
+
export function getSourcesByCategory(category: SourcePattern['category']): SourcePattern[] {
|
|
70
|
+
return ALL_SOURCES.filter(s => s.category === category);
|
|
71
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taint Analysis - tracks data flow from sources to sinks
|
|
3
|
+
* Generates taint-trace proofs for each vulnerability
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DataFlowGraph, TaintPath, formatTaintPath } from './graph.js';
|
|
7
|
+
|
|
8
|
+
export interface TaintTrace {
|
|
9
|
+
finding: string;
|
|
10
|
+
severity: 'critical' | 'high' | 'medium';
|
|
11
|
+
category: string;
|
|
12
|
+
cwe?: string;
|
|
13
|
+
path: string;
|
|
14
|
+
evidence: string[];
|
|
15
|
+
location: {
|
|
16
|
+
file: string;
|
|
17
|
+
line: number;
|
|
18
|
+
column: number;
|
|
19
|
+
};
|
|
20
|
+
sanitized: boolean;
|
|
21
|
+
confidence: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate taint traces from data-flow graph
|
|
26
|
+
*/
|
|
27
|
+
export function generateTaintTraces(graph: DataFlowGraph, filename: string): TaintTrace[] {
|
|
28
|
+
const traces: TaintTrace[] = [];
|
|
29
|
+
|
|
30
|
+
for (const taintPath of graph.taintPaths) {
|
|
31
|
+
const trace = buildTaintTrace(taintPath, filename);
|
|
32
|
+
traces.push(trace);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return traces;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build a single taint trace from a taint path
|
|
40
|
+
*/
|
|
41
|
+
function buildTaintTrace(path: TaintPath, filename: string): TaintTrace {
|
|
42
|
+
const source = path.source;
|
|
43
|
+
const sink = path.sink;
|
|
44
|
+
|
|
45
|
+
const evidence: string[] = [];
|
|
46
|
+
|
|
47
|
+
// Build evidence chain
|
|
48
|
+
evidence.push(`Source: ${source.value} at line ${source.loc?.start.line || '?'}`);
|
|
49
|
+
evidence.push(` Type: ${source.sourcePattern?.description || 'untrusted input'}`);
|
|
50
|
+
|
|
51
|
+
if (path.sanitized) {
|
|
52
|
+
evidence.push(' ✅ Sanitized');
|
|
53
|
+
} else {
|
|
54
|
+
evidence.push(' ❌ NOT sanitized');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
evidence.push(`Sink: ${sink.name} at line ${sink.loc?.start.line || '?'}`);
|
|
58
|
+
evidence.push(` Type: ${sink.sinkPattern?.description || 'dangerous operation'}`);
|
|
59
|
+
|
|
60
|
+
const finding = path.sanitized
|
|
61
|
+
? `Sanitized ${sink.sinkPattern?.category || 'data flow'}`
|
|
62
|
+
: `Potential ${sink.sinkPattern?.category || 'vulnerability'}`;
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
finding,
|
|
66
|
+
severity: sink.sinkPattern?.severity || 'medium',
|
|
67
|
+
category: sink.sinkPattern?.category || 'unknown',
|
|
68
|
+
cwe: sink.sinkPattern?.cwe,
|
|
69
|
+
path: formatTaintPath(path),
|
|
70
|
+
evidence,
|
|
71
|
+
location: {
|
|
72
|
+
file: filename,
|
|
73
|
+
line: sink.loc?.start.line || 0,
|
|
74
|
+
column: sink.loc?.start.column || 0,
|
|
75
|
+
},
|
|
76
|
+
sanitized: path.sanitized,
|
|
77
|
+
confidence: path.confidence,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Filter traces by severity
|
|
83
|
+
*/
|
|
84
|
+
export function filterBySeverity(
|
|
85
|
+
traces: TaintTrace[],
|
|
86
|
+
severity: 'critical' | 'high' | 'medium'
|
|
87
|
+
): TaintTrace[] {
|
|
88
|
+
return traces.filter(t => t.severity === severity);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get only vulnerable (unsanitized) traces
|
|
93
|
+
*/
|
|
94
|
+
export function getVulnerableTraces(traces: TaintTrace[]): TaintTrace[] {
|
|
95
|
+
return traces.filter(t => !t.sanitized);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Format trace for display
|
|
100
|
+
*/
|
|
101
|
+
export function formatTrace(trace: TaintTrace): string {
|
|
102
|
+
const icon = trace.sanitized ? '✅' : '❌';
|
|
103
|
+
const lines = [
|
|
104
|
+
`${icon} [${trace.severity.toUpperCase()}] ${trace.finding}`,
|
|
105
|
+
` ${trace.location.file}:${trace.location.line}:${trace.location.column}`,
|
|
106
|
+
` ${trace.path}`,
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
if (trace.cwe) {
|
|
110
|
+
lines.push(` CWE: ${trace.cwe}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
lines.push(' Evidence:');
|
|
114
|
+
trace.evidence.forEach(e => lines.push(` ${e}`));
|
|
115
|
+
|
|
116
|
+
return lines.join('\n');
|
|
117
|
+
}
|