@clear-capabilities/agentic-security-scanner 0.76.1 → 0.78.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/bin/.agentic-security/findings.json +320 -9
- package/bin/.agentic-security/last-scan.json +320 -9
- package/bin/.agentic-security/last-scan.json.sig +1 -1
- package/bin/.agentic-security/scan-history.json +17 -377
- package/bin/.agentic-security/streak.json +11 -16
- package/bin/agentic-security.js +33 -2
- package/dist/178.index.js +1 -1
- package/dist/384.index.js +1 -1
- package/dist/637.index.js +1 -1
- package/dist/718.index.js +106 -0
- package/dist/824.index.js +126 -0
- package/dist/838.index.js +1 -1
- package/dist/agentic-security.mjs +32 -32
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +7 -7
- package/src/.agentic-security/findings.json +5731 -3933
- package/src/.agentic-security/last-scan.json +5731 -3933
- package/src/.agentic-security/last-scan.json.sig +1 -1
- package/src/.agentic-security/scan-history.json +2533 -887
- package/src/.agentic-security/streak.json +11 -16
- package/src/dataflow/.agentic-security/findings.json +52 -24
- package/src/dataflow/.agentic-security/last-scan.json +52 -24
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -1
- package/src/dataflow/.agentic-security/scan-history.json +101 -134
- package/src/dataflow/.agentic-security/streak.json +8 -10
- package/src/dataflow/async-sequencing.js +16 -7
- package/src/dataflow/builtin-summaries.js +131 -0
- package/src/dataflow/catalog.js +107 -0
- package/src/dataflow/cross-repo.js +75 -1
- package/src/dataflow/engine.js +129 -0
- package/src/dataflow/implicit-flow.js +24 -6
- package/src/dataflow/stub-aware-filter.js +69 -11
- package/src/dataflow/summaries.js +28 -3
- package/src/engine-parallel.js +70 -0
- package/src/engine.js +165 -15
- package/src/ir/.agentic-security/findings.json +757 -16
- package/src/ir/.agentic-security/last-scan.json +757 -16
- package/src/ir/.agentic-security/last-scan.json.sig +1 -1
- package/src/ir/.agentic-security/scan-history.json +545 -138
- package/src/ir/.agentic-security/streak.json +11 -13
- package/src/ir/index.js +22 -1
- package/src/ir/parser-go.js +403 -0
- package/src/ir/parser-js.js +2 -0
- package/src/ir/parser-php.js +330 -0
- package/src/ir/parser-py.helper.py +137 -11
- package/src/ir/parser-rb.js +309 -0
- package/src/posture/.agentic-security/findings.json +407 -84
- package/src/posture/.agentic-security/last-scan.json +407 -84
- package/src/posture/.agentic-security/last-scan.json.sig +1 -1
- package/src/posture/.agentic-security/scan-history.json +16 -4923
- package/src/posture/.agentic-security/streak.json +10 -14
- package/src/posture/calibration.js +14 -0
- package/src/posture/triage.js +13 -0
- package/src/report/.agentic-security/findings.json +6 -5
- package/src/report/.agentic-security/last-scan.json +6 -5
- package/src/report/.agentic-security/last-scan.json.sig +1 -1
- package/src/report/.agentic-security/scan-history.json +3 -300
- package/src/report/.agentic-security/streak.json +7 -8
- package/src/report/index.js +23 -2
- package/src/sast/.agentic-security/findings.json +195 -56
- package/src/sast/.agentic-security/last-scan.json +195 -56
- package/src/sast/.agentic-security/last-scan.json.sig +1 -1
- package/src/sast/.agentic-security/scan-history.json +14 -394
- package/src/sast/.agentic-security/streak.json +10 -13
- package/src/sast/cache-poisoning.js +77 -0
- package/src/sast/comparison-safety.js +73 -0
- package/src/sast/db-taint.js +54 -0
- package/src/sast/graphql.js +127 -0
- package/src/sast/llm-stored-prompt.js +57 -0
- package/src/sast/mutation-xss.js +43 -0
- package/src/sast/nosql-injection.js +5 -0
- package/src/sast/null-byte-injection.js +76 -0
- package/src/sast/redos-nfa.js +338 -0
- package/src/sast/sensitive-data-logging.js +73 -0
- package/src/sast/weak-password-hash.js +77 -0
- package/src/sast/weak-randomness.js +100 -0
- package/src/sca/.agentic-security/findings.json +502 -11
- package/src/sca/.agentic-security/last-scan.json +502 -11
- package/src/sca/.agentic-security/last-scan.json.sig +1 -1
- package/src/sca/.agentic-security/scan-history.json +19 -1
- package/src/sca/.agentic-security/streak.json +6 -6
- package/src/sca/llm-function-extract.js +107 -0
- package/src/sca/vendor-detect.js +91 -0
- package/dist/218.index.js +0 -793
- package/dist/601.index.js +0 -1038
- package/dist/634.index.js +0 -1892
- package/src/integrations/.agentic-security/findings.json +0 -1504
- package/src/integrations/.agentic-security/last-scan.json +0 -1504
- package/src/integrations/.agentic-security/scan-history.json +0 -40
- package/src/integrations/.agentic-security/streak.json +0 -21
- package/src/llm-validator/.agentic-security/findings.json +0 -1891
- package/src/llm-validator/.agentic-security/last-scan.json +0 -1891
- package/src/llm-validator/.agentic-security/last-scan.json.sig +0 -1
- package/src/llm-validator/.agentic-security/scan-history.json +0 -168
- package/src/llm-validator/.agentic-security/streak.json +0 -20
- package/src/lsp/.agentic-security/findings.json +0 -28
- package/src/lsp/.agentic-security/last-scan.json +0 -28
- package/src/lsp/.agentic-security/scan-history.json +0 -79
- package/src/lsp/.agentic-security/streak.json +0 -22
- package/src/mcp/.agentic-security/findings.json +0 -8403
- package/src/mcp/.agentic-security/last-scan.json +0 -8403
- package/src/mcp/.agentic-security/last-scan.json.sig +0 -1
- package/src/mcp/.agentic-security/scan-history.json +0 -1182
- package/src/mcp/.agentic-security/streak.json +0 -22
- package/src/sast/bench-shape/.agentic-security/findings.json +0 -28
- package/src/sast/bench-shape/.agentic-security/last-scan.json +0 -28
- package/src/sast/bench-shape/.agentic-security/scan-history.json +0 -24
- package/src/sast/bench-shape/.agentic-security/streak.json +0 -22
|
@@ -72,23 +72,81 @@ function _normalizeType(t) {
|
|
|
72
72
|
* Returns the (mutated) findings array with `_stubFilterStats` non-
|
|
73
73
|
* enumerable sidecar.
|
|
74
74
|
*/
|
|
75
|
-
|
|
75
|
+
const TYPE_GUARD_PATTERNS = [
|
|
76
|
+
{ re: /typeof\s+(\w+)\s*===?\s*['"]number['"]/, type: 'number' },
|
|
77
|
+
{ re: /typeof\s+(\w+)\s*===?\s*['"]boolean['"]/, type: 'boolean' },
|
|
78
|
+
{ re: /Number\.isInteger\s*\(\s*(\w+)\s*\)/, type: 'number' },
|
|
79
|
+
{ re: /Number\.isFinite\s*\(\s*(\w+)\s*\)/, type: 'number' },
|
|
80
|
+
{ re: /!isNaN\s*\(\s*(\w+)\s*\)/, type: 'number' },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
function _extractTypeGuardType(condExpr) {
|
|
84
|
+
if (!condExpr) return null;
|
|
85
|
+
const condStr = _exprToString(condExpr);
|
|
86
|
+
if (!condStr) return null;
|
|
87
|
+
for (const { re, type } of TYPE_GUARD_PATTERNS) {
|
|
88
|
+
if (re.test(condStr)) return type;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function _exprToString(expr) {
|
|
94
|
+
if (!expr) return null;
|
|
95
|
+
if (expr.kind === 'literal') return String(expr.value || '');
|
|
96
|
+
if (expr.kind === 'ident') return expr.name;
|
|
97
|
+
if (expr.kind === 'binary') return `${_exprToString(expr.left)} ${expr.op} ${_exprToString(expr.right)}`;
|
|
98
|
+
if (expr.kind === 'call') return `${typeof expr.callee === 'string' ? expr.callee : _exprToString(expr.callee)}(${(expr.args || []).map(_exprToString).join(',')})`;
|
|
99
|
+
if (expr.kind === 'member') return `${_exprToString(expr.object)}.${expr.prop}`;
|
|
100
|
+
if (expr.kind === 'unknown') return 'typeof';
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function _hasTypeGuardOnPath(finding, perFileIR) {
|
|
105
|
+
if (!perFileIR || !finding.file) return null;
|
|
106
|
+
const ir = perFileIR[finding.file];
|
|
107
|
+
if (!ir || !ir.functions) return null;
|
|
108
|
+
const fn = ir.functions.find(f => {
|
|
109
|
+
const sinkLine = finding.line || 0;
|
|
110
|
+
return sinkLine >= f.line && sinkLine <= f.line + Object.keys(f.cfg.nodes).length * 3;
|
|
111
|
+
});
|
|
112
|
+
if (!fn) return null;
|
|
113
|
+
for (const node of Object.values(fn.cfg.nodes)) {
|
|
114
|
+
if (node.kind === 'if' && node.cond) {
|
|
115
|
+
const guardType = _extractTypeGuardType(node.cond);
|
|
116
|
+
if (guardType) return guardType;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function applyStubAwareFilter(findings, stubs, perFileIR) {
|
|
76
123
|
if (!Array.isArray(findings) || findings.length === 0) return findings;
|
|
77
|
-
if (!stubs || !stubs.signatures) return findings;
|
|
78
124
|
let demoted = 0;
|
|
79
125
|
for (const f of findings) {
|
|
80
126
|
if (!f || f.parser !== 'IR-TAINT') continue;
|
|
81
127
|
const safeSet = FAMILY_SAFE_TYPES[f.cwe];
|
|
82
128
|
if (!safeSet) continue;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
129
|
+
// Check 1: stub-based type demotion
|
|
130
|
+
const sourceType = stubs ? _sourceTypeFromStubs(f, stubs) : null;
|
|
131
|
+
if (sourceType && safeSet.has(sourceType)) {
|
|
132
|
+
f._stubTypeDemoted = true;
|
|
133
|
+
f._stubTypeReason = `source type ${sourceType} cannot carry ${f.cwe} metacharacters`;
|
|
134
|
+
f._stubTypeOriginalSeverity = f.severity;
|
|
135
|
+
const downgrade = { critical: 'high', high: 'medium', medium: 'low', low: 'info' };
|
|
136
|
+
if (downgrade[f.severity]) f.severity = downgrade[f.severity];
|
|
137
|
+
demoted++;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// Check 2: type-guard narrowing on CFG path
|
|
141
|
+
const guardType = _hasTypeGuardOnPath(f, perFileIR);
|
|
142
|
+
if (guardType && safeSet.has(guardType)) {
|
|
143
|
+
f._stubTypeDemoted = true;
|
|
144
|
+
f._stubTypeReason = `type guard narrows to ${guardType}, safe for ${f.cwe}`;
|
|
145
|
+
f._stubTypeOriginalSeverity = f.severity;
|
|
146
|
+
const downgrade = { critical: 'high', high: 'medium', medium: 'low', low: 'info' };
|
|
147
|
+
if (downgrade[f.severity]) f.severity = downgrade[f.severity];
|
|
148
|
+
demoted++;
|
|
149
|
+
}
|
|
92
150
|
}
|
|
93
151
|
Object.defineProperty(findings, '_stubFilterStats', {
|
|
94
152
|
value: { demoted, totalConsidered: findings.length },
|
|
@@ -68,20 +68,38 @@ export class SummaryCache {
|
|
|
68
68
|
// Compute the summary for a function (or return cached). The `analyze`
|
|
69
69
|
// callback is the per-function walker that returns
|
|
70
70
|
// { returnTainted, mutatedParams: Set, taintedGlobals: Set, findings: [] }
|
|
71
|
+
//
|
|
72
|
+
// Fixed-point iteration: when a recursive call returns a bottom stub,
|
|
73
|
+
// re-analyze up to FP_MAX times until the summary stabilizes.
|
|
71
74
|
compute(qid, taintedParams, analyze) {
|
|
72
75
|
const k = this._key(qid, taintedParams);
|
|
73
|
-
if (this._cache.has(k))
|
|
76
|
+
if (this._cache.has(k)) {
|
|
77
|
+
const cached = this._cache.get(k);
|
|
78
|
+
if (!cached._recursive) return cached;
|
|
79
|
+
}
|
|
74
80
|
if (this._stack.has(qid)) {
|
|
75
|
-
|
|
81
|
+
this._hitRecursion = true;
|
|
76
82
|
return { returnTainted: false, mutatedParams: new Set(), taintedGlobals: new Set(), findings: [], _recursive: true };
|
|
77
83
|
}
|
|
78
84
|
if (++this._iter > this._maxIter) {
|
|
79
85
|
return { returnTainted: false, mutatedParams: new Set(), taintedGlobals: new Set(), findings: [], _budgetExceeded: true };
|
|
80
86
|
}
|
|
81
87
|
this._stack.add(qid);
|
|
88
|
+
this._hitRecursion = false;
|
|
82
89
|
try {
|
|
83
|
-
|
|
90
|
+
let summary = analyze(qid, taintedParams);
|
|
84
91
|
this._cache.set(k, summary);
|
|
92
|
+
if (this._hitRecursion) {
|
|
93
|
+
const FP_MAX = 3;
|
|
94
|
+
for (let fp = 0; fp < FP_MAX; fp++) {
|
|
95
|
+
if (++this._iter > this._maxIter) break;
|
|
96
|
+
const prev = summary;
|
|
97
|
+
summary = analyze(qid, taintedParams);
|
|
98
|
+
if (_summaryEq(prev, summary)) break;
|
|
99
|
+
this._cache.set(k, summary);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (summary._recursive) delete summary._recursive;
|
|
85
103
|
return summary;
|
|
86
104
|
} finally {
|
|
87
105
|
this._stack.delete(qid);
|
|
@@ -110,6 +128,13 @@ export class SummaryCache {
|
|
|
110
128
|
clear() { this._cache.clear(); this._iter = 0; }
|
|
111
129
|
}
|
|
112
130
|
|
|
131
|
+
function _summaryEq(a, b) {
|
|
132
|
+
if (!a || !b) return a === b;
|
|
133
|
+
if (!!a.returnTainted !== !!b.returnTainted) return false;
|
|
134
|
+
if ((a.mutatedParams?.size || 0) !== (b.mutatedParams?.size || 0)) return false;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
113
138
|
// Build the entry-taint-state for a callee from a call site:
|
|
114
139
|
// given the callee's param names + the caller's tainted-var set + the
|
|
115
140
|
// call args, return a Set of param names that are tainted at entry.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Worker-thread parallelism infrastructure for per-file SAST analysis.
|
|
2
|
+
//
|
|
3
|
+
// Gated behind AGENTIC_SECURITY_PARALLEL=1 (default OFF).
|
|
4
|
+
// When enabled, distributes per-file detector execution across a bounded
|
|
5
|
+
// worker pool (default 2 workers, max 4).
|
|
6
|
+
//
|
|
7
|
+
// Architecture:
|
|
8
|
+
// - Main thread: orchestrates file distribution, collects findings
|
|
9
|
+
// - Workers: receive (filepath, content), run detectors, return findings[]
|
|
10
|
+
// - Bounded queue prevents memory exhaustion on large monorepos
|
|
11
|
+
//
|
|
12
|
+
// v1: stub infrastructure. The actual worker dispatch is deferred until
|
|
13
|
+
// the per-file detectors are refactored into a single function that can
|
|
14
|
+
// be serialized to a worker. Today the detectors import 60+ modules with
|
|
15
|
+
// shared state (e.g., _GLOBAL_JAVA_TAINTED_METHODS), making them
|
|
16
|
+
// non-trivially parallelizable.
|
|
17
|
+
|
|
18
|
+
import { availableParallelism } from 'node:os';
|
|
19
|
+
|
|
20
|
+
export function isParallelEnabled() {
|
|
21
|
+
return process.env.AGENTIC_SECURITY_PARALLEL === '1';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function recommendedWorkerCount() {
|
|
25
|
+
const cpus = availableParallelism();
|
|
26
|
+
return Math.max(1, Math.min(4, Math.floor(cpus / 2)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createParallelContext(opts = {}) {
|
|
30
|
+
const workerCount = opts.workers || recommendedWorkerCount();
|
|
31
|
+
return {
|
|
32
|
+
enabled: isParallelEnabled(),
|
|
33
|
+
workerCount,
|
|
34
|
+
filesProcessed: 0,
|
|
35
|
+
totalMs: 0,
|
|
36
|
+
_stats: {
|
|
37
|
+
dispatched: 0,
|
|
38
|
+
completed: 0,
|
|
39
|
+
errors: 0,
|
|
40
|
+
avgMs: 0,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function runParallelFileScans(files, fileContents, detectorFn, opts = {}) {
|
|
46
|
+
if (!isParallelEnabled()) return null;
|
|
47
|
+
|
|
48
|
+
const ctx = createParallelContext(opts);
|
|
49
|
+
const results = [];
|
|
50
|
+
|
|
51
|
+
// v1 stub: run sequentially but through the parallel context for testing.
|
|
52
|
+
// v2 will use worker_threads with a bounded queue.
|
|
53
|
+
for (const fp of files) {
|
|
54
|
+
const content = fileContents[fp];
|
|
55
|
+
if (!content) continue;
|
|
56
|
+
const t0 = Date.now();
|
|
57
|
+
try {
|
|
58
|
+
const findings = detectorFn(fp, content);
|
|
59
|
+
results.push(...(findings || []));
|
|
60
|
+
ctx._stats.completed++;
|
|
61
|
+
} catch {
|
|
62
|
+
ctx._stats.errors++;
|
|
63
|
+
}
|
|
64
|
+
ctx._stats.dispatched++;
|
|
65
|
+
ctx.totalMs += Date.now() - t0;
|
|
66
|
+
ctx.filesProcessed++;
|
|
67
|
+
}
|
|
68
|
+
ctx._stats.avgMs = ctx.filesProcessed ? Math.round(ctx.totalMs / ctx.filesProcessed) : 0;
|
|
69
|
+
return { findings: results, stats: ctx._stats };
|
|
70
|
+
}
|
package/src/engine.js
CHANGED
|
@@ -23,6 +23,13 @@ import { scanSpringbootHardening } from './sast/springboot-hardening.js';
|
|
|
23
23
|
import { scanLaravelHardening } from './sast/laravel-hardening.js';
|
|
24
24
|
import { scanSwift } from './sast/swift.js';
|
|
25
25
|
import { scanDartFlutter } from './sast/dart-flutter.js';
|
|
26
|
+
import { scanWeakRandomness } from './sast/weak-randomness.js';
|
|
27
|
+
import { scanGraphQL as scanGraphQLModule } from './sast/graphql.js';
|
|
28
|
+
import { scanSensitiveDataLogging } from './sast/sensitive-data-logging.js';
|
|
29
|
+
import { scanComparisonSafety } from './sast/comparison-safety.js';
|
|
30
|
+
import { scanWeakPasswordHash } from './sast/weak-password-hash.js';
|
|
31
|
+
import { scanCachePoisoning } from './sast/cache-poisoning.js';
|
|
32
|
+
import { scanNullByteInjection } from './sast/null-byte-injection.js';
|
|
26
33
|
import { scanLlmTradingAgent } from './sast/llm-trading-agent.js';
|
|
27
34
|
import { scanMobileManifest } from './sast/mobile-manifest.js';
|
|
28
35
|
import { scanQuarkusHardening } from './sast/quarkus-hardening.js';
|
|
@@ -74,10 +81,10 @@ import { scanXPathInjection } from './sast/xpath-injection.js';
|
|
|
74
81
|
import { scanSSTI } from './sast/ssti.js';
|
|
75
82
|
import { scanOpenRedirect } from './sast/open-redirect.js';
|
|
76
83
|
import { scanResponseSplitting } from './sast/response-splitting.js';
|
|
77
|
-
import { scanStoredPromptInjection } from './sast/llm-stored-prompt.js';
|
|
84
|
+
import { scanStoredPromptInjection, scanStoredPromptInjectionCrossFile } from './sast/llm-stored-prompt.js';
|
|
78
85
|
import { scanRAGPoisoning } from './sast/rag-poisoning.js';
|
|
79
86
|
import { scanAgentToolEscalation } from './sast/agent-tool-escalation.js';
|
|
80
|
-
import { scanDbTaint } from './sast/db-taint.js';
|
|
87
|
+
import { scanDbTaint, scanDbTaintCrossFile } from './sast/db-taint.js';
|
|
81
88
|
import { scanSSRFCloudMetadata } from './sast/ssrf-cloud-metadata.js';
|
|
82
89
|
import { scanMutationXSS } from './sast/mutation-xss.js';
|
|
83
90
|
import { scanDeserializationGadgets, _detectGadgets } from './sast/deserialization-gadgets.js';
|
|
@@ -5444,15 +5451,60 @@ function dedupeFindingsWithEvidence(findings){
|
|
|
5444
5451
|
// that export is actually imported or invoked in the codebase.
|
|
5445
5452
|
let _VULN_FUNCTION_HINTS_DATA;
|
|
5446
5453
|
try { _VULN_FUNCTION_HINTS_DATA = _require('./sca/vuln-function-hints.json'); } catch(_) { _VULN_FUNCTION_HINTS_DATA = {}; }
|
|
5454
|
+
let _VULN_FUNCTION_HINTS_GENERATED;
|
|
5455
|
+
try { _VULN_FUNCTION_HINTS_GENERATED = _require('./sca/vuln-function-hints-generated.json'); } catch(_) { _VULN_FUNCTION_HINTS_GENERATED = {}; }
|
|
5447
5456
|
const VULN_FUNCTION_HINTS = {
|
|
5448
|
-
"lodash":["merge","defaultsDeep","set","setWith","zipObjectDeep"],
|
|
5449
|
-
"jsonwebtoken":["decode"],
|
|
5450
|
-
"marked":["parse"],
|
|
5457
|
+
"lodash":["merge","defaultsDeep","set","setWith","zipObjectDeep","template"],
|
|
5458
|
+
"jsonwebtoken":["decode","verify","sign"],
|
|
5459
|
+
"marked":["parse","lexer"],
|
|
5451
5460
|
"ejs":["render","renderFile","compile"],
|
|
5452
|
-
"node-fetch":["default"],
|
|
5453
|
-
"xml2js":["parseString"],
|
|
5454
|
-
"js-yaml":["load"],
|
|
5461
|
+
"node-fetch":["default","fetch"],
|
|
5462
|
+
"xml2js":["parseString","parseStringPromise"],
|
|
5463
|
+
"js-yaml":["load","loadAll"],
|
|
5455
5464
|
"minimist":["parse"],
|
|
5465
|
+
"express":["static","urlencoded","json"],
|
|
5466
|
+
"axios":["get","post","put","patch","delete","request","create"],
|
|
5467
|
+
"pg":["query","connect"],
|
|
5468
|
+
"mysql2":["query","execute","createConnection","createPool"],
|
|
5469
|
+
"mongoose":["connect","model","find","findOne","findById","aggregate"],
|
|
5470
|
+
"sequelize":["query","literal","define"],
|
|
5471
|
+
"handlebars":["compile","precompile","registerHelper"],
|
|
5472
|
+
"pug":["compile","render","renderFile"],
|
|
5473
|
+
"sharp":["resize","toBuffer","toFile"],
|
|
5474
|
+
"tar":["extract","create","list"],
|
|
5475
|
+
"glob":["sync","glob"],
|
|
5476
|
+
"cookie":["parse","serialize"],
|
|
5477
|
+
"cookie-parser":["default"],
|
|
5478
|
+
"cors":["default"],
|
|
5479
|
+
"helmet":["default","contentSecurityPolicy"],
|
|
5480
|
+
"passport":["authenticate","initialize","session"],
|
|
5481
|
+
"bcrypt":["hash","compare","genSalt"],
|
|
5482
|
+
"bcryptjs":["hash","compare","genSalt"],
|
|
5483
|
+
"crypto-js":["AES","DES","TripleDES","MD5","SHA1","SHA256","HmacSHA256"],
|
|
5484
|
+
"serialize-javascript":["default"],
|
|
5485
|
+
"shelljs":["exec","which","cat","sed"],
|
|
5486
|
+
"child_process":["exec","execSync","spawn","fork"],
|
|
5487
|
+
"vm2":["VM","NodeVM"],
|
|
5488
|
+
"yaml":["parse","parseDocument"],
|
|
5489
|
+
"dotenv":["config","parse"],
|
|
5490
|
+
"jsonwebtoken":["decode","verify","sign"],
|
|
5491
|
+
"jose":["jwtVerify","SignJWT","compactDecrypt"],
|
|
5492
|
+
"cheerio":["load"],
|
|
5493
|
+
"puppeteer":["launch","connect"],
|
|
5494
|
+
"nodemailer":["createTransport"],
|
|
5495
|
+
"redis":["createClient","get","set","del"],
|
|
5496
|
+
"ioredis":["get","set","del","eval"],
|
|
5497
|
+
"knex":["raw","select","where","insert","update","del"],
|
|
5498
|
+
"prisma":["findUnique","findFirst","findMany","create","update","delete","queryRaw"],
|
|
5499
|
+
"typeorm":["query","createQueryBuilder","getRepository"],
|
|
5500
|
+
"sqlite3":["run","get","all","exec"],
|
|
5501
|
+
"better-sqlite3":["prepare","exec","pragma"],
|
|
5502
|
+
"ws":["on","send","close"],
|
|
5503
|
+
"socket.io":["on","emit","to","broadcast"],
|
|
5504
|
+
"formidable":["parse"],
|
|
5505
|
+
"multer":["single","array","fields"],
|
|
5506
|
+
"path-to-regexp":["compile","parse"],
|
|
5507
|
+
...(typeof _VULN_FUNCTION_HINTS_GENERATED === 'object' && !Array.isArray(_VULN_FUNCTION_HINTS_GENERATED) ? Object.fromEntries(Object.entries(_VULN_FUNCTION_HINTS_GENERATED).filter(([k])=>!k.startsWith('_'))) : {}),
|
|
5456
5508
|
...(typeof _VULN_FUNCTION_HINTS_DATA === 'object' && !Array.isArray(_VULN_FUNCTION_HINTS_DATA) ? Object.fromEntries(Object.entries(_VULN_FUNCTION_HINTS_DATA).filter(([k])=>!k.startsWith('_'))) : {}),
|
|
5457
5509
|
};
|
|
5458
5510
|
function markUsedVulnFunctions(supplyChain,fc){
|
|
@@ -5477,7 +5529,31 @@ function markUsedVulnFunctions(supplyChain,fc){
|
|
|
5477
5529
|
}
|
|
5478
5530
|
for(const sc of supplyChain||[]){
|
|
5479
5531
|
if(sc.type!=='vulnerable_dep')continue;
|
|
5480
|
-
|
|
5532
|
+
// Merge hints: hardcoded → OSV ecosystem_specific → skip if none
|
|
5533
|
+
const hardcoded=VULN_FUNCTION_HINTS[sc.name]||[];
|
|
5534
|
+
const osvFns=Array.isArray(sc.osvVulnFunctions)?sc.osvVulnFunctions.map(f=>{const d=f.lastIndexOf('.');return d>0?f.slice(d+1):f;}):[];
|
|
5535
|
+
const allFns=[...new Set([...hardcoded,...osvFns])];
|
|
5536
|
+
if(!allFns.length){sc.functionReachable='unknown';sc.noKnownCallSite=true;sc._hintSource='none';continue;}
|
|
5537
|
+
sc._hintSource=osvFns.length?(hardcoded.length?'hardcoded+osv':'osv'):'hardcoded';
|
|
5538
|
+
// Search codebase for these functions (if not already searched via VULN_FUNCTION_HINTS)
|
|
5539
|
+
if(osvFns.length&&!hardcoded.length){
|
|
5540
|
+
for(const[fp,content] of Object.entries(fc)){
|
|
5541
|
+
const lines=content.split('\n');
|
|
5542
|
+
for(const fn of osvFns){
|
|
5543
|
+
const shortFn=fn.lastIndexOf('.')>0?fn.slice(fn.lastIndexOf('.')+1):fn;
|
|
5544
|
+
const re=new RegExp(`\\b${shortFn.replace(/\W/g,'\\$&')}\\b`,'g');
|
|
5545
|
+
for(let li=0;li<lines.length;li++){
|
|
5546
|
+
if(re.test(lines[li])){
|
|
5547
|
+
if(!perFile[sc.name])perFile[sc.name]=[];
|
|
5548
|
+
perFile[sc.name].push({pkg:sc.name,fn:shortFn,file:fp,line:li+1});
|
|
5549
|
+
if(!used[sc.name])used[sc.name]=new Set();
|
|
5550
|
+
used[sc.name].add(shortFn);
|
|
5551
|
+
}
|
|
5552
|
+
re.lastIndex=0;
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
}
|
|
5556
|
+
}
|
|
5481
5557
|
sc.usedVulnerableFunctions=[...(used[sc.name]||[])];
|
|
5482
5558
|
const sites=(perFile[sc.name]||[]);
|
|
5483
5559
|
const seen=new Set();
|
|
@@ -5971,7 +6047,10 @@ function _parsePackageLockJson(text,filePath){
|
|
|
5971
6047
|
const parts=scoped?name.slice(1).split('/'):['',name];
|
|
5972
6048
|
const group=scoped?`@${parts[0]}`:'';
|
|
5973
6049
|
const pkgName=scoped?parts[1]:name;
|
|
6050
|
+
const depChain=path.split('node_modules/').filter(Boolean);
|
|
6051
|
+
const isDirect=depChain.length<=1;
|
|
5974
6052
|
out.push({name,version:ver,group,scope:info.dev?'optional':'required',
|
|
6053
|
+
depChain,isDirect,
|
|
5975
6054
|
purl:_makePurl('npm',pkgName,ver,group),ecosystem:'npm',filePath,isUnpinned:false});
|
|
5976
6055
|
}
|
|
5977
6056
|
}catch(_){}return out;
|
|
@@ -6393,7 +6472,13 @@ async function queryOSV(components,allFileContents){
|
|
|
6393
6472
|
const resp=await fetch(`https://api.osv.dev/v1/vulns/${vid}`);
|
|
6394
6473
|
const d=await resp.json();
|
|
6395
6474
|
const fixedVersions=new Set();
|
|
6396
|
-
|
|
6475
|
+
const osvVulnFunctions=[];
|
|
6476
|
+
for(const aff of(d.affected||[])){
|
|
6477
|
+
for(const rng of(aff.ranges||[]))for(const ev of(rng.events||[]))if(ev.fixed)fixedVersions.add(ev.fixed);
|
|
6478
|
+
const es=aff.ecosystem_specific||aff.database_specific||{};
|
|
6479
|
+
if(Array.isArray(es.vulnerable_functions))osvVulnFunctions.push(...es.vulnerable_functions);
|
|
6480
|
+
if(Array.isArray(es.imports))for(const imp of es.imports)if(Array.isArray(imp.symbols))osvVulnFunctions.push(...imp.symbols);
|
|
6481
|
+
}
|
|
6397
6482
|
let severity='medium';
|
|
6398
6483
|
const db=d.database_specific||{};
|
|
6399
6484
|
if(db.severity)severity=db.severity.toLowerCase()==='moderate'?'medium':db.severity.toLowerCase();
|
|
@@ -6409,6 +6494,7 @@ async function queryOSV(components,allFileContents){
|
|
|
6409
6494
|
vuln={id:vid,description:(d.summary||d.details||'No description.').slice(0,300),
|
|
6410
6495
|
fixedVersions:[...fixedVersions].sort(),
|
|
6411
6496
|
aliases:(d.aliases||[]).filter(a=>a.startsWith('CVE-')),
|
|
6497
|
+
osvVulnFunctions:[...new Set(osvVulnFunctions)],
|
|
6412
6498
|
severity,cvssVector,hasKnownAttackRef};
|
|
6413
6499
|
_osvCacheSet('vuln:'+vid,vuln);
|
|
6414
6500
|
}catch(_){continue;}
|
|
@@ -6419,7 +6505,7 @@ async function queryOSV(components,allFileContents){
|
|
|
6419
6505
|
results.push({type:'vulnerable_dep',name:comp.name,version:comp.version,ecosystem:comp.ecosystem,
|
|
6420
6506
|
purl:comp.purl,osvId:vid,cveAliases:vuln.aliases,description:vuln.description,
|
|
6421
6507
|
fixedVersions:vuln.fixedVersions,severity:vuln.severity,cvssVector:vuln.cvssVector,
|
|
6422
|
-
hasKnownAttackRef:vuln.hasKnownAttackRef,reachable:comp.reachable,scope:comp.scope,
|
|
6508
|
+
hasKnownAttackRef:vuln.hasKnownAttackRef,osvVulnFunctions:vuln.osvVulnFunctions||[],reachable:comp.reachable,scope:comp.scope,
|
|
6423
6509
|
file:comp.filePath,
|
|
6424
6510
|
// kept for generateRecs() compat
|
|
6425
6511
|
advisory:`${vid}${cveStr}, ${vuln.description}`,
|
|
@@ -6605,7 +6691,10 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
6605
6691
|
// variants, OWASP Benchmark's helpers package). Roadmap item #5.
|
|
6606
6692
|
try { _GLOBAL_JAVA_TAINTED_METHODS = _buildGlobalJavaTaintedMethodIndex(fileContents); }
|
|
6607
6693
|
catch { _GLOBAL_JAVA_TAINTED_METHODS = new Set(); }
|
|
6608
|
-
const
|
|
6694
|
+
const _perFileTimeoutMs = parseInt(process.env.AGENTIC_SECURITY_PER_FILE_TIMEOUT_MS || '10000', 10);
|
|
6695
|
+
const _fileTimings = [];
|
|
6696
|
+
let _filesSkipped = 0, _filesTimedOut = 0;
|
|
6697
|
+
const files=Object.keys(fileContents).filter(f=>shouldScan(f) && !_isPathIgnored(f));const fc={},pfr={};const aR=[],aF=[],aSrc=[],aSink=[],aSan=[],aLogic=[],aSupply=[],aSecrets=[],aCiphersRest=[],aCiphersTransit=[];let i=0;for(const p of files){i++;const _ft0=Date.now();setProgress({current:i,total:files.length,file:p.split("/").pop(),phase:"Scanning"});try{const c=fileContents[p];if(!c||c.length>500000){_filesSkipped++;continue;}const _avgLine=c.length/Math.max(c.split('\n').length,1);if(_avgLine>400&&c.length>10000)continue;fc[p]=c;aR.push(...scanRoutes(p,c));const ta=performAnalysis(p,c);pfr[p]=ta;aF.push(...ta.findings);aSrc.push(...ta.sources);aSink.push(...ta.sinks);aSan.push(...ta.sanitizers);aLogic.push(...scanLogicVulns(p,c));aSecrets.push(...scanCredentials(p,c));aF.push(...scanStructuralVulns(p,c));aF.push(...scanExtraStructural(p,c));aF.push(...scanAliasedSinks(p,c));aF.push(...scanJavaSAST(p,c));aF.push(...scanJavaBenchExtras(p,c));aLogic.push(...scanMiddlewareOrdering(p,c));aLogic.push(...scanReDoS(p,c));aLogic.push(...scanTodosNearSecurity(p,c));aSecrets.push(...scanEntropySecrets(p,c));const cp=scanCiphers(p,c);aCiphersRest.push(...cp.atRest);aCiphersTransit.push(...cp.inTransit);if(/\.(graphql|gql)$/i.test(p))aF.push(...scanGraphQL(p,c));aF.push(...scanIaC(p,c));
|
|
6609
6698
|
aF.push(...scanLLM(p,c));
|
|
6610
6699
|
aF.push(...scanLLMOwasp(p,c));
|
|
6611
6700
|
aLogic.push(...scanBusinessLogic(p,c));
|
|
@@ -6621,6 +6710,13 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
6621
6710
|
aF.push(...scanLaravelHardening(p,c));
|
|
6622
6711
|
aF.push(...scanSwift(p,c));
|
|
6623
6712
|
aF.push(...scanDartFlutter(p,c));
|
|
6713
|
+
aF.push(...scanWeakRandomness(p,c));
|
|
6714
|
+
aF.push(...scanGraphQLModule(p,c));
|
|
6715
|
+
aF.push(...scanSensitiveDataLogging(p,c));
|
|
6716
|
+
aF.push(...scanComparisonSafety(p,c));
|
|
6717
|
+
aF.push(...scanWeakPasswordHash(p,c));
|
|
6718
|
+
aF.push(...scanCachePoisoning(p,c));
|
|
6719
|
+
aF.push(...scanNullByteInjection(p,c));
|
|
6624
6720
|
aF.push(...scanLlmTradingAgent(p,c));
|
|
6625
6721
|
aF.push(...scanMobileManifest(p,c));
|
|
6626
6722
|
aF.push(...scanQuarkusHardening(p,c));
|
|
@@ -6669,7 +6765,11 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
6669
6765
|
aF.push(...scanMutationXSS(p,c));
|
|
6670
6766
|
aF.push(...scanKotlin(p,c));
|
|
6671
6767
|
aF.push(...scanRuby(p,c));
|
|
6672
|
-
aF.push(...scanPhp(p,c));
|
|
6768
|
+
aF.push(...scanPhp(p,c));
|
|
6769
|
+
const _ftElapsed=Date.now()-_ft0;
|
|
6770
|
+
if(_ftElapsed>_perFileTimeoutMs){aF.push({id:`file-timeout:${p}`,file:p,line:0,vuln:`File analysis exceeded ${_perFileTimeoutMs}ms (${_ftElapsed}ms)`,severity:'info',parser:'ENGINE',confidence:0.5,_timeout:true});_filesTimedOut++;}
|
|
6771
|
+
_fileTimings.push({file:p,ms:_ftElapsed});
|
|
6772
|
+
}catch(_){_fileTimings.push({file:p,ms:Date.now()-_ft0,error:true});}if(i%5===0)await new Promise(r=>setTimeout(r,0));}
|
|
6673
6773
|
// Deserialization-gadget detector runs once with full-tree context (it needs
|
|
6674
6774
|
// manifest contents to know which gadget libs are on the classpath).
|
|
6675
6775
|
try {
|
|
@@ -6817,26 +6917,48 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
6817
6917
|
setProgress({current:i,total:files.length,file:"OSV vulnerability database...",phase:"SCA"});
|
|
6818
6918
|
const allFileContents={...fc, ...depFileContents};
|
|
6819
6919
|
const components=parseManifests(allFileContents);
|
|
6920
|
+
try{const{detectVendoredLibraries}=await import('./sca/vendor-detect.js');const vendored=detectVendoredLibraries(fc);for(const v of vendored){const key=`${v.ecosystem}:${v.name}:${v.version}`;if(!components.some(c=>`${c.ecosystem}:${c.name}:${c.version}`===key))components.push({...v,group:'',purl:`pkg:${v.ecosystem}/${v.name}@${v.version}`,filePath:v.file,isUnpinned:false,reachable:true});}}catch(_){}
|
|
6820
6921
|
const reach=buildReachabilitySet(fc);
|
|
6821
6922
|
const reachabilitySet=reach.imported;
|
|
6822
|
-
components.forEach(c=>{
|
|
6923
|
+
components.forEach(c=>{
|
|
6924
|
+
c.reachable=reachabilitySet.has(c.name.toLowerCase())||(c.ecosystem==='pypi'&&reachabilitySet.has(c.name.replace(/-/g,'_').toLowerCase()));
|
|
6925
|
+
c.isBuildOnly=(c.scope==='optional'&&!c.reachable);
|
|
6926
|
+
if(!c.isDirect&&c.isDirect!==false)c.isDirect=!c.depChain||!c.depChain.length||c.depChain.length<=1;
|
|
6927
|
+
});
|
|
6823
6928
|
let supplyChain=[];try{supplyChain=await queryOSV(components,allFileContents);}catch(_){supplyChain=[];}
|
|
6824
6929
|
// Feat-9: enrich SCA findings with EPSS abuse-probability scores
|
|
6825
6930
|
try{supplyChain=await _enrichWithEPSS(supplyChain);}catch(_){}
|
|
6826
6931
|
// 0.10.0: enrich SCA findings with CISA KEV (CISA KEV catalog)
|
|
6827
6932
|
try{supplyChain=await _enrichWithKEV(supplyChain);}catch(_){}
|
|
6828
6933
|
try{markUsedVulnFunctions(supplyChain,fc);}catch(_){}
|
|
6934
|
+
// LLM-assisted function extraction for CVEs without hints (opt-in: AGENTIC_SECURITY_LLM_SCA=1)
|
|
6935
|
+
if(process.env.AGENTIC_SECURITY_LLM_SCA==='1'){try{const{extractVulnFunctionsViaLLM}=await import('./sca/llm-function-extract.js');const enriched=await extractVulnFunctionsViaLLM(supplyChain);if(enriched.length){markUsedVulnFunctions(supplyChain,fc);}}catch(_){}}
|
|
6829
6936
|
setProgress({current:i,total:files.length,file:"Registry metadata...",phase:"SCA"});
|
|
6830
6937
|
let registryInfo=new Map();try{registryInfo=await queryRegistries(components);}catch(_){}
|
|
6831
6938
|
const dd=(a,k)=>[...new Map(a.map(x=>[k(x),x])).values()];
|
|
6832
6939
|
// 0.6.0 Feat-1: annotate function-level reachability on SCA findings
|
|
6833
6940
|
try { _annotateFunctionReachability(supplyChain,dd(aR,r=>`${r.method}:${r.path}:${r.file}:${r.line}`).map(r=>({...r})),callGraph,fc); } catch(_) {}
|
|
6941
|
+
// Reachability tier classification for SCA findings
|
|
6942
|
+
for(const sc of supplyChain||[]){
|
|
6943
|
+
if(sc.type!=='vulnerable_dep')continue;
|
|
6944
|
+
if(sc.functionReachable==='reachable')sc.reachabilityTier='function-reachable';
|
|
6945
|
+
else if(sc.functionReachable==='unreachable')sc.reachabilityTier='unreachable';
|
|
6946
|
+
else if(sc.reachable)sc.reachabilityTier='import-reachable';
|
|
6947
|
+
else if(sc.isBuildOnly||sc.scope==='optional'&&!sc.reachable){sc.reachabilityTier='build-only';sc.isBuildOnly=true;if(sc.severity==='critical')sc.severity='medium';else if(sc.severity==='high')sc.severity='low';}
|
|
6948
|
+
else if(sc.scope==='required')sc.reachabilityTier='manifest-only';
|
|
6949
|
+
else sc.reachabilityTier='transitive-only';
|
|
6950
|
+
if(sc.reachabilityTier==='transitive-only'){sc.unreachable=true;sc._reachabilityDemoted=true;}
|
|
6951
|
+
}
|
|
6952
|
+
// Early dedup: collapse duplicates BEFORE the annotation pipeline to reduce work.
|
|
6953
|
+
try{const _earlyMap=new Map();for(const f of aF){const k=`${f.file||''}:${f.line||0}:${f.vuln||''}`;if(!_earlyMap.has(k))_earlyMap.set(k,f);}aF.length=0;aF.push(..._earlyMap.values());}catch(_){}
|
|
6834
6954
|
// Sort findings: critical first, then structural patterns last within same severity
|
|
6835
6955
|
aF.sort((a,b)=>({critical:0,high:1,medium:2,low:3}[a.severity]??4)-({critical:0,high:1,medium:2,low:3}[b.severity]??4));
|
|
6836
6956
|
const vulnsByKey={};for(const sc of supplyChain.filter(s=>s.type==='vulnerable_dep')){const k=`${sc.ecosystem}:${sc.name}:${sc.version}`;if(!vulnsByKey[k])vulnsByKey[k]=[];vulnsByKey[k].push(sc);}
|
|
6837
6957
|
const attackResult=computeAttackPathComponents(aF,components,reach.byFile);
|
|
6838
6958
|
for(const[key,paths]of attackResult.pathsByKey){const[eco,name,...vp]=key.split(':');const ver=vp.join(':');for(const f of paths){if(!f.linkedComponents)f.linkedComponents=[];if(!f.linkedComponents.some(c=>c.name===name&&c.ecosystem===eco))f.linkedComponents.push({ecosystem:eco,name,version:ver});}}
|
|
6839
6959
|
const annotatedComponents=components.map(c=>{const key=`${c.ecosystem}:${c.name}:${c.version}`;const vulns=vulnsByKey[key]||[];const riKey=c.ecosystem==='maven'&&c.group?`maven:${c.group}/${c.name}`:`${c.ecosystem}:${c.name}`;const ri=registryInfo.get(riKey)||{};const latestVersion=ri.latestVersion||'';const vd=(ri.versions||{})[c.version]||{};const isDeprecated=typeof vd.deprecated==='string'&&vd.deprecated.length>0;const deprecationMessage=isDeprecated?vd.deprecated:'';const isOutdated=!isDeprecated&&typeof vd.outdated==='string'&&vd.outdated.length>0;const outdatedMessage=isOutdated?vd.outdated:'';const license=ri.license||vd.license||'';return{...c,vulns,hasVulns:vulns.length>0,hasAttackPath:attackResult.flagged.has(key),attackPaths:attackResult.pathsByKey.get(key)||[],latestVersion,isDeprecated,deprecationMessage,isOutdated,outdatedMessage,license};});
|
|
6960
|
+
try{aF.push(...scanDbTaintCrossFile(fc));}catch(_){}
|
|
6961
|
+
try{aF.push(...scanStoredPromptInjectionCrossFile(fc));}catch(_){}
|
|
6840
6962
|
let finalFindings;try{finalFindings=dedupeFindingsWithEvidence(aF);}catch(_){finalFindings=dd(aF,f=>f.id);}
|
|
6841
6963
|
// 0.34.6: filter out Java FPs where a sanitizer pattern (argv-form ProcessBuilder,
|
|
6842
6964
|
// parameterized prepareStatement, constant-folded dead-branch) is present.
|
|
@@ -7115,6 +7237,33 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7115
7237
|
f.validator_verdict = 'unvalidated';
|
|
7116
7238
|
}
|
|
7117
7239
|
finalFindings.push(...irFindings);
|
|
7240
|
+
// Java SCA enrichment: use deep-mode IR call graph to improve Java function reachability
|
|
7241
|
+
try {
|
|
7242
|
+
for (const sc of supplyChain) {
|
|
7243
|
+
if (sc.type !== 'vulnerable_dep' || sc.ecosystem !== 'maven') continue;
|
|
7244
|
+
if (sc.functionReachable === 'reachable') continue;
|
|
7245
|
+
const allFns = [...(sc.osvVulnFunctions || []), ...(VULN_FUNCTION_HINTS[sc.name] || [])];
|
|
7246
|
+
if (!allFns.length) continue;
|
|
7247
|
+
for (const fn of callGraph.functions ? callGraph.functions.values() : []) {
|
|
7248
|
+
if (!fn.cfg || !fn.cfg.nodes) continue;
|
|
7249
|
+
for (const node of Object.values(fn.cfg.nodes)) {
|
|
7250
|
+
if (node.kind !== 'call') continue;
|
|
7251
|
+
const callee = typeof node.callee === 'string' ? node.callee : null;
|
|
7252
|
+
if (!callee) continue;
|
|
7253
|
+
const shortCallee = callee.includes('.') ? callee.split('.').pop() : callee;
|
|
7254
|
+
if (allFns.some(f => f === shortCallee || f === callee)) {
|
|
7255
|
+
sc.functionReachable = 'reachable';
|
|
7256
|
+
sc.reachabilityTier = 'function-reachable';
|
|
7257
|
+
if (!sc.vulnerableFunctionCallSites) sc.vulnerableFunctionCallSites = [];
|
|
7258
|
+
sc.vulnerableFunctionCallSites.push({ pkg: sc.name, fn: shortCallee, file: fn.file, line: node.line });
|
|
7259
|
+
sc._javaIrEnriched = true;
|
|
7260
|
+
break;
|
|
7261
|
+
}
|
|
7262
|
+
}
|
|
7263
|
+
if (sc.functionReachable === 'reachable') break;
|
|
7264
|
+
}
|
|
7265
|
+
}
|
|
7266
|
+
} catch { /* Java SCA enrichment is best-effort */ }
|
|
7118
7267
|
} catch (e) {
|
|
7119
7268
|
// Deep mode is best-effort. A parser blowup in one file shouldn't kill
|
|
7120
7269
|
// the scan — fall back to the pattern-only result.
|
|
@@ -7308,7 +7457,8 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7308
7457
|
// v3 next-gen: why-fired provenance is captured LAST so it reflects the
|
|
7309
7458
|
// final state of each finding after every other annotator has run.
|
|
7310
7459
|
_runAnnotator("annotateWhyFired", () => { annotateWhyFired(finalFindings, {}); });
|
|
7311
|
-
|
|
7460
|
+
const _scanMeta={filesScanned:files.length,filesSkipped:_filesSkipped,filesTimedOut:_filesTimedOut,fileTimings:_fileTimings.sort((a,b)=>b.ms-a.ms).slice(0,20),findingsBySeverity:{critical:finalFindings.filter(f=>f.severity==='critical').length,high:finalFindings.filter(f=>f.severity==='high').length,medium:finalFindings.filter(f=>f.severity==='medium').length,low:finalFindings.filter(f=>f.severity==='low').length,info:finalFindings.filter(f=>f.severity==='info').length}};
|
|
7461
|
+
return{routes:dd(aR,r=>`${r.method}:${r.path}:${r.file}:${r.line}`),findings:finalFindings,sources:aSrc,sinks:aSink,sanitizers:aSan,filesScanned:files.length,crossFileCount:cf.length,logicVulns:aLogic,supplyChain,components:annotatedComponents,secrets:aSecrets,ciphers:{atRest:aCiphersRest,inTransit:aCiphersTransit},pfr,fc,suppressions:_getSuppressions(),_v3,_scanMeta,_engineErrors:{cppDataflowParseErrors:_cppDataflowParseErrors.value},annotatorErrors:_annotatorErrors};}
|
|
7312
7462
|
|
|
7313
7463
|
// Post-aggregation classification: every source becomes "unsafe"|"safe"; every sink becomes "confirmed"|"safe".
|
|
7314
7464
|
// Orphans (no finding linkage) are bucketed by file-local heuristic so the UI shows binary states only.
|