@clear-capabilities/agentic-security-scanner 0.84.1 → 0.118.2
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/CHANGELOG.md +404 -0
- package/dist/178.index.js +1 -1
- package/dist/384.index.js +1 -1
- package/dist/637.index.js +1 -1
- package/dist/838.index.js +1 -1
- package/dist/agentic-security.mjs +84 -84
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +12 -6
- package/src/.agentic-security/dpia.md +26 -0
- package/src/.agentic-security/pqc-migration-plan.json +80 -0
- package/src/.agentic-security/pqc-migration-plan.md +32 -0
- package/src/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +6 -0
- package/src/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +6 -0
- package/src/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
- package/src/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +6 -0
- package/src/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +6 -0
- package/src/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +6 -0
- package/src/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +6 -0
- package/src/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
- package/src/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +6 -0
- package/src/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +6 -0
- package/src/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +6 -0
- package/src/.agentic-security/threat-model.json +3702 -0
- package/src/.agentic-security/threat-model.md +118 -0
- package/src/dataflow/.agentic-security/dpia.md +26 -0
- package/src/dataflow/.agentic-security/sbom-history/15d7c4d20f9d968ebec29445c9e9b3fc6f74e6a1.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/8869fe64035bec0152d92ece961e9369d8637076.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +6 -0
- package/src/dataflow/.agentic-security/threat-model.json +161 -0
- package/src/dataflow/.agentic-security/threat-model.md +43 -0
- package/src/dataflow/CLAUDE.md +17 -5
- package/src/dataflow/catalog.js +9 -0
- package/src/dataflow/engine.js +30 -4
- package/src/dataflow/index.js +8 -0
- package/src/dataflow/proof-gate.js +95 -0
- package/src/dataflow/summaries.js +44 -5
- package/src/engine.js +199 -5
- package/src/ir/.agentic-security/dpia.md +26 -0
- package/src/ir/.agentic-security/exploit-bundles.json +77 -0
- package/src/ir/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +6 -0
- package/src/ir/.agentic-security/sbom-history/b5f468a158f6a145aedd8680841c106b9809f16b.json +6 -0
- package/src/ir/.agentic-security/threat-model.json +143 -0
- package/src/ir/.agentic-security/threat-model.md +39 -0
- package/src/ir/CLAUDE.md +1 -0
- package/src/ir/callgraph.js +12 -1
- package/src/ir/tree-sitter-loader.js +108 -0
- package/src/posture/.agentic-security/dpia.md +26 -0
- package/src/posture/.agentic-security/pqc-migration-plan.json +65 -0
- package/src/posture/.agentic-security/pqc-migration-plan.md +30 -0
- package/src/posture/.agentic-security/sbom-history/0724c133d2943c87555695b5b2adcfcfc3373a2d.json +6 -0
- package/src/posture/.agentic-security/sbom-history/7d45b5e03804aac084b4a2b4dc8c6f10107d2005.json +6 -0
- package/src/posture/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
- package/src/posture/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +6 -0
- package/src/posture/.agentic-security/threat-model.json +2038 -0
- package/src/posture/.agentic-security/threat-model.md +73 -0
- package/src/posture/auditor-walkthrough.js +1 -1
- package/src/posture/corpus-status.js +74 -0
- package/src/posture/coverage-report.js +113 -0
- package/src/posture/holdout-eval.js +57 -0
- package/src/posture/pr-augment.js +1 -1
- package/src/posture/router.js +37 -7
- package/src/report/.agentic-security/dpia.md +26 -0
- package/src/report/.agentic-security/sbom-history/7d45b5e03804aac084b4a2b4dc8c6f10107d2005.json +6 -0
- package/src/report/.agentic-security/sbom-history/f94be839aa6a1011f53da016416a561698b8f338.json +6 -0
- package/src/report/.agentic-security/threat-model.json +7 -0
- package/src/report/.agentic-security/threat-model.md +22 -0
- package/src/report/index.js +18 -1
- package/src/sast/.agentic-security/dpia.md +26 -0
- package/src/sast/.agentic-security/pqc-migration-plan.json +65 -0
- package/src/sast/.agentic-security/pqc-migration-plan.md +30 -0
- package/src/sast/.agentic-security/sbom-history/043a7b401c68f5a298c83d4c42e94f118ff2a89a.json +6 -0
- package/src/sast/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +6 -0
- package/src/sast/.agentic-security/sbom-history/20d77ad7363a82777d506dedbbb1ecfed52f094a.json +6 -0
- package/src/sast/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +6 -0
- package/src/sast/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
- package/src/sast/.agentic-security/sbom-history/3c6b3205f04e26f7654943d465281d00e0fa395e.json +6 -0
- package/src/sast/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +6 -0
- package/src/sast/.agentic-security/sbom-history/5282502e090013219eb006f7e02d8c983bc29c8e.json +6 -0
- package/src/sast/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +6 -0
- package/src/sast/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +6 -0
- package/src/sast/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +6 -0
- package/src/sast/.agentic-security/sbom-history/73514b485d4770f3c5ee57d87338bccd1c61d8b7.json +6 -0
- package/src/sast/.agentic-security/sbom-history/96fa584fc3134512237d58aac63d6f3778b0cbcb.json +6 -0
- package/src/sast/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
- package/src/sast/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +6 -0
- package/src/sast/.agentic-security/sbom-history/a4a34ab8b3d758de2adc8c3dc3e42d4cb0db00a3.json +6 -0
- package/src/sast/.agentic-security/sbom-history/ac236ec3602cfc8fb133f389ec3687e409084673.json +6 -0
- package/src/sast/.agentic-security/sbom-history/b57b3e1292f1f9f0de6c5a34a436e330c78008a3.json +6 -0
- package/src/sast/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +6 -0
- package/src/sast/.agentic-security/sbom-history/dcbfd4935922dd165093c08a1003c338eed937c5.json +6 -0
- package/src/sast/.agentic-security/sbom-history/fa017f725615b0293814344c8ef7dfeb06775ca0.json +6 -0
- package/src/sast/.agentic-security/threat-model.json +816 -0
- package/src/sast/.agentic-security/threat-model.md +91 -0
- package/src/sast/CLAUDE.md +9 -3
- package/src/sast/code-injection-multilang.js +117 -0
- package/src/sast/crypto-protocol.js +43 -1
- package/src/sast/csharp-structural.js +65 -0
- package/src/sast/csharp.js +3 -0
- package/src/sast/csrf.js +24 -3
- package/src/sast/csv-injection.js +84 -0
- package/src/sast/frontend-hygiene.js +106 -0
- package/src/sast/go-structural.js +60 -0
- package/src/sast/java-structural.js +81 -0
- package/src/sast/js-framework-structural.js +115 -0
- package/src/sast/kotlin.js +72 -0
- package/src/sast/ldap-injection.js +73 -17
- package/src/sast/open-redirect.js +47 -5
- package/src/sast/php.js +35 -0
- package/src/sast/python-structural.js +83 -0
- package/src/sast/redos-nfa.js +75 -0
- package/src/sast/response-splitting.js +48 -19
- package/src/sast/ruby.js +38 -0
- package/src/sast/secret-concat.js +60 -0
- package/src/sast/ssrf-cloud-metadata.js +13 -0
- package/src/sast/stored-taint.js +88 -0
- package/src/sast/tree-sitter-sinks.js +62 -0
- package/src/sast/weak-password-hash.js +28 -0
- package/src/sast/weak-randomness.js +46 -5
- package/src/sast/wrong-context-sanitizer.js +94 -0
- package/src/sast/xpath-injection.js +30 -6
- package/src/sast/xss-reflected-multilang.js +116 -0
- package/src/sast/xxe.js +67 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Proof-gate precision pass — roadmap item #6 ("perfect multi-language SAST").
|
|
2
|
+
//
|
|
3
|
+
// The engine produces two INDEPENDENT flow-proof signals that, until now,
|
|
4
|
+
// were either informational-only or wired in isolation:
|
|
5
|
+
//
|
|
6
|
+
// f.provenClean (proven-clean.js) — every reaching path to a SQL
|
|
7
|
+
// sink passes a parameterizer.
|
|
8
|
+
// f._provenUnreachable (exploit-prover.js) — a sanitizer on the path emits
|
|
9
|
+
// output that cannot contain the
|
|
10
|
+
// vuln family's metacharacters.
|
|
11
|
+
//
|
|
12
|
+
// This pass consolidates them into ONE verdict per finding and applies a
|
|
13
|
+
// precision demotion — the central idea of the precision gate:
|
|
14
|
+
//
|
|
15
|
+
// "Report only provably-feasible flows. A flow we can PROVE is clean or
|
|
16
|
+
// infeasible is demoted, not dropped — the auditor still sees it, and a
|
|
17
|
+
// severity-based CI gate still fires, but it stops dominating the risk
|
|
18
|
+
// ranking and stops tripping confidence filters."
|
|
19
|
+
//
|
|
20
|
+
// Output stamped on every finding:
|
|
21
|
+
// f.proof = { verdict, reasons[] }
|
|
22
|
+
// verdict ∈ 'proven-clean' | 'proven-infeasible' | 'feasible' | 'unproven'
|
|
23
|
+
// f.proofGated = true (only when a demotion was applied)
|
|
24
|
+
//
|
|
25
|
+
// Demotion policy is deliberately RECALL-PRESERVING:
|
|
26
|
+
// - lower `confidence` by DEMOTE_FACTOR and recompute its tier,
|
|
27
|
+
// - drop `confidenceTier` / `exploitabilityTier` one notch,
|
|
28
|
+
// - record an `exploitabilityFactors` breadcrumb,
|
|
29
|
+
// - leave `severity` UNTOUCHED. The proofs here are heuristic (path-
|
|
30
|
+
// existence / regex-exclusion), so they must never hide a finding from a
|
|
31
|
+
// severity gate. Confidence/exploitability are the safe levers.
|
|
32
|
+
|
|
33
|
+
const DEMOTE_FACTOR = 0.4;
|
|
34
|
+
|
|
35
|
+
// Tier ladders, lowest → highest. demoteTier moves one step down.
|
|
36
|
+
const CONFIDENCE_TIERS = ['very-low', 'low', 'medium', 'high'];
|
|
37
|
+
const EXPLOITABILITY_TIERS = ['low', 'medium', 'high', 'critical'];
|
|
38
|
+
|
|
39
|
+
function demoteTier(tier, ladder) {
|
|
40
|
+
const i = ladder.indexOf(tier);
|
|
41
|
+
if (i <= 0) return ladder[0];
|
|
42
|
+
return ladder[i - 1];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Decide the proof verdict for a single finding from the upstream signals.
|
|
46
|
+
export function verdictForFinding(f) {
|
|
47
|
+
if (f.provenClean === true) {
|
|
48
|
+
const sanitizers = (f.provenanceProof && f.provenanceProof.sanitizers) || [];
|
|
49
|
+
return { verdict: 'proven-clean', reason: sanitizers.length ? `parameterized via ${sanitizers.join(', ')}` : 'sql-parameterizer-on-path' };
|
|
50
|
+
}
|
|
51
|
+
if (f._provenUnreachable === true) {
|
|
52
|
+
return { verdict: 'proven-infeasible', reason: f._provenUnreachableReason || 'sanitizer-excludes-metacharacters' };
|
|
53
|
+
}
|
|
54
|
+
// A taint finding that reached a sink with attributed sources, for which we
|
|
55
|
+
// could NOT discharge a clean/infeasible proof, is "feasible" — the flows
|
|
56
|
+
// a precision-gated report should lead with. Non-taint findings are simply
|
|
57
|
+
// outside the proof model → "unproven" (no demotion, no claim).
|
|
58
|
+
if (f.parser === 'IR-TAINT') return { verdict: 'feasible', reason: 'reaches-sink, no clean/infeasible proof discharged' };
|
|
59
|
+
return { verdict: 'unproven', reason: 'no flow-proof applicable to this finding class' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Annotate findings in place. Returns the same array with a non-enumerable
|
|
63
|
+
// `_proofGateStats` for benchmarking.
|
|
64
|
+
export function annotateProofGate(findings, opts = {}) {
|
|
65
|
+
if (!Array.isArray(findings)) return findings;
|
|
66
|
+
const factor = typeof opts.demoteFactor === 'number' ? opts.demoteFactor : DEMOTE_FACTOR;
|
|
67
|
+
const stats = { gated: 0, feasible: 0, provenClean: 0, provenInfeasible: 0, unproven: 0 };
|
|
68
|
+
|
|
69
|
+
for (const f of findings) {
|
|
70
|
+
if (!f || typeof f !== 'object') continue;
|
|
71
|
+
const { verdict, reason } = verdictForFinding(f);
|
|
72
|
+
f.proof = { verdict, reasons: [reason] };
|
|
73
|
+
|
|
74
|
+
if (verdict === 'feasible') { stats.feasible++; continue; }
|
|
75
|
+
if (verdict === 'unproven') { stats.unproven++; continue; }
|
|
76
|
+
|
|
77
|
+
// proven-clean | proven-infeasible → recall-preserving demotion.
|
|
78
|
+
if (verdict === 'proven-clean') stats.provenClean++; else stats.provenInfeasible++;
|
|
79
|
+
|
|
80
|
+
if (typeof f.confidence === 'number') {
|
|
81
|
+
f._confidenceBeforeProofGate = f.confidence;
|
|
82
|
+
f.confidence = Math.max(0.01, Number((f.confidence * factor).toFixed(4)));
|
|
83
|
+
}
|
|
84
|
+
if (f.confidenceTier) f.confidenceTier = demoteTier(f.confidenceTier, CONFIDENCE_TIERS);
|
|
85
|
+
if (f.exploitabilityTier) f.exploitabilityTier = demoteTier(f.exploitabilityTier, EXPLOITABILITY_TIERS);
|
|
86
|
+
if (Array.isArray(f.exploitabilityFactors)) f.exploitabilityFactors.push(`proof:${verdict}`);
|
|
87
|
+
f.proofGated = true;
|
|
88
|
+
stats.gated++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
Object.defineProperty(findings, '_proofGateStats', { value: stats, enumerable: false, configurable: true });
|
|
92
|
+
return findings;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const _internals = { DEMOTE_FACTOR, CONFIDENCE_TIERS, EXPLOITABILITY_TIERS, demoteTier };
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
// Function-summary cache for context-sensitive interprocedural taint.
|
|
2
2
|
//
|
|
3
|
-
// PRD §6.2: "k-CFA configurable per analysis." This module
|
|
4
|
-
//
|
|
5
|
-
// entry-taint-state, cached by hash
|
|
6
|
-
//
|
|
3
|
+
// PRD §6.2: "k-CFA configurable per analysis." This module is VALUE-context
|
|
4
|
+
// sensitive (FR-SEM-2): a function gets a distinct summary PER distinct
|
|
5
|
+
// entry-taint-state, cached by hash and computed lazily at the call sites
|
|
6
|
+
// that need it (see engine.js). So a helper that is pure when called with
|
|
7
|
+
// clean args but vulnerable when called with tainted args is modelled
|
|
8
|
+
// correctly at each call site, not collapsed to one empty-entry result.
|
|
9
|
+
//
|
|
10
|
+
// To bound the blowup that per-context computation invites, the number of
|
|
11
|
+
// distinct NON-empty contexts kept per function is capped
|
|
12
|
+
// (`_maxContextsPerFn`, env `AGENTIC_SECURITY_KCFA_MAX_CONTEXTS`, default 16;
|
|
13
|
+
// 0 = pure monovariant). Over the cap we reuse the empty-entry summary.
|
|
14
|
+
// Call-string (k>1) sensitivity is still not modelled — context is the
|
|
15
|
+
// value-abstraction (which params are tainted), not the call stack.
|
|
7
16
|
//
|
|
8
17
|
// A summary captures, given a set of tainted parameter names at function
|
|
9
18
|
// entry, what the function does:
|
|
@@ -43,6 +52,18 @@ export class SummaryCache {
|
|
|
43
52
|
this._stack = new Set(); // qids currently being analyzed (recursion guard)
|
|
44
53
|
this._iter = 0;
|
|
45
54
|
this._maxIter = 5000;
|
|
55
|
+
// FR-SEM-2 (roadmap #2): the engine computes a distinct summary per
|
|
56
|
+
// entry-taint-state (value-context sensitivity), computed lazily at call
|
|
57
|
+
// sites. To stop that from blowing up when a hot helper is reached under
|
|
58
|
+
// many distinct tainted-arg combinations, cap the number of NON-empty
|
|
59
|
+
// entry contexts kept per function. Over the cap we reuse the empty-entry
|
|
60
|
+
// (monovariant) summary — the conservative base the pre-pass computed.
|
|
61
|
+
// AGENTIC_SECURITY_KCFA_MAX_CONTEXTS=N (default 16)
|
|
62
|
+
// ...=0 disables context-sensitivity (pure monovariant).
|
|
63
|
+
this._contextsByQid = new Map(); // qid → Set<stateHash> of computed contexts
|
|
64
|
+
const envCap = Number(process.env.AGENTIC_SECURITY_KCFA_MAX_CONTEXTS);
|
|
65
|
+
this._maxContextsPerFn = Number.isFinite(envCap) && envCap >= 0 ? envCap : 16;
|
|
66
|
+
this._contextCapHits = 0;
|
|
46
67
|
}
|
|
47
68
|
|
|
48
69
|
_key(qid, taintedParams, receiverType) {
|
|
@@ -77,6 +98,24 @@ export class SummaryCache {
|
|
|
77
98
|
const cached = this._cache.get(k);
|
|
78
99
|
if (!cached._recursive) return cached;
|
|
79
100
|
}
|
|
101
|
+
// Context cap (FR-SEM-2). Empty entry is always allowed — it's the base
|
|
102
|
+
// summary the pre-pass computes for every function. For a NON-empty
|
|
103
|
+
// context that's new and over budget, reuse the empty-entry summary
|
|
104
|
+
// instead of computing a fresh one.
|
|
105
|
+
const stateHash = _hashState(taintedParams);
|
|
106
|
+
if (stateHash !== 'empty') {
|
|
107
|
+
let ctxs = this._contextsByQid.get(qid);
|
|
108
|
+
if (!ctxs) { ctxs = new Set(); this._contextsByQid.set(qid, ctxs); }
|
|
109
|
+
if (!ctxs.has(stateHash) && ctxs.size >= this._maxContextsPerFn) {
|
|
110
|
+
this._contextCapHits++;
|
|
111
|
+
const base = this._cache.get(this._key(qid, new Set()));
|
|
112
|
+
if (base && !base._recursive) return base;
|
|
113
|
+
// No base summary yet → fall through and compute this one (we need a
|
|
114
|
+
// result), but don't grow the context set.
|
|
115
|
+
} else {
|
|
116
|
+
ctxs.add(stateHash);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
80
119
|
if (this._stack.has(qid)) {
|
|
81
120
|
this._hitRecursion = true;
|
|
82
121
|
return { returnTainted: false, mutatedParams: new Set(), taintedGlobals: new Set(), findings: [], _recursive: true };
|
|
@@ -125,7 +164,7 @@ export class SummaryCache {
|
|
|
125
164
|
}
|
|
126
165
|
|
|
127
166
|
size() { return this._cache.size; }
|
|
128
|
-
clear() { this._cache.clear(); this._iter = 0; }
|
|
167
|
+
clear() { this._cache.clear(); this._iter = 0; this._contextsByQid.clear(); this._contextCapHits = 0; }
|
|
129
168
|
}
|
|
130
169
|
|
|
131
170
|
function _summaryEq(a, b) {
|
package/src/engine.js
CHANGED
|
@@ -81,6 +81,20 @@ import { scanLDAPInjection } from './sast/ldap-injection.js';
|
|
|
81
81
|
import { scanXPathInjection } from './sast/xpath-injection.js';
|
|
82
82
|
import { scanSSTI } from './sast/ssti.js';
|
|
83
83
|
import { scanOpenRedirect } from './sast/open-redirect.js';
|
|
84
|
+
import { scanWrongContextSanitizer } from './sast/wrong-context-sanitizer.js';
|
|
85
|
+
import { scanFrontendHygiene } from './sast/frontend-hygiene.js';
|
|
86
|
+
import { scanCsvInjection } from './sast/csv-injection.js';
|
|
87
|
+
import { scanStoredTaint } from './sast/stored-taint.js';
|
|
88
|
+
import { scanTreeSitterSinks } from './sast/tree-sitter-sinks.js';
|
|
89
|
+
import { scanJavaStructural } from './sast/java-structural.js';
|
|
90
|
+
import { scanCsharpStructural } from './sast/csharp-structural.js';
|
|
91
|
+
import { scanJsFrameworkStructural } from './sast/js-framework-structural.js';
|
|
92
|
+
import { scanPythonStructural } from './sast/python-structural.js';
|
|
93
|
+
import { scanGoStructural } from './sast/go-structural.js';
|
|
94
|
+
import { scanSecretConcat } from './sast/secret-concat.js';
|
|
95
|
+
import { scanXssReflectedMultilang } from './sast/xss-reflected-multilang.js';
|
|
96
|
+
import { scanCodeInjectionMultilang } from './sast/code-injection-multilang.js';
|
|
97
|
+
import { scanRegexReDoS } from './sast/redos-nfa.js';
|
|
84
98
|
import { scanResponseSplitting } from './sast/response-splitting.js';
|
|
85
99
|
import { scanStoredPromptInjection, scanStoredPromptInjectionCrossFile } from './sast/llm-stored-prompt.js';
|
|
86
100
|
import { scanRAGPoisoning } from './sast/rag-poisoning.js';
|
|
@@ -148,6 +162,8 @@ import { annotateRuntimeCorrelation } from './posture/runtime-correlation.js';
|
|
|
148
162
|
import { applyLearnedCalibration } from './posture/triage-learning.js';
|
|
149
163
|
import { annotateFormalVerification } from './dataflow/formal-verify.js';
|
|
150
164
|
import { annotatePathFeasibility } from './dataflow/smt-feasibility.js';
|
|
165
|
+
import { annotateProofGate } from './dataflow/proof-gate.js';
|
|
166
|
+
import { computeAnalysisTiers, countUnmodeledSinkCandidates } from './posture/coverage-report.js';
|
|
151
167
|
import { annotatePrivacyTaint, emitDpiaArtifact } from './dataflow/privacy-taint.js';
|
|
152
168
|
import { buildThreatModel as buildAutoThreatModel, persistThreatModel as persistAutoThreatModel } from './posture/threat-model-auto.js';
|
|
153
169
|
import { runApiContractScan } from './posture/api-contract.js';
|
|
@@ -1187,6 +1203,111 @@ function _hasPostLookupOwnershipCheck(lines, sinkLine){
|
|
|
1187
1203
|
return false;
|
|
1188
1204
|
}
|
|
1189
1205
|
|
|
1206
|
+
// An SSRF host allow/deny guard near the request sink: a hostname checked
|
|
1207
|
+
// against an allow/deny-list, an RFC1918 / loopback / link-local / metadata
|
|
1208
|
+
// prefix check, or a known SSRF-filter helper. Control-flow (not assignment),
|
|
1209
|
+
// so it isn't caught by the sanitizer-on-assignment path. Recall-safe: an
|
|
1210
|
+
// unguarded fetch still has no guard in its window.
|
|
1211
|
+
const _SSRF_HOST_GUARD_RE = /\b(?:DENY|ALLOW(?:ED)?|allow_?list|deny_?list|block_?list|allowed_?hosts?|blocked_?hosts?|ALLOWED_HOSTS)\b|\.(?:hostname|host|getHost\s*\(\s*\))\b[\s\S]{0,60}?(?:\bin\b|\.has\s*\(|\.includes\s*\(|\.contains\s*\(|[!=]=|\.startsWith\s*\(|\.startswith\s*\()|\b(?:startsWith|startswith)\s*\(\s*['"](?:10\.|192\.168\.|127\.|169\.254\.|172\.(?:1[6-9]|2\d|3[01])\.|::1|fc00|fd)|\bipaddress\b|\bis_private\b|\bis_loopback\b|getaddrinfo|gethostbyname|ssrf[-_]?req[-_]?filter|isSafeUrl|isPrivateIp|validateUrl|_is_metadata_or_private|urlparse\s*\([^)]*\)\s*\.\s*(?:hostname|netloc)/i;
|
|
1212
|
+
|
|
1213
|
+
// A comment-stripped window around the sink. Comments must not read as guards
|
|
1214
|
+
// (e.g. a "no host allow-list / 169.254.169.254" comment describing the vuln).
|
|
1215
|
+
function _guardWindow(ctx, before = 25, after = 5) {
|
|
1216
|
+
if (!ctx || !Array.isArray(ctx.lines)) return '';
|
|
1217
|
+
const line = ctx.line || 1;
|
|
1218
|
+
const lo = Math.max(0, line - before);
|
|
1219
|
+
const hi = Math.min(ctx.lines.length, line + after);
|
|
1220
|
+
return ctx.lines.slice(lo, hi).join('\n')
|
|
1221
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
1222
|
+
.replace(/\/\/[^\n]*/g, ' ')
|
|
1223
|
+
.replace(/(^|[^\w'"`])#[^\n]*/g, '$1 ');
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function _hasSsrfHostGuard(ctx) { return _SSRF_HOST_GUARD_RE.test(_guardWindow(ctx)); }
|
|
1227
|
+
|
|
1228
|
+
// A path-traversal containment guard near the file sink: a basename/strip
|
|
1229
|
+
// helper that removes directory components, a framework safe-join, or a
|
|
1230
|
+
// canonicalize-then-startsWith containment check.
|
|
1231
|
+
const _PATH_GUARD_RE = /\b(?:basename|GetFileName|secure_filename|sanitize_filename|send_from_directory|safe_join)\s*\(|\b(?:startsWith|startswith|StartsWith|HasPrefix)\s*\(|\bgetCanonicalPath\b|\btoRealPath\b|\bfilepath\s*\.\s*(?:Clean|Base|Abs)\b/;
|
|
1232
|
+
function _hasPathGuard(ctx) { return _PATH_GUARD_RE.test(_guardWindow(ctx)); }
|
|
1233
|
+
|
|
1234
|
+
// Reflected-XSS output-encoding guard: an HTML escaper applied near the sink.
|
|
1235
|
+
const _XSS_ESCAPER = String.raw`(?:escapeHtml|escape_html|escape-html|sanitizeHtml|sanitize_html|DOMPurify\.sanitize|he\.encode|he\.escape|_\.escape|validator\.escape|bleach\.clean|markupsafe|htmlspecialchars|htmlentities|html\.escape|escapeHTML|encodeURIComponent|escape)\s*\(`;
|
|
1236
|
+
const _XSS_ESCAPE_RE = new RegExp(_XSS_ESCAPER);
|
|
1237
|
+
// An escaper whose RESULT is captured (assigned / returned) — not a bare,
|
|
1238
|
+
// return-discarded `escapeHtml(s);` call, which does nothing for the reflected
|
|
1239
|
+
// value (FP-3 in smoke). Requires `… = <escaper>(` or `return <escaper>(`.
|
|
1240
|
+
const _XSS_ASSIGNED_ESCAPE_RE = new RegExp(String.raw`(?:[:=]|\breturn\b)\s*[^=;\n]*?` + _XSS_ESCAPER);
|
|
1241
|
+
// A raw request source appearing on the sink line itself (not via a prior
|
|
1242
|
+
// escaped var). If present, the reflection is unescaped — keep the finding.
|
|
1243
|
+
const _XSS_RAW_SOURCE_RE = /\breq(?:uest)?\s*\.|\bctx\s*\.\s*(?:query|params|request|body)\b|\$_(?:GET|POST|REQUEST|COOKIE)\b|\bparams\s*\[|\.(?:query|body|params)\s*\.[A-Za-z_$]/;
|
|
1244
|
+
function _hasXssOutputEncoding(ctx) {
|
|
1245
|
+
if (!ctx || !Array.isArray(ctx.lines)) return false;
|
|
1246
|
+
const line = ctx.lines[(ctx.line || 1) - 1] || '';
|
|
1247
|
+
// The reflected-XSS finding is anchored either on the escaped-assignment line
|
|
1248
|
+
// (`const safe = escape(req.query.x)`) or on the response sink line
|
|
1249
|
+
// (`res.send('…' + safe + '…')`).
|
|
1250
|
+
// - If the finding line itself captures the escaped value (`const safe =
|
|
1251
|
+
// escape(req.query.x)`), the reflection is encoded → safe. A bare,
|
|
1252
|
+
// return-discarded `escapeHtml(s);` does NOT count.
|
|
1253
|
+
if (_XSS_ASSIGNED_ESCAPE_RE.test(line)) return true;
|
|
1254
|
+
// - A raw, unescaped request source on the finding line is the real vuln
|
|
1255
|
+
// shape (`res.send('…' + req.query.x)`) → keep it.
|
|
1256
|
+
if (_XSS_RAW_SOURCE_RE.test(line)) return false;
|
|
1257
|
+
// - Otherwise the sink reflects a variable; drop only if that variable was
|
|
1258
|
+
// escaped (and the result captured) in the surrounding window. A bare
|
|
1259
|
+
// `escapeHtml(s);` with a discarded return does NOT count.
|
|
1260
|
+
return _XSS_ASSIGNED_ESCAPE_RE.test(_guardWindow(ctx, 8, 2));
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// Centralized guard-recognition pass (#1). SSRF (CWE-918) and path-traversal
|
|
1264
|
+
// (CWE-22) are emitted by several independent detectors (regex, structural,
|
|
1265
|
+
// per-language flow, PY-SAST, CSHARP, GO) — none of which shared a notion of
|
|
1266
|
+
// "the sink is hardened by a host allow/deny check or a path containment
|
|
1267
|
+
// guard." This single pass drops such findings uniformly, regardless of which
|
|
1268
|
+
// detector produced them, using the same comment-stripped window guards.
|
|
1269
|
+
// Recall-safe: a genuinely-unguarded sink has no guard in its window (the
|
|
1270
|
+
// cve-replay vulnerable-tier TP count is the invariant that proves this).
|
|
1271
|
+
export function dropGuardedFindings(findings, fileContents) {
|
|
1272
|
+
if (!Array.isArray(findings)) return findings;
|
|
1273
|
+
const cache = new Map();
|
|
1274
|
+
const linesOf = (file) => {
|
|
1275
|
+
if (cache.has(file)) return cache.get(file);
|
|
1276
|
+
const c = fileContents && fileContents[file];
|
|
1277
|
+
const arr = typeof c === 'string' ? c.split('\n') : null;
|
|
1278
|
+
cache.set(file, arr);
|
|
1279
|
+
return arr;
|
|
1280
|
+
};
|
|
1281
|
+
return findings.filter((f) => {
|
|
1282
|
+
if (!f || !f.file) return true;
|
|
1283
|
+
// Already-sanitized findings (isSanitized / info) are owned by the
|
|
1284
|
+
// sanitizer-dataflow pipeline, which records them in scan.suppressions.
|
|
1285
|
+
// Don't hard-drop them here or that bookkeeping is lost (smoke FP-3).
|
|
1286
|
+
if (f.isSanitized) return true;
|
|
1287
|
+
const cwe = f.cwe || '';
|
|
1288
|
+
const isXss = cwe === 'CWE-79' && /reflected xss/i.test(f.vuln || '');
|
|
1289
|
+
if (cwe !== 'CWE-918' && cwe !== 'CWE-22' && !isXss) return true;
|
|
1290
|
+
// Flow-pair findings (REGEX parser) carry the line on sink/source, not at
|
|
1291
|
+
// the top level; fall back to those before parsing it out of the id.
|
|
1292
|
+
let line = f.line;
|
|
1293
|
+
if (typeof line !== 'number') {
|
|
1294
|
+
line = (f.sink && f.sink.line) ?? (f.source && f.source.line);
|
|
1295
|
+
if (typeof line !== 'number') {
|
|
1296
|
+
const m = /:(\d+):/.exec(f.id || '');
|
|
1297
|
+
if (m) line = Number(m[1]);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (typeof line !== 'number') return true;
|
|
1301
|
+
const lines = linesOf(f.file);
|
|
1302
|
+
if (!lines) return true;
|
|
1303
|
+
const ctx = { lines, line };
|
|
1304
|
+
if (cwe === 'CWE-918' && _hasSsrfHostGuard(ctx)) return false;
|
|
1305
|
+
if (cwe === 'CWE-22' && _hasPathGuard(ctx)) return false;
|
|
1306
|
+
if (isXss && _hasXssOutputEncoding(ctx)) return false;
|
|
1307
|
+
return true;
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1190
1311
|
function _detectSafeSinkShape(vuln, args, ctx){
|
|
1191
1312
|
// 1.1: Reflected XSS / SSRF / Path Traversal / Open Redirect / Code Injection
|
|
1192
1313
|
// sinks with empty or whitespace-only args cannot reflect user input — skip
|
|
@@ -1221,6 +1342,8 @@ function _detectSafeSinkShape(vuln, args, ctx){
|
|
|
1221
1342
|
if (_isSafeExecFileCall(args)) return 'execFile-list';
|
|
1222
1343
|
}
|
|
1223
1344
|
if(/SSRF/.test(vuln) && _isStaticUrlFirstArg(args)) return 'static-url';
|
|
1345
|
+
if(/SSRF/.test(vuln) && _hasSsrfHostGuard(ctx)) return 'ssrf-host-guard';
|
|
1346
|
+
if(/Path Traversal/.test(vuln) && _hasPathGuard(ctx)) return 'path-contained';
|
|
1224
1347
|
if(/Open Redirect/.test(vuln)) {
|
|
1225
1348
|
// Static redirect targets (env var or string literal) are not user-controlled
|
|
1226
1349
|
if (/^\s*process\.env\b/.test(args)) return 'static-redirect-target';
|
|
@@ -1959,6 +2082,17 @@ function scanStructuralVulns(fp, raw) {
|
|
|
1959
2082
|
}
|
|
1960
2083
|
effectiveSeverity = s;
|
|
1961
2084
|
}
|
|
2085
|
+
// Guard recognition (precision, #1): a structural SSRF/path finding on
|
|
2086
|
+
// hardened code — an SSRF host allow/deny check, or a path
|
|
2087
|
+
// basename/containment guard near the sink — is a false positive.
|
|
2088
|
+
if (/SSRF/.test(pat.vuln) && _hasSsrfHostGuard({ lines, line })) {
|
|
2089
|
+
_suppressionLog.push({vuln:pat.vuln, file:fp, line, snippet, reason:'ssrf-host-guard'});
|
|
2090
|
+
continue;
|
|
2091
|
+
}
|
|
2092
|
+
if (/Path Traversal/.test(pat.vuln) && _hasPathGuard({ lines, line })) {
|
|
2093
|
+
_suppressionLog.push({vuln:pat.vuln, file:fp, line, snippet, reason:'path-contained'});
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
1962
2096
|
const id = `struct:${fp}:${line}:${pat.vuln.replace(/\s/g, '_')}`;
|
|
1963
2097
|
if (!findings.find(f => f.id === id)) {
|
|
1964
2098
|
findings.push({
|
|
@@ -5512,8 +5646,17 @@ function dedupeFindingsWithEvidence(findings){
|
|
|
5512
5646
|
const key=`${file}:${sinkLine}:${fam}`;
|
|
5513
5647
|
if(!buckets.has(key)){buckets.set(key,f);continue;}
|
|
5514
5648
|
const kept=buckets.get(key);
|
|
5515
|
-
//
|
|
5516
|
-
|
|
5649
|
+
// Winner selection: an interprocedural flow finding (carries source→sink
|
|
5650
|
+
// attribution) is the better carrier than a flat structural/regex match at
|
|
5651
|
+
// the same sink — keep its chain/source-line attribution. When neither or
|
|
5652
|
+
// both carry flow, fall back to severity. The winner keeps its own severity
|
|
5653
|
+
// (we must not resurrect a rating that ownership/reachability analysis
|
|
5654
|
+
// deliberately downgraded on one of the two findings).
|
|
5655
|
+
const fHasFlow = !!(f.source && f.sink);
|
|
5656
|
+
const kHasFlow = !!(kept.source && kept.sink);
|
|
5657
|
+
const keepNew = (fHasFlow !== kHasFlow)
|
|
5658
|
+
? fHasFlow
|
|
5659
|
+
: (SEV_RANK[f.severity]??9) < (SEV_RANK[kept.severity]??9);
|
|
5517
5660
|
const winner = keepNew ? f : kept;
|
|
5518
5661
|
const loser = keepNew ? kept : f;
|
|
5519
5662
|
if(!winner.evidence)winner.evidence=[winner.parser||"UNKNOWN"];
|
|
@@ -7202,8 +7345,8 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7202
7345
|
catch { _GLOBAL_JAVA_TAINTED_METHODS = new Set(); }
|
|
7203
7346
|
const _perFileTimeoutMs = parseInt(process.env.AGENTIC_SECURITY_PER_FILE_TIMEOUT_MS || '10000', 10);
|
|
7204
7347
|
const _fileTimings = [];
|
|
7205
|
-
let _filesSkipped = 0, _filesTimedOut = 0;
|
|
7206
|
-
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));
|
|
7348
|
+
let _filesSkipped = 0, _filesTimedOut = 0, _filesDenseSkipped = 0;
|
|
7349
|
+
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){_filesDenseSkipped++;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));if(/\.(?:java|cs|kt|py|php|phtml)$/i.test(p)){try{aLogic.push(...scanRegexReDoS(p,c));}catch(_){}}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));
|
|
7207
7350
|
aF.push(...scanLLM(p,c));
|
|
7208
7351
|
aF.push(...scanLLMOwasp(p,c));
|
|
7209
7352
|
aLogic.push(...scanBusinessLogic(p,c));
|
|
@@ -7265,6 +7408,18 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7265
7408
|
aF.push(...scanXPathInjection(p,c));
|
|
7266
7409
|
aF.push(...scanSSTI(p,c));
|
|
7267
7410
|
aF.push(...scanOpenRedirect(p,c));
|
|
7411
|
+
aF.push(...scanWrongContextSanitizer(p,c));
|
|
7412
|
+
aF.push(...scanFrontendHygiene(p,c));
|
|
7413
|
+
aF.push(...scanCsvInjection(p,c));
|
|
7414
|
+
aF.push(...scanStoredTaint(p,c));
|
|
7415
|
+
aF.push(...scanJavaStructural(p,c));
|
|
7416
|
+
aF.push(...scanCsharpStructural(p,c));
|
|
7417
|
+
aF.push(...scanJsFrameworkStructural(p,c));
|
|
7418
|
+
aF.push(...scanPythonStructural(p,c));
|
|
7419
|
+
aF.push(...scanGoStructural(p,c));
|
|
7420
|
+
aF.push(...scanSecretConcat(p,c));
|
|
7421
|
+
aF.push(...scanXssReflectedMultilang(p,c));
|
|
7422
|
+
aF.push(...scanCodeInjectionMultilang(p,c));
|
|
7268
7423
|
aF.push(...scanResponseSplitting(p,c));
|
|
7269
7424
|
aF.push(...scanStoredPromptInjection(p,c));
|
|
7270
7425
|
aF.push(...scanRAGPoisoning(p,c));
|
|
@@ -7552,7 +7707,14 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7552
7707
|
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};});
|
|
7553
7708
|
try{aF.push(...scanDbTaintCrossFile(fc));}catch(_){}
|
|
7554
7709
|
try{aF.push(...scanStoredPromptInjectionCrossFile(fc));}catch(_){}
|
|
7710
|
+
// Roadmap #8 — tree-sitter sinks for long-tail languages (opt-in,
|
|
7711
|
+
// AGENTIC_SECURITY_TREE_SITTER=1; degrades to no-op without the optional dep).
|
|
7712
|
+
if(process.env.AGENTIC_SECURITY_TREE_SITTER==='1'){try{aF.push(...await scanTreeSitterSinks(fc));}catch(_){}}
|
|
7555
7713
|
let finalFindings;try{finalFindings=dedupeFindingsWithEvidence(aF);}catch(_){finalFindings=dd(aF,f=>f.id);}
|
|
7714
|
+
// #1 — centralized SSRF/path guard recognition: drop CWE-918/CWE-22 findings
|
|
7715
|
+
// on code hardened by a host allow/deny check or a path containment guard,
|
|
7716
|
+
// regardless of which detector emitted them. Opt out: AGENTIC_SECURITY_NO_GUARD_RECOGNITION=1.
|
|
7717
|
+
if(process.env.AGENTIC_SECURITY_NO_GUARD_RECOGNITION!=='1'){try{finalFindings=dropGuardedFindings(finalFindings,fc);}catch(_){}}
|
|
7556
7718
|
// 0.34.6: filter out Java FPs where a sanitizer pattern (argv-form ProcessBuilder,
|
|
7557
7719
|
// parameterized prepareStatement, constant-folded dead-branch) is present.
|
|
7558
7720
|
// applyJavaBenchSuppressions is a no-op on non-.java files.
|
|
@@ -7670,6 +7832,14 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7670
7832
|
_runAnnotator("annotateCalibratedConfidence", () => { annotateCalibratedConfidence(finalFindings, { scanRoot }); });
|
|
7671
7833
|
const _projectCtx = (() => { try { return detectProjectContext(fc, aR); } catch { return {}; } })();
|
|
7672
7834
|
_runAnnotator("annotateExploitability", () => { annotateExploitability(finalFindings, _projectCtx); });
|
|
7835
|
+
// Roadmap #6 — proof-gate precision pass. Runs AFTER confidence +
|
|
7836
|
+
// exploitability so it can demote their tiers, and BEFORE mitigation /
|
|
7837
|
+
// composite-risk so the demotion flows into the canonical ranking. Default
|
|
7838
|
+
// on; opt out with AGENTIC_SECURITY_NO_PROOF_GATE=1. Demotes proven-clean /
|
|
7839
|
+
// proven-infeasible flows (confidence + tiers only, never severity).
|
|
7840
|
+
if (process.env.AGENTIC_SECURITY_NO_PROOF_GATE !== '1') {
|
|
7841
|
+
_runAnnotator("annotateProofGate", () => { annotateProofGate(finalFindings); });
|
|
7842
|
+
}
|
|
7673
7843
|
// v3 next-gen: production-aware context ingest (Pillar 9). Must run BEFORE
|
|
7674
7844
|
// the mitigation composite, persona prioritization, and final why-fired
|
|
7675
7845
|
// record so those see the demotion signals.
|
|
@@ -8150,6 +8320,25 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
8150
8320
|
finalFindings = finalFindings.filter(_shouldKeep);
|
|
8151
8321
|
_filterInPlace(aLogic);
|
|
8152
8322
|
_filterInPlace(aSecrets);
|
|
8323
|
+
// Drop a generic High-Entropy Credential Candidate when a named/structural
|
|
8324
|
+
// secret detector (e.g. secret-concat, hardcoded-secret) already flagged the
|
|
8325
|
+
// SAME file:line — otherwise one split/hardcoded key is reported twice (the
|
|
8326
|
+
// named finding lives in `findings`, the entropy candidate in `secrets`).
|
|
8327
|
+
{
|
|
8328
|
+
const _namedSecretLines = new Set();
|
|
8329
|
+
for (const f of finalFindings) {
|
|
8330
|
+
const ln = f.line ?? f.source?.line;
|
|
8331
|
+
if (typeof ln === 'number' && (f.cwe === 'CWE-798' || /hardcoded|credential|secret/i.test(f.vuln || ''))) {
|
|
8332
|
+
_namedSecretLines.add(`${f.file}:${ln}`);
|
|
8333
|
+
}
|
|
8334
|
+
}
|
|
8335
|
+
const _kept = aSecrets.filter((s) => {
|
|
8336
|
+
if (!/High-Entropy|entropy/i.test(s.vuln || '')) return true;
|
|
8337
|
+
return !_namedSecretLines.has(`${s.file}:${s.line}`);
|
|
8338
|
+
});
|
|
8339
|
+
aSecrets.length = 0;
|
|
8340
|
+
aSecrets.push(..._kept);
|
|
8341
|
+
}
|
|
8153
8342
|
_filterInPlace(supplyChain);
|
|
8154
8343
|
classifyOrphans(aSrc,aSink,finalFindings,fc);
|
|
8155
8344
|
// v3 next-gen: capture scan-level reports (counterfactual, threat model,
|
|
@@ -8255,7 +8444,12 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
8255
8444
|
}
|
|
8256
8445
|
}
|
|
8257
8446
|
|
|
8258
|
-
|
|
8447
|
+
// Coverage-honesty report (#5 + #6): per-language analysis tier (IR-taint vs
|
|
8448
|
+
// pattern-only), dense/large/timeout skips, and unmodeled-sink candidates.
|
|
8449
|
+
let _analysisTier = null, _unmodeledSinks = null;
|
|
8450
|
+
try { _analysisTier = computeAnalysisTiers(Object.keys(fc)); } catch {}
|
|
8451
|
+
try { _unmodeledSinks = countUnmodeledSinkCandidates(fc, finalFindings); } catch {}
|
|
8452
|
+
const _scanMeta={filesScanned:files.length,filesSkipped:_filesSkipped,filesDenseSkipped:_filesDenseSkipped,filesTimedOut:_filesTimedOut,analysisTier:_analysisTier,unmodeledSinkCandidates:_unmodeledSinks,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}};
|
|
8259
8453
|
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,threatModel:_threatModel,sbomDiff:_sbomDiff,complianceReport:_complianceReport,exploitBundles:_exploitBundles,pqcPlan:_pqcPlan,licenseGraph:_licenseGraph,attributions:_attributions,attackTaxonomy:_taxonomySummary};}
|
|
8260
8454
|
|
|
8261
8455
|
// Post-aggregation classification: every source becomes "unsafe"|"safe"; every sink becomes "confirmed"|"safe".
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Data Protection Impact Assessment (DPIA)
|
|
2
|
+
|
|
3
|
+
Generated by agentic-security scanner on 2026-05-31.
|
|
4
|
+
|
|
5
|
+
This is an automated DPIA scaffold derived from static analysis.
|
|
6
|
+
It must be reviewed and completed by a privacy officer before use.
|
|
7
|
+
|
|
8
|
+
## Data classes identified
|
|
9
|
+
|
|
10
|
+
## Privacy-related findings
|
|
11
|
+
|
|
12
|
+
| Severity | File:Line | Class → Sink | Description |
|
|
13
|
+
|---|---|---|---|
|
|
14
|
+
|
|
15
|
+
## Regulatory framework mapping
|
|
16
|
+
|
|
17
|
+
- **GDPR Art. 35** — DPIA required when processing is likely to result in high risk to data subjects.
|
|
18
|
+
- **CCPA §1798.130** — Notice + access rights for collected personal information.
|
|
19
|
+
|
|
20
|
+
## Reviewer checklist
|
|
21
|
+
|
|
22
|
+
- [ ] Confirm each PII field's collection has a documented lawful basis
|
|
23
|
+
- [ ] Confirm retention period for each class is documented
|
|
24
|
+
- [ ] Confirm DSAR (data subject access request) workflow exists
|
|
25
|
+
- [ ] Confirm encryption at rest + in transit for each class
|
|
26
|
+
- [ ] Confirm logging of PII access for audit (where applicable)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"struct:parser-cs.js:208:Mass_Assignment_(req.body_Direct_to_Model)": {
|
|
3
|
+
"family": "mass-assignment",
|
|
4
|
+
"exploitable": "unknown",
|
|
5
|
+
"pocs": {
|
|
6
|
+
"curl": null
|
|
7
|
+
},
|
|
8
|
+
"tests": {
|
|
9
|
+
"jest": null,
|
|
10
|
+
"pytest": null
|
|
11
|
+
},
|
|
12
|
+
"remediation": {
|
|
13
|
+
"summary": "no template for this family yet — manual review required"
|
|
14
|
+
},
|
|
15
|
+
"stub": true
|
|
16
|
+
},
|
|
17
|
+
"struct:parser-go.js:253:Mass_Assignment_(req.body_Direct_to_Model)": {
|
|
18
|
+
"family": "mass-assignment",
|
|
19
|
+
"exploitable": "unknown",
|
|
20
|
+
"pocs": {
|
|
21
|
+
"curl": null
|
|
22
|
+
},
|
|
23
|
+
"tests": {
|
|
24
|
+
"jest": null,
|
|
25
|
+
"pytest": null
|
|
26
|
+
},
|
|
27
|
+
"remediation": {
|
|
28
|
+
"summary": "no template for this family yet — manual review required"
|
|
29
|
+
},
|
|
30
|
+
"stub": true
|
|
31
|
+
},
|
|
32
|
+
"struct:parser-kt.js:207:Mass_Assignment_(req.body_Direct_to_Model)": {
|
|
33
|
+
"family": "mass-assignment",
|
|
34
|
+
"exploitable": "unknown",
|
|
35
|
+
"pocs": {
|
|
36
|
+
"curl": null
|
|
37
|
+
},
|
|
38
|
+
"tests": {
|
|
39
|
+
"jest": null,
|
|
40
|
+
"pytest": null
|
|
41
|
+
},
|
|
42
|
+
"remediation": {
|
|
43
|
+
"summary": "no template for this family yet — manual review required"
|
|
44
|
+
},
|
|
45
|
+
"stub": true
|
|
46
|
+
},
|
|
47
|
+
"struct:parser-php.js:209:Mass_Assignment_(req.body_Direct_to_Model)": {
|
|
48
|
+
"family": "mass-assignment",
|
|
49
|
+
"exploitable": "unknown",
|
|
50
|
+
"pocs": {
|
|
51
|
+
"curl": null
|
|
52
|
+
},
|
|
53
|
+
"tests": {
|
|
54
|
+
"jest": null,
|
|
55
|
+
"pytest": null
|
|
56
|
+
},
|
|
57
|
+
"remediation": {
|
|
58
|
+
"summary": "no template for this family yet — manual review required"
|
|
59
|
+
},
|
|
60
|
+
"stub": true
|
|
61
|
+
},
|
|
62
|
+
"struct:parser-rb.js:201:Mass_Assignment_(req.body_Direct_to_Model)": {
|
|
63
|
+
"family": "mass-assignment",
|
|
64
|
+
"exploitable": "unknown",
|
|
65
|
+
"pocs": {
|
|
66
|
+
"curl": null
|
|
67
|
+
},
|
|
68
|
+
"tests": {
|
|
69
|
+
"jest": null,
|
|
70
|
+
"pytest": null
|
|
71
|
+
},
|
|
72
|
+
"remediation": {
|
|
73
|
+
"summary": "no template for this family yet — manual review required"
|
|
74
|
+
},
|
|
75
|
+
"stub": true
|
|
76
|
+
}
|
|
77
|
+
}
|