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.
Files changed (110) hide show
  1. package/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
  2. package/.github/PULL_REQUEST_TEMPLATE.md +22 -0
  3. package/.github/workflows/ci.yml +52 -0
  4. package/BLUEPRINT.md +215 -0
  5. package/BUILD_PLAN.md +175 -0
  6. package/CONTRIBUTING.md +80 -0
  7. package/DOCS_VERIFICATION.md +232 -0
  8. package/FINAL_CHECKLIST.md +263 -0
  9. package/FINAL_SUMMARY.md +238 -0
  10. package/GITHUB_PUSH.md +64 -0
  11. package/LICENSE +21 -0
  12. package/PROGRESS.md +153 -0
  13. package/README.md +443 -0
  14. package/RELEASE_READY.md +201 -0
  15. package/SECURITY.md +211 -0
  16. package/SECURITY_DEEP_AUDIT.md +331 -0
  17. package/TODO.md +52 -0
  18. package/dist/ai/judge.d.ts +36 -0
  19. package/dist/ai/judge.d.ts.map +1 -0
  20. package/dist/ai/judge.js +75 -0
  21. package/dist/ai/judge.js.map +1 -0
  22. package/dist/cli/index.d.ts +6 -0
  23. package/dist/cli/index.d.ts.map +1 -0
  24. package/dist/cli/index.js +39 -0
  25. package/dist/cli/index.js.map +1 -0
  26. package/dist/editors/vscode.d.ts +30 -0
  27. package/dist/editors/vscode.d.ts.map +1 -0
  28. package/dist/editors/vscode.js +103 -0
  29. package/dist/editors/vscode.js.map +1 -0
  30. package/dist/engine/adversary.d.ts +24 -0
  31. package/dist/engine/adversary.d.ts.map +1 -0
  32. package/dist/engine/adversary.js +83 -0
  33. package/dist/engine/adversary.js.map +1 -0
  34. package/dist/engine/graph.d.ts +38 -0
  35. package/dist/engine/graph.d.ts.map +1 -0
  36. package/dist/engine/graph.js +131 -0
  37. package/dist/engine/graph.js.map +1 -0
  38. package/dist/engine/reach.d.ts +22 -0
  39. package/dist/engine/reach.d.ts.map +1 -0
  40. package/dist/engine/reach.js +107 -0
  41. package/dist/engine/reach.js.map +1 -0
  42. package/dist/engine/sinks.d.ts +52 -0
  43. package/dist/engine/sinks.d.ts.map +1 -0
  44. package/dist/engine/sinks.js +96 -0
  45. package/dist/engine/sinks.js.map +1 -0
  46. package/dist/engine/sources.d.ts +35 -0
  47. package/dist/engine/sources.d.ts.map +1 -0
  48. package/dist/engine/sources.js +59 -0
  49. package/dist/engine/sources.js.map +1 -0
  50. package/dist/engine/taint.d.ts +37 -0
  51. package/dist/engine/taint.d.ts.map +1 -0
  52. package/dist/engine/taint.js +83 -0
  53. package/dist/engine/taint.js.map +1 -0
  54. package/dist/engine/verify.d.ts +20 -0
  55. package/dist/engine/verify.d.ts.map +1 -0
  56. package/dist/engine/verify.js +65 -0
  57. package/dist/engine/verify.js.map +1 -0
  58. package/dist/features/badge.d.ts +20 -0
  59. package/dist/features/badge.d.ts.map +1 -0
  60. package/dist/features/badge.js +86 -0
  61. package/dist/features/badge.js.map +1 -0
  62. package/dist/features/fix.d.ts +20 -0
  63. package/dist/features/fix.d.ts.map +1 -0
  64. package/dist/features/fix.js +115 -0
  65. package/dist/features/fix.js.map +1 -0
  66. package/dist/features/roast.d.ts +23 -0
  67. package/dist/features/roast.d.ts.map +1 -0
  68. package/dist/features/roast.js +96 -0
  69. package/dist/features/roast.js.map +1 -0
  70. package/dist/hooks/agent.d.ts +19 -0
  71. package/dist/hooks/agent.d.ts.map +1 -0
  72. package/dist/hooks/agent.js +69 -0
  73. package/dist/hooks/agent.js.map +1 -0
  74. package/dist/index.d.ts +34 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +116 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/rules/destructive.d.ts +35 -0
  79. package/dist/rules/destructive.d.ts.map +1 -0
  80. package/dist/rules/destructive.js +117 -0
  81. package/dist/rules/destructive.js.map +1 -0
  82. package/dist/rules/secrets.d.ts +29 -0
  83. package/dist/rules/secrets.d.ts.map +1 -0
  84. package/dist/rules/secrets.js +100 -0
  85. package/dist/rules/secrets.js.map +1 -0
  86. package/package.json +56 -0
  87. package/skill/SKILL.md +86 -0
  88. package/skill/prompts/path-judge.md +22 -0
  89. package/src/ai/judge.ts +100 -0
  90. package/src/cli/index.ts +46 -0
  91. package/src/editors/vscode.ts +125 -0
  92. package/src/engine/adversary.ts +100 -0
  93. package/src/engine/graph.ts +167 -0
  94. package/src/engine/reach.ts +141 -0
  95. package/src/engine/sinks.ts +113 -0
  96. package/src/engine/sources.ts +71 -0
  97. package/src/engine/taint.ts +117 -0
  98. package/src/engine/verify.ts +94 -0
  99. package/src/features/badge.ts +102 -0
  100. package/src/features/fix.ts +138 -0
  101. package/src/features/roast.ts +110 -0
  102. package/src/hooks/agent.ts +84 -0
  103. package/src/index.ts +147 -0
  104. package/src/rules/destructive.ts +131 -0
  105. package/src/rules/secrets.ts +120 -0
  106. package/test/engine.test.ts +110 -0
  107. package/test/features.test.ts +131 -0
  108. package/test/phase3.test.ts +129 -0
  109. package/tsconfig.json +20 -0
  110. 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
+ }