@clear-capabilities/agentic-security-scanner 0.142.0 → 0.144.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/CHANGELOG.md +399 -0
- package/bin/agentic-security.js +530 -54
- package/dist/1.index.js +223 -0
- package/dist/113.index.js +108 -17
- package/dist/144.index.js +163 -0
- package/dist/178.index.js +1 -1
- package/dist/238.index.js +3 -2
- package/dist/265.index.js +191 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +165 -52
- package/dist/526.index.js +108 -17
- package/dist/552.index.js +97 -0
- package/dist/637.index.js +1 -1
- package/dist/730.index.js +311 -0
- package/dist/736.index.js +301 -0
- package/dist/824.index.js +7 -0
- package/dist/905.index.js +88 -22
- package/dist/920.index.js +491 -0
- package/dist/970.index.js +109 -0
- package/dist/agentic-security.mjs +13 -13
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/calibration-seed.json +2 -0
- package/package.json +19 -11
- package/src/dataflow/index.js +18 -0
- package/src/dataflow/privacy-catalog.js +290 -0
- package/src/dataflow/privacy-deep-walker.js +515 -0
- package/src/dataflow/privacy-governance.js +126 -0
- package/src/dataflow/privacy-inventory.js +154 -0
- package/src/dataflow/privacy-sink-policy.js +125 -0
- package/src/dataflow/privacy-taint.js +115 -54
- package/src/dataflow/privacy-taxonomy.js +233 -0
- package/src/discovery/disprove.js +7 -3
- package/src/discovery/hunter.js +9 -5
- package/src/discovery/index.js +2 -2
- package/src/discovery/llm-invoke.js +69 -13
- package/src/egress/audit.js +147 -0
- package/src/egress/policy.js +313 -0
- package/src/egress/redact.js +180 -0
- package/src/engine.js +575 -288
- package/src/fix/apply-fix-service.js +403 -0
- package/src/fix/approver-registry.js +157 -0
- package/src/llm-validator/index.js +86 -9
- package/src/llm-validator/model-status.js +66 -0
- package/src/mcp/tools.js +157 -50
- package/src/pipeline/analyzer-supervisor.js +93 -0
- package/src/pipeline/analyzer-worker.js +26 -0
- package/src/pipeline/annotator-runner.js +33 -0
- package/src/pipeline/assurance-mode.js +91 -0
- package/src/pipeline/cascade-worker-pool.js +172 -0
- package/src/pipeline/cascade-worker.js +43 -0
- package/src/pipeline/coverage-ledger.js +0 -0
- package/src/pipeline/detector-runner.js +51 -0
- package/src/pipeline/enrichment-completion.js +58 -0
- package/src/pipeline/evidence-provenance.js +91 -0
- package/src/pipeline/finding-schema.js +101 -0
- package/src/pipeline/legacy-compat.js +101 -0
- package/src/pipeline/producer-collector.js +48 -0
- package/src/pipeline/producer-registry.js +112 -0
- package/src/pipeline/scan-health.js +144 -0
- package/src/posture/CLAUDE.md +2 -0
- package/src/posture/accuracy-scorecard.js +96 -1
- package/src/posture/adversary-agent.js +15 -3
- package/src/posture/artifact-registry.js +217 -0
- package/src/posture/auditor-walkthrough.js +70 -8
- package/src/posture/calibration-feedback.js +201 -0
- package/src/posture/calibration-seed.json +2 -0
- package/src/posture/calibration.js +25 -0
- package/src/posture/compliance-evidence-signing.js +131 -0
- package/src/posture/compliance-policy.js +314 -17
- package/src/posture/custom-rules.js +36 -0
- package/src/posture/deterministic.js +8 -1
- package/src/posture/encryption-provider.js +205 -0
- package/src/posture/evidence-grade-wording.js +71 -0
- package/src/posture/fix-history.js +113 -19
- package/src/posture/fix-honesty-gate.js +47 -6
- package/src/posture/fix-verify.js +56 -7
- package/src/posture/fleet.js +0 -0
- package/src/posture/flow-narration.js +7 -2
- package/src/posture/legal-hold.js +140 -0
- package/src/posture/llm-redteam.js +10 -1
- package/src/posture/material-change.js +90 -0
- package/src/posture/policy-bundle.js +274 -0
- package/src/posture/privacy-framework.js +33 -6
- package/src/posture/production-feedback.js +179 -0
- package/src/posture/retention-policy.js +132 -0
- package/src/posture/risk-dollars.js +216 -26
- package/src/posture/scan-checkpoint.js +176 -31
- package/src/posture/state-dir.js +36 -1
- package/src/posture/state-lifecycle-report.js +77 -0
- package/src/posture/suppressions.js +59 -3
- package/src/privacy/ir-adapter.js +380 -0
- package/src/report/index.js +83 -18
- package/src/report/oscal.js +635 -0
- package/src/sast/cpp.js +3 -14
- package/src/sca/llm-function-extract.js +6 -0
package/src/engine.js
CHANGED
|
@@ -8,7 +8,24 @@ import * as os from 'node:os';
|
|
|
8
8
|
import * as crypto from 'node:crypto';
|
|
9
9
|
import * as yaml from './util/yaml.js';
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
11
12
|
const _require = createRequire(import.meta.url);
|
|
13
|
+
|
|
14
|
+
// FR-202 phase 3b (D-0050): a worker thread must import.() the real, exports-bearing
|
|
15
|
+
// engine.js -- NEVER `fileURLToPath(import.meta.url)` taken naively from inside this
|
|
16
|
+
// module's own code, because once bundled by ncc that resolves to
|
|
17
|
+
// dist/agentic-security.mjs, the CLI ENTRYPOINT (bin/agentic-security.js), which has
|
|
18
|
+
// no _runFileCascade/_initCascadeWorkerState exports at all and runs CLI dispatch as
|
|
19
|
+
// a side effect of being imported. `src/` and `dist/` are always shipped as SIBLING
|
|
20
|
+
// directories (see package.json's "files"), in both a dev checkout and the published
|
|
21
|
+
// npm package, so this is stable in both the unbundled (this file IS src/engine.js)
|
|
22
|
+
// and bundled (this file's code now lives inside dist/agentic-security.mjs) cases.
|
|
23
|
+
function _resolveEngineModulePathForWorkers() {
|
|
24
|
+
const hereFile = fileURLToPath(import.meta.url);
|
|
25
|
+
const hereDir = path.dirname(hereFile);
|
|
26
|
+
if (path.basename(hereDir) === 'src') return hereFile;
|
|
27
|
+
return path.join(hereDir, '..', 'src', 'engine.js');
|
|
28
|
+
}
|
|
12
29
|
import { scanLLM } from './sast/llm.js';
|
|
13
30
|
import { scanLLMOwasp } from './sast/llm-owasp.js';
|
|
14
31
|
import { scanLlmCost } from './sast/llm-cost-advisor.js';
|
|
@@ -38,6 +55,33 @@ import { scanQuarkusHardening } from './sast/quarkus-hardening.js';
|
|
|
38
55
|
import { scanFastapiHardening } from './sast/fastapi-hardening.js';
|
|
39
56
|
import { isDeterministic } from './posture/deterministic.js';
|
|
40
57
|
import { proofCoverage } from './posture/proof-coverage.js';
|
|
58
|
+
import { computeScanHealth, applyFreshness } from './pipeline/scan-health.js';
|
|
59
|
+
import { computeCoverageLedger, summarizeCoverageForScanHealth } from './pipeline/coverage-ledger.js';
|
|
60
|
+
import { runAnnotatorAsync } from './pipeline/annotator-runner.js';
|
|
61
|
+
import { runDetector } from './pipeline/detector-runner.js';
|
|
62
|
+
import { registerProducer } from './pipeline/producer-registry.js';
|
|
63
|
+
import { collectProducerResult } from './pipeline/producer-collector.js';
|
|
64
|
+
import { completeEnrichment } from './pipeline/enrichment-completion.js';
|
|
65
|
+
|
|
66
|
+
// FR-101 (assurance-hardening PRD): the "late producers" — see
|
|
67
|
+
// pipeline/producer-registry.js's header for why this list and not the
|
|
68
|
+
// full ~90-detector set. Registered once at module load (this file is an
|
|
69
|
+
// ESM singleton; registering inside runFullScan would throw "duplicate
|
|
70
|
+
// producer id" on a second scan in the same process).
|
|
71
|
+
for (const def of [
|
|
72
|
+
{ id: 'cross-lang-openapi', version: '1.0.0', phase: 'cross-language', languages: [] },
|
|
73
|
+
{ id: 'cross-lang-grpc', version: '1.0.0', phase: 'cross-language', languages: [] },
|
|
74
|
+
{ id: 'cross-lang-graphql', version: '1.0.0', phase: 'cross-language', languages: [] },
|
|
75
|
+
{ id: 'cross-lang-orm', version: '1.0.0', phase: 'cross-language', languages: [] },
|
|
76
|
+
{ id: 'cross-lang-queues', version: '1.0.0', phase: 'cross-language', languages: [] },
|
|
77
|
+
{ id: 'iac-reachability', version: '1.0.0', phase: 'cross-language', languages: [] },
|
|
78
|
+
{ id: 'iam-policy', version: '1.0.0', phase: 'cross-language', languages: [] },
|
|
79
|
+
{ id: 'container-runtime', version: '1.0.0', phase: 'cross-language', languages: [] },
|
|
80
|
+
{ id: 'business-logic-v2', version: '1.0.0', phase: 'business-logic', languages: [] },
|
|
81
|
+
{ id: 'specification-drift', version: '1.0.0', phase: 'business-logic', languages: [] },
|
|
82
|
+
{ id: 'concurrency', version: '1.0.0', phase: 'business-logic', languages: [] },
|
|
83
|
+
{ id: 'privacy-taint', version: '1.0.0', phase: 'privacy', languages: [] },
|
|
84
|
+
]) registerProducer(def);
|
|
41
85
|
import { scanAuthZ } from './sast/authz.js';
|
|
42
86
|
import { scanApiBrokenAuthz } from './sast/api-authz.js';
|
|
43
87
|
import { scanCloudTemplates, isCloudFormationTemplate } from './sast/iac-cloud-templates.js';
|
|
@@ -139,7 +183,7 @@ import { annotateExecutionProofs } from './posture/prove-findings.js';
|
|
|
139
183
|
import { mineVulnHistory, annotateHistoricalRisk } from './posture/vuln-archaeology.js';
|
|
140
184
|
import { annotateVerifierVerdicts } from './posture/verifier.js';
|
|
141
185
|
import { annotateRegressionTests } from './posture/regression-test-gen.js';
|
|
142
|
-
import { annotateCalibratedConfidence } from './posture/calibration.js';
|
|
186
|
+
import { annotateCalibratedConfidence, calibrationFreshness } from './posture/calibration.js';
|
|
143
187
|
import { annotateStableIds } from './posture/stable-id.js';
|
|
144
188
|
import { clusterByRootCause } from './posture/clustering.js';
|
|
145
189
|
import { demoteUnreachable } from './posture/reachability-filter.js';
|
|
@@ -200,6 +244,9 @@ import { annotateRelevance } from './posture/relevance.js';
|
|
|
200
244
|
import { sweepRootCauses } from './posture/root-cause-sweep.js';
|
|
201
245
|
import { computeAnalysisTiers, countUnmodeledSinkCandidates } from './posture/coverage-report.js';
|
|
202
246
|
import { annotatePrivacyTaint, emitDpiaArtifact } from './dataflow/privacy-taint.js';
|
|
247
|
+
import { loadPrivacyGovernanceConfig, emitRopaArtifact } from './dataflow/privacy-governance.js';
|
|
248
|
+
import { buildDataInventory, emitDataInventoryArtifact, emitDataFlowGraph } from './dataflow/privacy-inventory.js';
|
|
249
|
+
import { adaptIRForPrivacyTaint } from './privacy/ir-adapter.js';
|
|
203
250
|
import { buildThreatModel as buildAutoThreatModel, persistThreatModel as persistAutoThreatModel } from './posture/threat-model-auto.js';
|
|
204
251
|
import { runApiContractScan } from './posture/api-contract.js';
|
|
205
252
|
import { annotateProvenance } from './sca/sigstore-verify.js';
|
|
@@ -235,7 +282,7 @@ import { annotateAttackPlaybooks } from './posture/attack-playbooks.js';
|
|
|
235
282
|
// R8: opt-in scan checkpointing/resume for the per-file loop.
|
|
236
283
|
import {
|
|
237
284
|
openCheckpoint, recordFileDone, completedFiles, resumeFindings, closeCheckpoint,
|
|
238
|
-
|
|
285
|
+
computeGlobalKey, globalKeyMeta, invalidatedFiles, bundleShaForRunKey,
|
|
239
286
|
} from './posture/scan-checkpoint.js';
|
|
240
287
|
import { SCANNER_VERSION as _ENGINE_VERSION } from './posture/version.js';
|
|
241
288
|
import { effectiveVersion as _effectiveRulesetVersion } from './posture/ruleset-version.js';
|
|
@@ -2311,7 +2358,8 @@ function _brokenMarkerSecurityPredicate(matchText, ctx) {
|
|
|
2311
2358
|
// SAST findings here are expected by design — suppress to avoid noise.
|
|
2312
2359
|
const _INTENTIONAL_VULN_PATH_RE = /(?:^|\/)(?:codefixes|challenge[_\-]?(?:solution|code|fix|answer)|intentional[_\-]?vuln|ctf[_\-]?solution|vulnerable[_\-]?(?:example|sample|code))(?:\/|$)/i;
|
|
2313
2360
|
|
|
2314
|
-
function scanStructuralVulns(fp, raw) {
|
|
2361
|
+
function scanStructuralVulns(fp, raw, _suppOut) {
|
|
2362
|
+
const _supp = _suppOut || _suppressionLog;
|
|
2315
2363
|
if (_INTENTIONAL_VULN_PATH_RE.test(fp.replace(/\\/g, '/'))) return [];
|
|
2316
2364
|
// Structural patterns vary: some describe code shapes (eval(), child_process.)
|
|
2317
2365
|
// and shouldn't match in strings; others ALSO scan string content (e.g.
|
|
@@ -2324,7 +2372,7 @@ function scanStructuralVulns(fp, raw) {
|
|
|
2324
2372
|
const findings = [];
|
|
2325
2373
|
const ctx = inferFileContext(fp, raw);
|
|
2326
2374
|
for (const pat of STRUCTURAL_VULN_PATTERNS) {
|
|
2327
|
-
if (!_ruleAppliesIn(pat, ctx)) {
|
|
2375
|
+
if (!_ruleAppliesIn(pat, ctx)) { _supp.push({vuln:pat.vuln,file:fp,line:0,snippet:'',reason:'context-mismatch:'+ctx.kind}); continue; }
|
|
2328
2376
|
if (pat.langScope && !pat.langScope.test(fp)) { continue; }
|
|
2329
2377
|
const re = new RegExp(pat.regex.source, pat.regex.flags);
|
|
2330
2378
|
// Default: match against the string-stripped view so rule-library shapes
|
|
@@ -2341,7 +2389,7 @@ function scanStructuralVulns(fp, raw) {
|
|
|
2341
2389
|
if (typeof pat.predicate === 'function') {
|
|
2342
2390
|
const verdict = pat.predicate(m[0], { file: fp, line, snippet, lines, raw, cleanedNoise });
|
|
2343
2391
|
if (verdict && !verdict.fire) {
|
|
2344
|
-
|
|
2392
|
+
_supp.push({vuln:pat.vuln, file:fp, line, snippet, reason:'predicate-pass:'+(verdict.reason||'ok')});
|
|
2345
2393
|
continue;
|
|
2346
2394
|
}
|
|
2347
2395
|
}
|
|
@@ -2349,7 +2397,7 @@ function scanStructuralVulns(fp, raw) {
|
|
|
2349
2397
|
// Used to scope rules like Django DEBUG=True to files that actually
|
|
2350
2398
|
// import / configure Django (avoid mis-firing on Flask's app.debug).
|
|
2351
2399
|
if (pat.contextRe && !pat.contextRe.test(raw)) {
|
|
2352
|
-
|
|
2400
|
+
_supp.push({vuln:pat.vuln, file:fp, line, snippet, reason:'context-mismatch'});
|
|
2353
2401
|
continue;
|
|
2354
2402
|
}
|
|
2355
2403
|
// FP-4: severity classifier — return null to suppress, otherwise overrides pat.severity.
|
|
@@ -2357,7 +2405,7 @@ function scanStructuralVulns(fp, raw) {
|
|
|
2357
2405
|
if (typeof pat.severityFn === 'function') {
|
|
2358
2406
|
const s = pat.severityFn(m[0], { file: fp, line, snippet, lines });
|
|
2359
2407
|
if (s === null) {
|
|
2360
|
-
|
|
2408
|
+
_supp.push({vuln:pat.vuln, file:fp, line, snippet, reason:'severity-fn:non-security-context'});
|
|
2361
2409
|
continue;
|
|
2362
2410
|
}
|
|
2363
2411
|
effectiveSeverity = s;
|
|
@@ -2366,11 +2414,11 @@ function scanStructuralVulns(fp, raw) {
|
|
|
2366
2414
|
// hardened code — an SSRF host allow/deny check, or a path
|
|
2367
2415
|
// basename/containment guard near the sink — is a false positive.
|
|
2368
2416
|
if (/SSRF/.test(pat.vuln) && _hasSsrfHostGuard({ lines, line })) {
|
|
2369
|
-
|
|
2417
|
+
_supp.push({vuln:pat.vuln, file:fp, line, snippet, reason:'ssrf-host-guard'});
|
|
2370
2418
|
continue;
|
|
2371
2419
|
}
|
|
2372
2420
|
if (/Path Traversal/.test(pat.vuln) && _hasPathGuard({ lines, line })) {
|
|
2373
|
-
|
|
2421
|
+
_supp.push({vuln:pat.vuln, file:fp, line, snippet, reason:'path-contained'});
|
|
2374
2422
|
continue;
|
|
2375
2423
|
}
|
|
2376
2424
|
const id = `struct:${fp}:${line}:${pat.vuln.replace(/\s/g, '_')}`;
|
|
@@ -3002,7 +3050,8 @@ function _logicPredicateFor(vuln){
|
|
|
3002
3050
|
return null;
|
|
3003
3051
|
}
|
|
3004
3052
|
|
|
3005
|
-
function scanLogicVulns(fp,raw){
|
|
3053
|
+
function scanLogicVulns(fp,raw,_suppOut){
|
|
3054
|
+
const _supp = _suppOut || _suppressionLog;
|
|
3006
3055
|
// Logic rules generally inspect the surrounding handler block including
|
|
3007
3056
|
// string-literal route paths and key names, so the comment-stripped (but
|
|
3008
3057
|
// string-preserving) view is the right default. Rules that explicitly only
|
|
@@ -3012,7 +3061,7 @@ function scanLogicVulns(fp,raw){
|
|
|
3012
3061
|
const lines=raw.split("\n");const results=[];
|
|
3013
3062
|
const ctx = inferFileContext(fp, raw);
|
|
3014
3063
|
for(const pat of LOGIC_PATTERNS){
|
|
3015
|
-
if (!_ruleAppliesIn(pat, ctx)) {
|
|
3064
|
+
if (!_ruleAppliesIn(pat, ctx)) { _supp.push({vuln:pat.vuln,file:fp,line:0,snippet:'',reason:'context-mismatch:'+ctx.kind}); continue; }
|
|
3016
3065
|
if (pat.langScope && !pat.langScope.test(fp)) { continue; }
|
|
3017
3066
|
const re=new RegExp(pat.regex.source,pat.regex.flags);
|
|
3018
3067
|
const predicate = _logicPredicateFor(pat.vuln);
|
|
@@ -3025,7 +3074,7 @@ function scanLogicVulns(fp,raw){
|
|
|
3025
3074
|
// FP-2: credential FP filter
|
|
3026
3075
|
if(pat.vuln==='Hardcoded Secret'||pat.vuln==='Hardcoded Credential Check'){
|
|
3027
3076
|
const fpCheck=_isFalsePositiveCredential(fp,snippet,m[0]);
|
|
3028
|
-
if(fpCheck.skip){
|
|
3077
|
+
if(fpCheck.skip){_supp.push({vuln:pat.vuln,file:fp,line,snippet,reason:fpCheck.reason});continue;}
|
|
3029
3078
|
// Stage 4 correctness audit (coverage breadth, secrets): same
|
|
3030
3079
|
// unredacted-snippet leak found in engine.js's scanEntropySecrets/
|
|
3031
3080
|
// scanCredentials and sast/secret-concat.js — this is a THIRD,
|
|
@@ -3044,7 +3093,7 @@ function scanLogicVulns(fp,raw){
|
|
|
3044
3093
|
if (predicate) {
|
|
3045
3094
|
const verdict = predicate(m[0], {file:fp, line, snippet, lines});
|
|
3046
3095
|
if (verdict && !verdict.fire) {
|
|
3047
|
-
|
|
3096
|
+
_supp.push({vuln:pat.vuln, file:fp, line, snippet, reason:'logic-gate:'+verdict.reason});
|
|
3048
3097
|
continue;
|
|
3049
3098
|
}
|
|
3050
3099
|
}
|
|
@@ -5834,14 +5883,15 @@ const EXTRA_STRUCTURAL_PATTERNS=[
|
|
|
5834
5883
|
fix:"Delete debug routes from production builds. NODE_ENV is unreliable as a security boundary."},
|
|
5835
5884
|
];
|
|
5836
5885
|
|
|
5837
|
-
function scanExtraStructural(fp,raw){
|
|
5886
|
+
function scanExtraStructural(fp,raw,_suppOut){
|
|
5887
|
+
const _supp = _suppOut || _suppressionLog;
|
|
5838
5888
|
const cleaned=stripNoiseAndStrings(raw);
|
|
5839
5889
|
const cleanedNoise=stripNoise(raw,fp);
|
|
5840
5890
|
const lines=raw.split('\n');
|
|
5841
5891
|
const findings=[];
|
|
5842
5892
|
const ctx = inferFileContext(fp, raw);
|
|
5843
5893
|
for(const pat of EXTRA_STRUCTURAL_PATTERNS){
|
|
5844
|
-
if (!_ruleAppliesIn(pat, ctx)) {
|
|
5894
|
+
if (!_ruleAppliesIn(pat, ctx)) { _supp.push({vuln:pat.vuln,file:fp,line:0,snippet:'',reason:'context-mismatch:'+ctx.kind}); continue; }
|
|
5845
5895
|
if (pat.langScope && !pat.langScope.test(fp)) { continue; }
|
|
5846
5896
|
const re=new RegExp(pat.regex.source,pat.regex.flags);
|
|
5847
5897
|
const haystack = pat.readsStringContent ? cleanedNoise : cleaned;
|
|
@@ -5851,14 +5901,14 @@ function scanExtraStructural(fp,raw){
|
|
|
5851
5901
|
const snippet=lines[line-1]?.trim()||'';
|
|
5852
5902
|
// contextRe: require a context match across the whole file.
|
|
5853
5903
|
if (pat.contextRe && !pat.contextRe.test(raw)) {
|
|
5854
|
-
|
|
5904
|
+
_supp.push({vuln:pat.vuln, file:fp, line, snippet, reason:'context-mismatch'});
|
|
5855
5905
|
continue;
|
|
5856
5906
|
}
|
|
5857
5907
|
// Per-pattern predicate gate (mirrors scanStructuralVulns).
|
|
5858
5908
|
if (typeof pat.predicate === 'function') {
|
|
5859
5909
|
const verdict = pat.predicate(m[0], { file: fp, line, snippet, lines, raw, cleanedNoise });
|
|
5860
5910
|
if (verdict && !verdict.fire) {
|
|
5861
|
-
|
|
5911
|
+
_supp.push({vuln:pat.vuln, file:fp, line, snippet, reason:'predicate-pass:'+(verdict.reason||'ok')});
|
|
5862
5912
|
continue;
|
|
5863
5913
|
}
|
|
5864
5914
|
}
|
|
@@ -5913,7 +5963,8 @@ function _isLikelyNonSecret(v, ctxLine, surroundingLines){
|
|
|
5913
5963
|
return null;
|
|
5914
5964
|
}
|
|
5915
5965
|
|
|
5916
|
-
function scanEntropySecrets(fp,raw){
|
|
5966
|
+
function scanEntropySecrets(fp,raw,_suppOut){
|
|
5967
|
+
const _supp = _suppOut || _suppressionLog;
|
|
5917
5968
|
if(raw.length>400000)return[]; // skip huge blobs
|
|
5918
5969
|
const out=[];
|
|
5919
5970
|
const lines=raw.split("\n");
|
|
@@ -5942,7 +5993,7 @@ function scanEntropySecrets(fp,raw){
|
|
|
5942
5993
|
// Same redaction concern as the main finding below applies to the
|
|
5943
5994
|
// suppression log — it's exposed via --include-suppressed, and a
|
|
5944
5995
|
// heuristic "probably not a real secret" call can be wrong.
|
|
5945
|
-
|
|
5996
|
+
_supp.push({vuln:"High-Entropy Credential Candidate",file:fp,line,snippet:ctx.trim().split(v).join(masked),reason:'entropy-'+nonSecretReason});
|
|
5946
5997
|
continue;
|
|
5947
5998
|
}
|
|
5948
5999
|
// Stage 4 correctness audit (coverage breadth, secrets): `snippet` used
|
|
@@ -7055,6 +7106,15 @@ function _osvCacheSet(key,val){try{sessionStorage.setItem('osv_'+key,JSON.string
|
|
|
7055
7106
|
// Batched: api.first.org accepts up to ~100 CVEs per request via ?cve=A,B,C…
|
|
7056
7107
|
// One HTTP round trip per 100 CVEs instead of one per CVE. Cache lookups
|
|
7057
7108
|
// remain per-CVE so a partial cache-hit still benefits.
|
|
7109
|
+
//
|
|
7110
|
+
// FR-207: EPSS scores decay in relevance (same note as posture/epss.js's
|
|
7111
|
+
// separate implementation) -- track how old the CVE-keyed entries actually
|
|
7112
|
+
// USED by this scan are, mirroring _setKevMeta immediately below, so a
|
|
7113
|
+
// stale feed is visible on scan.scanHealth rather than silently applied.
|
|
7114
|
+
const _EPSS_STALE_AFTER_MS = 7 * 24 * 60 * 60 * 1000;
|
|
7115
|
+
let _epssLiveMeta = { source: 'not-loaded', ageDays: null, stale: null, cvesChecked: 0 };
|
|
7116
|
+
export function epssLiveMeta() { return { ..._epssLiveMeta }; }
|
|
7117
|
+
|
|
7058
7118
|
const _EPSS_BATCH = 100;
|
|
7059
7119
|
async function _fetchEPSSBatch(cveIds){
|
|
7060
7120
|
if (!cveIds || !cveIds.length) return new Map();
|
|
@@ -7069,7 +7129,7 @@ async function _fetchEPSSBatch(cveIds){
|
|
|
7069
7129
|
const res = await fetch(url, { headers: { 'User-Agent': 'agentic-security/0.1' } });
|
|
7070
7130
|
if (!res.ok) {
|
|
7071
7131
|
// Mark every CVE in the batch as "tried and failed" so we don't refetch this scan.
|
|
7072
|
-
for (const c of batch) _osvCacheSet('epss:'+c,
|
|
7132
|
+
for (const c of batch) _osvCacheSet('epss:'+c, { miss: true, ts: Date.now() });
|
|
7073
7133
|
continue;
|
|
7074
7134
|
}
|
|
7075
7135
|
const j = await res.json();
|
|
@@ -7080,7 +7140,7 @@ async function _fetchEPSSBatch(cveIds){
|
|
|
7080
7140
|
const score = parseFloat(row.epss);
|
|
7081
7141
|
const percentile = parseFloat(row.percentile);
|
|
7082
7142
|
if (Number.isFinite(score) && Number.isFinite(percentile)) {
|
|
7083
|
-
const v = { score, percentile };
|
|
7143
|
+
const v = { score, percentile, ts: Date.now() };
|
|
7084
7144
|
out.set(cve, v);
|
|
7085
7145
|
_osvCacheSet('epss:'+cve, v);
|
|
7086
7146
|
seen.add(cve);
|
|
@@ -7088,7 +7148,7 @@ async function _fetchEPSSBatch(cveIds){
|
|
|
7088
7148
|
}
|
|
7089
7149
|
// CVEs in the batch that EPSS does not know — cache the negative so we
|
|
7090
7150
|
// don't retry within this scan run.
|
|
7091
|
-
for (const c of batch) if (!seen.has(c.toUpperCase())) _osvCacheSet('epss:'+c,
|
|
7151
|
+
for (const c of batch) if (!seen.has(c.toUpperCase())) _osvCacheSet('epss:'+c, { miss: true, ts: Date.now() });
|
|
7092
7152
|
} catch { /* network error → caller continues without enrichment */ }
|
|
7093
7153
|
}
|
|
7094
7154
|
return out;
|
|
@@ -7103,14 +7163,43 @@ async function _enrichWithEPSS(supplyChainResults){
|
|
|
7103
7163
|
// Cache lookup pass: keep CVEs we already have, defer the rest to one batched fetch.
|
|
7104
7164
|
const epssByCve = new Map();
|
|
7105
7165
|
const uncached = [];
|
|
7166
|
+
// FR-207: age of every entry actually used this scan, cache hit or fresh
|
|
7167
|
+
// fetch — a fresh fetch is age~0, a cache hit carries whatever `ts` it was
|
|
7168
|
+
// written with. A pre-existing on-disk entry from before this field
|
|
7169
|
+
// existed has no `ts` (`|| 0` → maximally stale), matching this
|
|
7170
|
+
// codebase's established "never-dated == already stale" convention
|
|
7171
|
+
// (compliance-policy.js's `_staleness`).
|
|
7172
|
+
const ages = [];
|
|
7106
7173
|
for (const c of allCves) {
|
|
7107
7174
|
const hit = _osvCacheGet('epss:'+c);
|
|
7108
|
-
if (hit === null) uncached.push(c);
|
|
7109
|
-
|
|
7175
|
+
if (hit === null) { uncached.push(c); continue; }
|
|
7176
|
+
if (hit === false || hit.miss) continue; // tried, no data — leave unset
|
|
7177
|
+
epssByCve.set(c, hit);
|
|
7178
|
+
ages.push(Date.now() - (hit.ts || 0));
|
|
7110
7179
|
}
|
|
7111
7180
|
if (uncached.length) {
|
|
7112
7181
|
const fetched = await _fetchEPSSBatch(uncached);
|
|
7113
|
-
for (const [cve, v] of fetched) epssByCve.set(cve, v);
|
|
7182
|
+
for (const [cve, v] of fetched) { epssByCve.set(cve, v); ages.push(Date.now() - (v.ts || 0)); }
|
|
7183
|
+
}
|
|
7184
|
+
// Offline mode means "we deliberately have no network access this run" --
|
|
7185
|
+
// mirroring _loadKEVCatalog's own offline handling immediately above,
|
|
7186
|
+
// that is a genuine "we cannot vouch for freshness right now" state
|
|
7187
|
+
// (stale: null), not license to label whatever is on disk stale OR
|
|
7188
|
+
// fresh. This matters in practice: a cache entry written before this
|
|
7189
|
+
// field existed has no `ts` at all, and without the offline carve-out
|
|
7190
|
+
// that would report as "56 years old" (age computed against epoch)
|
|
7191
|
+
// under the exact conditions (no network) where that claim is least
|
|
7192
|
+
// verifiable.
|
|
7193
|
+
if (process.env.AGENTIC_SECURITY_OFFLINE === '1') {
|
|
7194
|
+
_epssLiveMeta = { source: 'offline-skipped', ageDays: null, stale: null, cvesChecked: ages.length };
|
|
7195
|
+
} else if (ages.length) {
|
|
7196
|
+
const oldestMs = Math.max(...ages);
|
|
7197
|
+
_epssLiveMeta = {
|
|
7198
|
+
source: 'cache/live',
|
|
7199
|
+
ageDays: Math.floor(oldestMs / 86400000),
|
|
7200
|
+
stale: oldestMs > _EPSS_STALE_AFTER_MS,
|
|
7201
|
+
cvesChecked: ages.length,
|
|
7202
|
+
};
|
|
7114
7203
|
}
|
|
7115
7204
|
for (const r of out) {
|
|
7116
7205
|
const cve = (r.cveAliases || []).find(a => /^CVE-/.test(a));
|
|
@@ -8268,6 +8357,175 @@ async function queryRegistries(components){
|
|
|
8268
8357
|
|
|
8269
8358
|
// Node port: takes { fileContents, depFileContents } maps directly instead of a JSZip object.
|
|
8270
8359
|
// fileContents = code files keyed by relative path; depFileContents = manifest/lockfiles keyed by relative path.
|
|
8360
|
+
// FR-202 phase 3b (D-0050): a worker importing this module gets a FRESH, unmutated
|
|
8361
|
+
// copy of SOURCE_PATTERNS/SINK_PATTERNS/SANITIZER_PATTERNS/_projectIndex/
|
|
8362
|
+
// _GLOBAL_JAVA_TAINTED_METHODS -- none of the main thread's pre-loop setup carries
|
|
8363
|
+
// across the worker boundary. This reproduces that exact setup (mirroring
|
|
8364
|
+
// runFullScan's own pre-loop sequence) so a long-lived worker can run it ONCE at
|
|
8365
|
+
// startup, then service many _runFileCascade calls with correctly-populated state,
|
|
8366
|
+
// instead of redoing whole-project work per file.
|
|
8367
|
+
export async function _initCascadeWorkerState(fileContents, scanRoot) {
|
|
8368
|
+
_buildProjectIndex(fileContents);
|
|
8369
|
+
try { _GLOBAL_JAVA_TAINTED_METHODS = _buildGlobalJavaTaintedMethodIndex(fileContents); }
|
|
8370
|
+
catch { _GLOBAL_JAVA_TAINTED_METHODS = new Set(); }
|
|
8371
|
+
await _loadCustomRules(scanRoot);
|
|
8372
|
+
}
|
|
8373
|
+
|
|
8374
|
+
// FR-202 phase 3b (D-0050): the shape a preemptively-KILLED worker task leaves
|
|
8375
|
+
// behind. Unlike the existing post-hoc _perFileTimeoutMs check (which fires only
|
|
8376
|
+
// after a slow-but-completed cascade already populated a real delta), a worker
|
|
8377
|
+
// timeout means the file's analysis was terminated mid-execution -- there is no
|
|
8378
|
+
// partial result to salvage, so every field is empty and only the timeout marker
|
|
8379
|
+
// finding (added by the caller, same as the cooperative path) reflects the file.
|
|
8380
|
+
function _emptyCascadeDelta(c) {
|
|
8381
|
+
return {
|
|
8382
|
+
content: c, pfr: { findings: [], sources: [], sinks: [], sanitizers: [] },
|
|
8383
|
+
routes: [], findings: [], sources: [], sinks: [], sanitizers: [],
|
|
8384
|
+
logic: [], secrets: [], ciphersRest: [], ciphersTransit: [], suppressions: [],
|
|
8385
|
+
};
|
|
8386
|
+
}
|
|
8387
|
+
|
|
8388
|
+
// FR-202 phase 3a (D-0049): the per-file detector cascade returns a self-contained
|
|
8389
|
+
// delta instead of mutating the outer accumulator arrays directly, so the same
|
|
8390
|
+
// function body can later run inside a worker (phase 3b) and post its result back
|
|
8391
|
+
// rather than reaching into shared memory it would not have access to.
|
|
8392
|
+
export function _runFileCascade(p, c, scanRoot, _detectorErrors) {
|
|
8393
|
+
const _aR=[],_aF=[],_aSrc=[],_aSink=[],_aSan=[],_aLogic=[],_aSecrets=[],_aCiphersRest=[],_aCiphersTransit=[],_aSupp=[];
|
|
8394
|
+
const cc=_blankCached(c,_commentLangFor(p));_aR.push(...(runDetector(_detectorErrors,p,'scanRoutes',()=>scanRoutes(p,cc))||[]));const ta=performAnalysis(p,c);_aF.push(...ta.findings);_aSrc.push(...ta.sources);_aSink.push(...ta.sinks);_aSan.push(...ta.sanitizers);_aLogic.push(...(runDetector(_detectorErrors,p,'scanLogicVulns',()=>scanLogicVulns(p,cc,_aSupp))||[]));_aSecrets.push(...(runDetector(_detectorErrors,p,'scanCredentials',()=>scanCredentials(p,c))||[]));_aF.push(...(runDetector(_detectorErrors,p,'scanStructuralVulns',()=>scanStructuralVulns(p,cc,_aSupp))||[]));_aF.push(...(runDetector(_detectorErrors,p,'scanExtraStructural',()=>scanExtraStructural(p,cc,_aSupp))||[]));_aF.push(...(runDetector(_detectorErrors,p,'scanAliasedSinks',()=>scanAliasedSinks(p,cc))||[]));_aF.push(...(runDetector(_detectorErrors,p,'scanJavaSAST',()=>scanJavaSAST(p,cc))||[]));_aF.push(...(runDetector(_detectorErrors,p,'scanJavaBenchExtras',()=>scanJavaBenchExtras(p,cc))||[]));_aLogic.push(...(runDetector(_detectorErrors,p,'scanMiddlewareOrdering',()=>scanMiddlewareOrdering(p,cc))||[]));_aLogic.push(...(runDetector(_detectorErrors,p,'scanReDoS',()=>scanReDoS(p,cc))||[]));if(/\.(?:java|cs|kt|py|php|phtml)$/i.test(p)){_aLogic.push(...(runDetector(_detectorErrors,p,'scanRegexReDoS',()=>scanRegexReDoS(p,cc))||[]));}_aLogic.push(...(runDetector(_detectorErrors,p,'scanTodosNearSecurity',()=>scanTodosNearSecurity(p,c))||[]));_aSecrets.push(...(runDetector(_detectorErrors,p,'scanEntropySecrets',()=>scanEntropySecrets(p,c,_aSupp))||[]));const cp=scanCiphers(p,cc);_aCiphersRest.push(...cp.atRest);_aCiphersTransit.push(...cp.inTransit);if(/\.(graphql|gql)$/i.test(p))_aF.push(...(runDetector(_detectorErrors,p,'scanGraphQL',()=>scanGraphQL(p,cc))||[]));_aF.push(...(runDetector(_detectorErrors,p,'scanIaC',()=>scanIaC(p,cc))||[]));_aF.push(...(runDetector(_detectorErrors,p,'scanTerraform',()=>scanTerraform(p,cc))||[]));_aF.push(...(runDetector(_detectorErrors,p,'scanCloudTemplates',()=>scanCloudTemplates(p,c))||[]));
|
|
8395
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanLLM',()=>scanLLM(p,c))||[]));
|
|
8396
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanLLMOwasp',()=>scanLLMOwasp(p,c))||[]));
|
|
8397
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanLlmCost',()=>scanLlmCost(p,c))||[]));
|
|
8398
|
+
_aLogic.push(...(runDetector(_detectorErrors,p,'scanBusinessLogic',()=>scanBusinessLogic(p,cc))||[]));
|
|
8399
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanPipeline',()=>scanPipeline(p,cc))||[]));
|
|
8400
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanContainer',()=>scanContainer(p,cc))||[]));
|
|
8401
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanInstallScripts',()=>scanInstallScripts(p,cc))||[]));
|
|
8402
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanMCP',()=>scanMCP(p,c))||[]));
|
|
8403
|
+
// PRD F5.2 — rug-pull: a tool whose definition changed AFTER approval.
|
|
8404
|
+
// Every scanMCP rule judges the CURRENT content, so a description that is
|
|
8405
|
+
// innocuous today and hostile tomorrow passes both scans. This compares
|
|
8406
|
+
// against a recorded baseline, which is the only way to see a change.
|
|
8407
|
+
// Wired here rather than left as a tested module: a detector with no call
|
|
8408
|
+
// site is a dark detector, which is the exact class this session keeps
|
|
8409
|
+
// finding.
|
|
8410
|
+
if (/(?:^|[\\/])\.?mcp(?:\.[a-z]+)?\.json$|(?:^|[\\/])\.mcp\.json$/i.test(p)) {
|
|
8411
|
+
try {
|
|
8412
|
+
const _cfg = JSON.parse(c);
|
|
8413
|
+
const _rp = _detectRugPull(scanRoot, _cfg, { file: p });
|
|
8414
|
+
_aF.push(..._rp.findings);
|
|
8415
|
+
// Record on first sight so the NEXT scan has something to compare
|
|
8416
|
+
// against; refresh after reporting so a reviewed change is not
|
|
8417
|
+
// re-reported forever.
|
|
8418
|
+
_saveMcpBaseline(scanRoot, _fingerprintMcp(_cfg));
|
|
8419
|
+
} catch (e) {
|
|
8420
|
+
// Only a malformed config is tolerated here — scanMCP already reports
|
|
8421
|
+
// what it can from one. Anything else is a programmer error and must
|
|
8422
|
+
// not be swallowed: a bare `catch {}` around this block hid a
|
|
8423
|
+
// ReferenceError (`root` vs `scanRoot`) that silently disabled the
|
|
8424
|
+
// whole detector while every unit test still passed.
|
|
8425
|
+
if (!(e instanceof SyntaxError)) throw e;
|
|
8426
|
+
}
|
|
8427
|
+
}
|
|
8428
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanClaudeSettings',()=>scanClaudeSettings(p,c))||[]));
|
|
8429
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanClaudeMdPromptInjection',()=>scanClaudeMdPromptInjection(p,c))||[]));
|
|
8430
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanClaudeHookInjection',()=>scanClaudeHookInjection(p,c))||[]));
|
|
8431
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanDjangoHardening',()=>scanDjangoHardening(p,cc))||[]));
|
|
8432
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanDefiDeep',()=>scanDefiDeep(p,cc))||[]));
|
|
8433
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanSpringbootHardening',()=>scanSpringbootHardening(p,cc))||[]));
|
|
8434
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanLaravelHardening',()=>scanLaravelHardening(p,cc))||[]));
|
|
8435
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanSwift',()=>scanSwift(p,cc))||[]));
|
|
8436
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanDartFlutter',()=>scanDartFlutter(p,cc))||[]));
|
|
8437
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanWeakRandomness',()=>scanWeakRandomness(p,cc))||[]));
|
|
8438
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanGraphQLModule',()=>scanGraphQLModule(p,cc))||[]));
|
|
8439
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanSensitiveDataLogging',()=>scanSensitiveDataLogging(p,cc))||[]));
|
|
8440
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanComparisonSafety',()=>scanComparisonSafety(p,cc))||[]));
|
|
8441
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanWeakPasswordHash',()=>scanWeakPasswordHash(p,cc))||[]));
|
|
8442
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCachePoisoning',()=>scanCachePoisoning(p,cc))||[]));
|
|
8443
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanNullByteInjection',()=>scanNullByteInjection(p,cc))||[]));
|
|
8444
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanLlmTradingAgent',()=>scanLlmTradingAgent(p,c))||[]));
|
|
8445
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanMobileManifest',()=>scanMobileManifest(p,cc))||[]));
|
|
8446
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanQuarkusHardening',()=>scanQuarkusHardening(p,cc))||[]));
|
|
8447
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanFastapiHardening',()=>scanFastapiHardening(p,cc))||[]));
|
|
8448
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanAuthZ',()=>scanAuthZ(p,cc))||[]));
|
|
8449
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanModelLoad',()=>scanModelLoad(p,cc))||[]));
|
|
8450
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanPromptTemplate',()=>scanPromptTemplate(p,c))||[]));
|
|
8451
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanXXE',()=>scanXXE(p,cc))||[]));
|
|
8452
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanJNDI',()=>scanJNDI(p,cc))||[]));
|
|
8453
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanJavaDeserialization',()=>scanJavaDeserialization(p,cc))||[]));
|
|
8454
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanJwtExp',()=>scanJwtExp(p,cc))||[]));
|
|
8455
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanZipSlip',()=>scanZipSlip(p,cc))||[]));_aF.push(...(runDetector(_detectorErrors,p,'scanSiblingGuard',()=>scanSiblingGuard(p,cc))||[]));
|
|
8456
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanFileUpload',()=>scanFileUpload(p,cc))||[]));
|
|
8457
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanHostHeader',()=>scanHostHeader(p,cc))||[]));
|
|
8458
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanPythonSinks',()=>scanPythonSinks(p,cc))||[]));
|
|
8459
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCSharp',()=>scanCSharp(p,cc))||[]));
|
|
8460
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCpp',()=>scanCpp(p,cc))||[]));
|
|
8461
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanSolidity',()=>scanSolidity(p,cc))||[]));
|
|
8462
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanRust',()=>scanRust(p,cc))||[]));
|
|
8463
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanGoExtended',()=>scanGoExtended(p,cc))||[]));
|
|
8464
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanDatabaseRLS',()=>scanDatabaseRLS(p,cc))||[]));
|
|
8465
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanRateLimit',()=>scanRateLimit(p,cc))||[]));_aF.push(...(runDetector(_detectorErrors,p,'scanResourceExhaustion',()=>scanResourceExhaustion(p,cc))||[]));_aF.push(...(runDetector(_detectorErrors,p,'scanRedirectToctou',()=>scanRedirectToctou(p,cc))||[]));_aF.push(...(runDetector(_detectorErrors,p,'scanCodegenSink',()=>scanCodegenSink(p,cc))||[]));_aF.push(...(runDetector(_detectorErrors,p,'scanOwnershipAuthz',()=>scanOwnershipAuthz(p,cc))||[]));
|
|
8466
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanAuthProvider',()=>scanAuthProvider(p,cc))||[]));
|
|
8467
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanEnvHygiene',()=>scanEnvHygiene(p,cc))||[]));
|
|
8468
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanWebhook',()=>scanWebhook(p,cc))||[]));
|
|
8469
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanClientSide',()=>scanClientSide(p,cc))||[]));
|
|
8470
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanPromptFirewall',()=>scanPromptFirewall(p,c))||[]));
|
|
8471
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanLlmRedteam',()=>scanLlmRedteam(p,c))||[]));
|
|
8472
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanJulietShape',()=>scanJulietShape(p,c))||[]));
|
|
8473
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCppDataflow',()=>scanCppDataflow(p,cc))||[]));
|
|
8474
|
+
// Phase 1: new detectors.
|
|
8475
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanMassAssignment',()=>scanMassAssignment(p,cc))||[]));
|
|
8476
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanPrototypePollution',()=>scanPrototypePollution(p,cc))||[]));
|
|
8477
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCSRF',()=>scanCSRF(p,cc))||[]));
|
|
8478
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanTOCTOU',()=>scanTOCTOU(p,cc))||[]));
|
|
8479
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanNoSQLInjection',()=>scanNoSQLInjection(p,cc))||[]));
|
|
8480
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanLDAPInjection',()=>scanLDAPInjection(p,cc))||[]));
|
|
8481
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanXPathInjection',()=>scanXPathInjection(p,cc))||[]));
|
|
8482
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanSSTI',()=>scanSSTI(p,cc))||[]));
|
|
8483
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanOpenRedirect',()=>scanOpenRedirect(p,cc))||[]));
|
|
8484
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanWrongContextSanitizer',()=>scanWrongContextSanitizer(p,cc))||[]));
|
|
8485
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanSanitizerContextMismatch',()=>scanSanitizerContextMismatch(p,cc))||[]));
|
|
8486
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanFrontendHygiene',()=>scanFrontendHygiene(p,cc))||[]));
|
|
8487
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCsvInjection',()=>scanCsvInjection(p,cc))||[]));
|
|
8488
|
+
// R16 — specialist crypto-hygiene classes (constant-time comparison,
|
|
8489
|
+
// secret zeroization). Narrow by design: keyed on the secret-ness of the
|
|
8490
|
+
// identifier, and silent whenever the correct constant-time or
|
|
8491
|
+
// guaranteed-wipe API is already present.
|
|
8492
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCryptoSpecialist',()=>scanCryptoSpecialist(p,cc))||[]));
|
|
8493
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanStoredTaint',()=>scanStoredTaint(p,cc))||[]));
|
|
8494
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanJavaStructural',()=>scanJavaStructural(p,cc))||[]));
|
|
8495
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCsharpStructural',()=>scanCsharpStructural(p,cc))||[]));
|
|
8496
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanJsFrameworkStructural',()=>scanJsFrameworkStructural(p,cc))||[]));
|
|
8497
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanPythonStructural',()=>scanPythonStructural(p,cc))||[]));
|
|
8498
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanGoStructural',()=>scanGoStructural(p,cc))||[]));
|
|
8499
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanSecretConcat',()=>scanSecretConcat(p,cc))||[]));
|
|
8500
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanXssReflectedMultilang',()=>scanXssReflectedMultilang(p,cc))||[]));
|
|
8501
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCodeInjectionMultilang',()=>scanCodeInjectionMultilang(p,cc))||[]));
|
|
8502
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanResponseSplitting',()=>scanResponseSplitting(p,cc))||[]));
|
|
8503
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanStoredPromptInjection',()=>scanStoredPromptInjection(p,c))||[]));
|
|
8504
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanRAGPoisoning',()=>scanRAGPoisoning(p,c))||[]));
|
|
8505
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanAgentToolEscalation',()=>scanAgentToolEscalation(p,c))||[]));
|
|
8506
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanAgentUntrustedFlow',()=>scanAgentUntrustedFlow(p,c))||[]));
|
|
8507
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanEventEntrypoints',()=>scanEventEntrypoints(p,cc))||[]));
|
|
8508
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanDbTaint',()=>scanDbTaint(p,cc))||[]));
|
|
8509
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanSSRFCloudMetadata',()=>scanSSRFCloudMetadata(p,cc))||[]));
|
|
8510
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanMutationXSS',()=>scanMutationXSS(p,cc))||[]));
|
|
8511
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanKotlin',()=>scanKotlin(p,cc))||[]));
|
|
8512
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanRuby',()=>scanRuby(p,cc))||[]));_aF.push(...(runDetector(_detectorErrors,p,'scanRubyPathJoin',()=>scanRubyPathJoin(p,cc))||[]));
|
|
8513
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanPhp',()=>scanPhp(p,cc))||[]));
|
|
8514
|
+
// Integration block: scaffolded SAST scanners. Gated by env var.
|
|
8515
|
+
if (process.env.AGENTIC_SECURITY_NO_INTEGRATION !== '1') {
|
|
8516
|
+
if (process.env.AGENTIC_SECURITY_NO_LLM_APP !== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanLlmApp',()=>scanLlmApp(p,c))||[]));
|
|
8517
|
+
if (process.env.AGENTIC_SECURITY_NO_MOBILE !== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanMobile',()=>scanMobile(p,cc))||[]));
|
|
8518
|
+
if (process.env.AGENTIC_SECURITY_NO_PQC !== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanPqc',()=>scanPqc(p,cc))||[]));
|
|
8519
|
+
if (process.env.AGENTIC_SECURITY_NO_WEB3_ADV!== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanWeb3Advanced',()=>scanWeb3Advanced(p,cc))||[]));
|
|
8520
|
+
if (process.env.AGENTIC_SECURITY_NO_DAPP !== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanDappFrontend',()=>scanDappFrontend(p,cc))||[]));
|
|
8521
|
+
if (process.env.AGENTIC_SECURITY_NO_CLOUD_IAM!== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanCloudIam',()=>scanCloudIam(p,cc))||[]));
|
|
8522
|
+
if (process.env.AGENTIC_SECURITY_NO_K8S_ADM !== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanK8sAdmission',()=>scanK8sAdmission(p,cc))||[]));
|
|
8523
|
+
if (process.env.AGENTIC_SECURITY_NO_CRYPTO_PROTO !== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanCryptoProtocol',()=>scanCryptoProtocol(p,cc))||[]));
|
|
8524
|
+
if (process.env.AGENTIC_SECURITY_NO_ML_SUPPLY !== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanMlSupplyChain',()=>scanMlSupplyChain(p,cc))||[]));
|
|
8525
|
+
}
|
|
8526
|
+
return {content:c, pfr:ta, routes:_aR, findings:_aF, sources:_aSrc, sinks:_aSink, sanitizers:_aSan, logic:_aLogic, secrets:_aSecrets, ciphersRest:_aCiphersRest, ciphersTransit:_aCiphersTransit, suppressions:_aSupp};
|
|
8527
|
+
}
|
|
8528
|
+
|
|
8271
8529
|
async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null, resume=undefined, deep=undefined, deepInCi=undefined}, setProgress=()=>{}){_resetSuppressions();_buildProjectIndex(fileContents);await _loadCustomRules(scanRoot);
|
|
8272
8530
|
// Pre-pass: build cross-file Java tainted-method index so per-file taint
|
|
8273
8531
|
// analysis can recognize calls to user-input-returning helper methods
|
|
@@ -8295,7 +8553,7 @@ function _deterministicFileTimings(timings) {
|
|
|
8295
8553
|
|
|
8296
8554
|
const _fileTimings = [];
|
|
8297
8555
|
let _filesSkipped = 0, _filesTimedOut = 0, _filesDenseSkipped = 0;
|
|
8298
|
-
const files=Object.keys(fileContents).filter(f=>(shouldScan(f) || isKubernetesManifest(f, fileContents[f]) || isCloudFormationTemplate(f, fileContents[f]) || isInstructionFile(f)) && !_isPathIgnored(f));const fc={},pfr={};const aR=[],aF=[],aSrc=[],aSink=[],aSan=[],aLogic=[],aSupply=[],aSecrets=[],aCiphersRest=[],aCiphersTransit=[];
|
|
8556
|
+
const files=Object.keys(fileContents).filter(f=>(shouldScan(f) || isKubernetesManifest(f, fileContents[f]) || isCloudFormationTemplate(f, fileContents[f]) || isInstructionFile(f)) && !_isPathIgnored(f));const fc={},pfr={};const aR=[],aF=[],aSrc=[],aSink=[],aSan=[],aLogic=[],aSupply=[],aSecrets=[],aCiphersRest=[],aCiphersTransit=[];const _detectorErrors=[];
|
|
8299
8557
|
// ---- R8: opt-in per-file checkpointing (AGENTIC_SECURITY_RESUME=1, or
|
|
8300
8558
|
// runScan({resume:true})). Default OFF, so existing behaviour is untouched.
|
|
8301
8559
|
// Only this loop is checkpointed; every cross-file pass below re-runs, so
|
|
@@ -8303,17 +8561,28 @@ function _deterministicFileTimings(timings) {
|
|
|
8303
8561
|
const _ckptEnabled = (resume === undefined ? process.env.AGENTIC_SECURITY_RESUME === '1' : !!resume) && !!scanRoot;
|
|
8304
8562
|
let _ckpt = null; const _ckptPayloads = new Map(); let _ckptDone = new Set(); let _ckptResumed = 0, _ckptWrites = 0;
|
|
8305
8563
|
const _ckptAbortAfter = parseInt(process.env.AGENTIC_SECURITY_CHECKPOINT_ABORT_AFTER || '0', 10) || 0;
|
|
8564
|
+
let _ckptInvalidated = [];
|
|
8306
8565
|
if (_ckptEnabled) {
|
|
8307
8566
|
try {
|
|
8308
|
-
const
|
|
8567
|
+
const _ckptIdentity = {
|
|
8309
8568
|
engineVersion: _ENGINE_VERSION,
|
|
8310
8569
|
rulesetVersion: (_effectiveRulesetVersion(scanRoot) || {}).version,
|
|
8311
8570
|
bundleSha: bundleShaForRunKey(),
|
|
8312
|
-
|
|
8571
|
+
depFileContents,
|
|
8572
|
+
};
|
|
8573
|
+
// FR-208: the global key covers everything that would affect how EVERY
|
|
8574
|
+
// file is analysed (engine/ruleset/bundle/deps/env); each scanned
|
|
8575
|
+
// file's OWN content is checked per-record inside openCheckpoint
|
|
8576
|
+
// instead, so changing one file no longer discards every other file's
|
|
8577
|
+
// already-completed work — see scan-checkpoint.js's module header.
|
|
8578
|
+
_ckpt = openCheckpoint(scanRoot, {
|
|
8579
|
+
globalKey: computeGlobalKey(_ckptIdentity),
|
|
8580
|
+
meta: globalKeyMeta(_ckptIdentity),
|
|
8581
|
+
fileContents,
|
|
8313
8582
|
});
|
|
8314
|
-
_ckpt = openCheckpoint(scanRoot, { runKey: _runKey });
|
|
8315
8583
|
for (const r of resumeFindings(_ckpt)) { if (r && r.findings) _ckptPayloads.set(r.file, r.findings); }
|
|
8316
8584
|
_ckptDone = completedFiles(_ckpt);
|
|
8585
|
+
_ckptInvalidated = invalidatedFiles(_ckpt);
|
|
8317
8586
|
} catch (_) { _ckpt = null; }
|
|
8318
8587
|
}
|
|
8319
8588
|
// Replay a checkpointed file's ENTIRE contribution, in the same array order
|
|
@@ -8360,146 +8629,45 @@ function _deterministicFileTimings(timings) {
|
|
|
8360
8629
|
// is what the checkpoint format has to survive. Never set in normal use.
|
|
8361
8630
|
if (_ckptAbortAfter > 0 && _ckptWrites >= _ckptAbortAfter) process.exit(137);
|
|
8362
8631
|
};
|
|
8363
|
-
|
|
8632
|
+
// FR-202 phase 3b (D-0050): opt-in real deadline enforcement for the per-file
|
|
8633
|
+
// cascade via a reused worker pool. Off by default -- the synchronous path above
|
|
8634
|
+
// this loop is completely unchanged when the flag is unset.
|
|
8635
|
+
const _useWorkerCascade = process.env.AGENTIC_SECURITY_WORKER_CASCADE === '1';
|
|
8636
|
+
let _cascadePool = null;
|
|
8637
|
+
if (_useWorkerCascade) {
|
|
8638
|
+
const { createCascadePool } = await import('./pipeline/cascade-worker-pool.js');
|
|
8639
|
+
_cascadePool = createCascadePool({
|
|
8640
|
+
fileContents, scanRoot,
|
|
8641
|
+
modulePath: _resolveEngineModulePathForWorkers(),
|
|
8642
|
+
poolSize: Number(process.env.AGENTIC_SECURITY_WORKER_POOL_SIZE) || 4,
|
|
8643
|
+
});
|
|
8644
|
+
}
|
|
8645
|
+
let i=0;
|
|
8646
|
+
try {
|
|
8647
|
+
for(const p of files){i++;const _ft0=Date.now();setProgress({current:i,total:files.length,file:p.split("/").pop(),phase:"Scanning"});
|
|
8364
8648
|
if(_ckptDone.has(p)&&_ckptReplay(p))continue;
|
|
8365
8649
|
const _mk={aR:aR.length,aF:aF.length,aSrc:aSrc.length,aSink:aSink.length,aSan:aSan.length,aLogic:aLogic.length,aSecrets:aSecrets.length,aCR:aCiphersRest.length,aCT:aCiphersTransit.length,sup:_suppressionLog.length};
|
|
8366
|
-
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;}
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
// PRD F5.2 — rug-pull: a tool whose definition changed AFTER approval.
|
|
8376
|
-
// Every scanMCP rule judges the CURRENT content, so a description that is
|
|
8377
|
-
// innocuous today and hostile tomorrow passes both scans. This compares
|
|
8378
|
-
// against a recorded baseline, which is the only way to see a change.
|
|
8379
|
-
// Wired here rather than left as a tested module: a detector with no call
|
|
8380
|
-
// site is a dark detector, which is the exact class this session keeps
|
|
8381
|
-
// finding.
|
|
8382
|
-
if (/(?:^|[\\/])\.?mcp(?:\.[a-z]+)?\.json$|(?:^|[\\/])\.mcp\.json$/i.test(p)) {
|
|
8383
|
-
try {
|
|
8384
|
-
const _cfg = JSON.parse(c);
|
|
8385
|
-
const _rp = _detectRugPull(scanRoot, _cfg, { file: p });
|
|
8386
|
-
aF.push(..._rp.findings);
|
|
8387
|
-
// Record on first sight so the NEXT scan has something to compare
|
|
8388
|
-
// against; refresh after reporting so a reviewed change is not
|
|
8389
|
-
// re-reported forever.
|
|
8390
|
-
_saveMcpBaseline(scanRoot, _fingerprintMcp(_cfg));
|
|
8391
|
-
} catch (e) {
|
|
8392
|
-
// Only a malformed config is tolerated here — scanMCP already reports
|
|
8393
|
-
// what it can from one. Anything else is a programmer error and must
|
|
8394
|
-
// not be swallowed: a bare `catch {}` around this block hid a
|
|
8395
|
-
// ReferenceError (`root` vs `scanRoot`) that silently disabled the
|
|
8396
|
-
// whole detector while every unit test still passed.
|
|
8397
|
-
if (!(e instanceof SyntaxError)) throw e;
|
|
8398
|
-
}
|
|
8399
|
-
}
|
|
8400
|
-
aF.push(...scanClaudeSettings(p,c));
|
|
8401
|
-
aF.push(...scanClaudeMdPromptInjection(p,c));
|
|
8402
|
-
aF.push(...scanClaudeHookInjection(p,c));
|
|
8403
|
-
aF.push(...scanDjangoHardening(p,cc));
|
|
8404
|
-
aF.push(...scanDefiDeep(p,cc));
|
|
8405
|
-
aF.push(...scanSpringbootHardening(p,cc));
|
|
8406
|
-
aF.push(...scanLaravelHardening(p,cc));
|
|
8407
|
-
aF.push(...scanSwift(p,cc));
|
|
8408
|
-
aF.push(...scanDartFlutter(p,cc));
|
|
8409
|
-
aF.push(...scanWeakRandomness(p,cc));
|
|
8410
|
-
aF.push(...scanGraphQLModule(p,cc));
|
|
8411
|
-
aF.push(...scanSensitiveDataLogging(p,cc));
|
|
8412
|
-
aF.push(...scanComparisonSafety(p,cc));
|
|
8413
|
-
aF.push(...scanWeakPasswordHash(p,cc));
|
|
8414
|
-
aF.push(...scanCachePoisoning(p,cc));
|
|
8415
|
-
aF.push(...scanNullByteInjection(p,cc));
|
|
8416
|
-
aF.push(...scanLlmTradingAgent(p,c));
|
|
8417
|
-
aF.push(...scanMobileManifest(p,cc));
|
|
8418
|
-
aF.push(...scanQuarkusHardening(p,cc));
|
|
8419
|
-
aF.push(...scanFastapiHardening(p,cc));
|
|
8420
|
-
aF.push(...scanAuthZ(p,cc));
|
|
8421
|
-
aF.push(...scanModelLoad(p,cc));
|
|
8422
|
-
aF.push(...scanPromptTemplate(p,c));
|
|
8423
|
-
aF.push(...scanXXE(p,cc));
|
|
8424
|
-
aF.push(...scanJNDI(p,cc));
|
|
8425
|
-
aF.push(...scanJavaDeserialization(p,cc));
|
|
8426
|
-
aF.push(...scanJwtExp(p,cc));
|
|
8427
|
-
aF.push(...scanZipSlip(p,cc));aF.push(...scanSiblingGuard(p,cc));
|
|
8428
|
-
aF.push(...scanFileUpload(p,cc));
|
|
8429
|
-
aF.push(...scanHostHeader(p,cc));
|
|
8430
|
-
aF.push(...scanPythonSinks(p,cc));
|
|
8431
|
-
aF.push(...scanCSharp(p,cc));
|
|
8432
|
-
aF.push(...scanCpp(p,cc));
|
|
8433
|
-
aF.push(...scanSolidity(p,cc));
|
|
8434
|
-
aF.push(...scanRust(p,cc));
|
|
8435
|
-
aF.push(...scanGoExtended(p,cc));
|
|
8436
|
-
aF.push(...scanDatabaseRLS(p,cc));
|
|
8437
|
-
aF.push(...scanRateLimit(p,cc));aF.push(...scanResourceExhaustion(p,cc));aF.push(...scanRedirectToctou(p,cc));aF.push(...scanCodegenSink(p,cc));aF.push(...scanOwnershipAuthz(p,cc));
|
|
8438
|
-
aF.push(...scanAuthProvider(p,cc));
|
|
8439
|
-
aF.push(...scanEnvHygiene(p,cc));
|
|
8440
|
-
aF.push(...scanWebhook(p,cc));
|
|
8441
|
-
aF.push(...scanClientSide(p,cc));
|
|
8442
|
-
aF.push(...scanPromptFirewall(p,c));
|
|
8443
|
-
aF.push(...scanLlmRedteam(p,c));
|
|
8444
|
-
aF.push(...scanJulietShape(p,c));
|
|
8445
|
-
aF.push(...scanCppDataflow(p,cc));
|
|
8446
|
-
// Phase 1: new detectors.
|
|
8447
|
-
aF.push(...scanMassAssignment(p,cc));
|
|
8448
|
-
aF.push(...scanPrototypePollution(p,cc));
|
|
8449
|
-
aF.push(...scanCSRF(p,cc));
|
|
8450
|
-
aF.push(...scanTOCTOU(p,cc));
|
|
8451
|
-
aF.push(...scanNoSQLInjection(p,cc));
|
|
8452
|
-
aF.push(...scanLDAPInjection(p,cc));
|
|
8453
|
-
aF.push(...scanXPathInjection(p,cc));
|
|
8454
|
-
aF.push(...scanSSTI(p,cc));
|
|
8455
|
-
aF.push(...scanOpenRedirect(p,cc));
|
|
8456
|
-
aF.push(...scanWrongContextSanitizer(p,cc));
|
|
8457
|
-
aF.push(...scanSanitizerContextMismatch(p,cc));
|
|
8458
|
-
aF.push(...scanFrontendHygiene(p,cc));
|
|
8459
|
-
aF.push(...scanCsvInjection(p,cc));
|
|
8460
|
-
// R16 — specialist crypto-hygiene classes (constant-time comparison,
|
|
8461
|
-
// secret zeroization). Narrow by design: keyed on the secret-ness of the
|
|
8462
|
-
// identifier, and silent whenever the correct constant-time or
|
|
8463
|
-
// guaranteed-wipe API is already present.
|
|
8464
|
-
aF.push(...scanCryptoSpecialist(p,cc));
|
|
8465
|
-
aF.push(...scanStoredTaint(p,cc));
|
|
8466
|
-
aF.push(...scanJavaStructural(p,cc));
|
|
8467
|
-
aF.push(...scanCsharpStructural(p,cc));
|
|
8468
|
-
aF.push(...scanJsFrameworkStructural(p,cc));
|
|
8469
|
-
aF.push(...scanPythonStructural(p,cc));
|
|
8470
|
-
aF.push(...scanGoStructural(p,cc));
|
|
8471
|
-
aF.push(...scanSecretConcat(p,cc));
|
|
8472
|
-
aF.push(...scanXssReflectedMultilang(p,cc));
|
|
8473
|
-
aF.push(...scanCodeInjectionMultilang(p,cc));
|
|
8474
|
-
aF.push(...scanResponseSplitting(p,cc));
|
|
8475
|
-
aF.push(...scanStoredPromptInjection(p,c));
|
|
8476
|
-
aF.push(...scanRAGPoisoning(p,c));
|
|
8477
|
-
aF.push(...scanAgentToolEscalation(p,c));
|
|
8478
|
-
aF.push(...scanAgentUntrustedFlow(p,c));
|
|
8479
|
-
aF.push(...scanEventEntrypoints(p,cc));
|
|
8480
|
-
aF.push(...scanDbTaint(p,cc));
|
|
8481
|
-
aF.push(...scanSSRFCloudMetadata(p,cc));
|
|
8482
|
-
aF.push(...scanMutationXSS(p,cc));
|
|
8483
|
-
aF.push(...scanKotlin(p,cc));
|
|
8484
|
-
aF.push(...scanRuby(p,cc));aF.push(...scanRubyPathJoin(p,cc));
|
|
8485
|
-
aF.push(...scanPhp(p,cc));
|
|
8486
|
-
// Integration block: scaffolded SAST scanners. Gated by env var.
|
|
8487
|
-
if (process.env.AGENTIC_SECURITY_NO_INTEGRATION !== '1') {
|
|
8488
|
-
if (process.env.AGENTIC_SECURITY_NO_LLM_APP !== '1') aF.push(...scanLlmApp(p,c));
|
|
8489
|
-
if (process.env.AGENTIC_SECURITY_NO_MOBILE !== '1') aF.push(...scanMobile(p,cc));
|
|
8490
|
-
if (process.env.AGENTIC_SECURITY_NO_PQC !== '1') aF.push(...scanPqc(p,cc));
|
|
8491
|
-
if (process.env.AGENTIC_SECURITY_NO_WEB3_ADV!== '1') aF.push(...scanWeb3Advanced(p,cc));
|
|
8492
|
-
if (process.env.AGENTIC_SECURITY_NO_DAPP !== '1') aF.push(...scanDappFrontend(p,cc));
|
|
8493
|
-
if (process.env.AGENTIC_SECURITY_NO_CLOUD_IAM!== '1') aF.push(...scanCloudIam(p,cc));
|
|
8494
|
-
if (process.env.AGENTIC_SECURITY_NO_K8S_ADM !== '1') aF.push(...scanK8sAdmission(p,cc));
|
|
8495
|
-
if (process.env.AGENTIC_SECURITY_NO_CRYPTO_PROTO !== '1') aF.push(...scanCryptoProtocol(p,cc));
|
|
8496
|
-
if (process.env.AGENTIC_SECURITY_NO_ML_SUPPLY !== '1') aF.push(...scanMlSupplyChain(p,cc));
|
|
8650
|
+
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;}
|
|
8651
|
+
let _delta;
|
|
8652
|
+
if (_cascadePool) {
|
|
8653
|
+
const _res = await _cascadePool.runFile(p, c, scanRoot, _detectorErrors, { timeoutMs: _perFileTimeoutMs });
|
|
8654
|
+
if (_res.ok) { _delta = _res.result; }
|
|
8655
|
+
else if (_res.timedOut) { _delta = _emptyCascadeDelta(c); }
|
|
8656
|
+
else { throw new Error(_res.error); }
|
|
8657
|
+
} else {
|
|
8658
|
+
_delta = _runFileCascade(p,c,scanRoot,_detectorErrors);
|
|
8497
8659
|
}
|
|
8660
|
+
fc[p]=_delta.content;pfr[p]=_delta.pfr;
|
|
8661
|
+
aR.push(..._delta.routes);aF.push(..._delta.findings);aSrc.push(..._delta.sources);aSink.push(..._delta.sinks);aSan.push(..._delta.sanitizers);aLogic.push(..._delta.logic);aSecrets.push(..._delta.secrets);aCiphersRest.push(..._delta.ciphersRest);aCiphersTransit.push(..._delta.ciphersTransit);_suppressionLog.push(..._delta.suppressions);
|
|
8498
8662
|
const _ftElapsed=Date.now()-_ft0;
|
|
8499
8663
|
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++;}
|
|
8500
8664
|
_fileTimings.push({file:p,ms:_ftElapsed});
|
|
8501
|
-
_ckptRecord(p,_mk,_ftElapsed,
|
|
8502
|
-
|
|
8665
|
+
_ckptRecord(p,_mk,_ftElapsed,_delta.pfr);
|
|
8666
|
+
}catch(_){_fileTimings.push({file:p,ms:Date.now()-_ft0,error:true});}
|
|
8667
|
+
if(i%5===0)await new Promise(r=>setTimeout(r,0));}
|
|
8668
|
+
} finally {
|
|
8669
|
+
if (_cascadePool) await _cascadePool.shutdown();
|
|
8670
|
+
}
|
|
8503
8671
|
// Deserialization-gadget detector runs once with full-tree context (it needs
|
|
8504
8672
|
// manifest contents to know which gadget libs are on the classpath).
|
|
8505
8673
|
try {
|
|
@@ -8507,7 +8675,7 @@ function _deterministicFileTimings(timings) {
|
|
|
8507
8675
|
if (_gadgets.size) {
|
|
8508
8676
|
for (const p of files) {
|
|
8509
8677
|
const c = fc[p]; if (!c) continue;
|
|
8510
|
-
aF.push(...scanDeserializationGadgets(p, c, { gadgets: _gadgets }));
|
|
8678
|
+
aF.push(...(runDetector(_detectorErrors,p,'scanDeserializationGadgets',()=>scanDeserializationGadgets(p, c, { gadgets: _gadgets }))||[]));
|
|
8511
8679
|
}
|
|
8512
8680
|
}
|
|
8513
8681
|
} catch(_) {}
|
|
@@ -8650,17 +8818,17 @@ function _deterministicFileTimings(timings) {
|
|
|
8650
8818
|
// R19 (PRD §5): cross-route BOLA/BFLA over the aggregated route inventory.
|
|
8651
8819
|
// Convention deviation is PROJECT-scoped by design: the convention is a
|
|
8652
8820
|
// property of the codebase, not of one file (see convention-deviation.js).
|
|
8653
|
-
|
|
8654
|
-
|
|
8821
|
+
aF.push(...(runDetector(_detectorErrors,'<project>','scanConventionDeviationProject',()=>scanConventionDeviationProject(fc))||[]));
|
|
8822
|
+
aF.push(...(runDetector(_detectorErrors,'<project>','scanApiBrokenAuthz',()=>scanApiBrokenAuthz(aR))||[]));
|
|
8655
8823
|
// R22 (PRD §5): cross-service edges inferred from code (client call → matched route).
|
|
8656
|
-
|
|
8824
|
+
aF.push(...(runDetector(_detectorErrors,'<project>','scanCrossService',()=>scanCrossService(aR,fc))||[]));
|
|
8657
8825
|
// R21 (PRD §5): RBAC role-tier consistency over the route inventory.
|
|
8658
|
-
|
|
8826
|
+
aF.push(...(runDetector(_detectorErrors,'<project>','scanRbacConsistency',()=>scanRbacConsistency(aR,fc))||[]));
|
|
8659
8827
|
setProgress({current:i,total:files.length,file:"Reachability + guards...",phase:"Linking"});annotateReachability(aF,aR,callGraph,fc);aF.forEach(f=>detectGuardsForFinding(f,fc));
|
|
8660
8828
|
setProgress({current:i,total:files.length,file:"Inferring sanitizers...",phase:"Linking"});const learned=inferSanitizers(fc);applyLearnedSanitizers(aF,learned,fc);
|
|
8661
8829
|
setProgress({current:i,total:files.length,file:"Sanitizer effectiveness...",phase:"Linking"});applySanitizerEffectiveness(aF);
|
|
8662
8830
|
setProgress({current:i,total:files.length,file:"Attack chains...",phase:"Linking"});const chains=crossFindingChain(aF);aF.push(...chains);
|
|
8663
|
-
setProgress({current:i,total:files.length,file:"Config file cross-ref...",phase:"Linking"});aLogic.push(...scanConfigFiles(fc));
|
|
8831
|
+
setProgress({current:i,total:files.length,file:"Config file cross-ref...",phase:"Linking"});aLogic.push(...(runDetector(_detectorErrors,'<project>','scanConfigFiles',()=>scanConfigFiles(fc))||[]));
|
|
8664
8832
|
setProgress({current:i,total:files.length,file:"OSV vulnerability database...",phase:"SCA"});
|
|
8665
8833
|
const allFileContents={...fc, ...depFileContents};
|
|
8666
8834
|
// PRD F11.4 — malicious install hooks, scanned where package.json actually
|
|
@@ -8675,7 +8843,7 @@ function _deterministicFileTimings(timings) {
|
|
|
8675
8843
|
// A manifest detector belongs on the manifest path.
|
|
8676
8844
|
for (const [mp, mc] of Object.entries(depFileContents)) {
|
|
8677
8845
|
if (!/(?:^|\/)package\.json$/i.test(mp)) continue;
|
|
8678
|
-
|
|
8846
|
+
aF.push(...(runDetector(_detectorErrors,mp,'scanInstallScripts',()=>scanInstallScripts(mp, mc))||[]));
|
|
8679
8847
|
}
|
|
8680
8848
|
const components=parseManifests(allFileContents);
|
|
8681
8849
|
// R8 (PRD §5): OS packages from an extracted container image's package DBs
|
|
@@ -8806,8 +8974,8 @@ function _deterministicFileTimings(timings) {
|
|
|
8806
8974
|
}
|
|
8807
8975
|
}
|
|
8808
8976
|
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};});
|
|
8809
|
-
|
|
8810
|
-
|
|
8977
|
+
aF.push(...(runDetector(_detectorErrors,'<project>','scanDbTaintCrossFile',()=>scanDbTaintCrossFile(fc))||[]));
|
|
8978
|
+
aF.push(...(runDetector(_detectorErrors,'<project>','scanStoredPromptInjectionCrossFile',()=>scanStoredPromptInjectionCrossFile(fc))||[]));
|
|
8811
8979
|
// Roadmap #8 — tree-sitter sinks for long-tail languages (opt-in,
|
|
8812
8980
|
// AGENTIC_SECURITY_TREE_SITTER=1; degrades to no-op without the optional dep).
|
|
8813
8981
|
if(process.env.AGENTIC_SECURITY_TREE_SITTER==='1'){try{aF.push(...await scanTreeSitterSinks(fc));}catch(_){}}
|
|
@@ -8894,6 +9062,7 @@ function _deterministicFileTimings(timings) {
|
|
|
8894
9062
|
const _deepInCiAllowed = deepInCi === true || process.env.AGENTIC_SECURITY_DEEP_IN_CI === '1';
|
|
8895
9063
|
const _deepEnabled = _deepRequested && (!_inCi || _deepInCiAllowed);
|
|
8896
9064
|
let _deepCallGraph = null;
|
|
9065
|
+
let _deepFailure = null;
|
|
8897
9066
|
if (_deepEnabled) {
|
|
8898
9067
|
const budgetMs = parseInt(process.env.AGENTIC_SECURITY_DEEP_TIMEOUT_MS || '300000', 10);
|
|
8899
9068
|
const t0 = Date.now();
|
|
@@ -8931,7 +9100,10 @@ function _deterministicFileTimings(timings) {
|
|
|
8931
9100
|
aF.push(...irFindings);
|
|
8932
9101
|
} catch (e) {
|
|
8933
9102
|
// Deep mode is best-effort. A parser blowup in one file shouldn't kill
|
|
8934
|
-
// the scan — fall back to the pattern-only result.
|
|
9103
|
+
// the scan — fall back to the pattern-only result. Recorded (not just
|
|
9104
|
+
// swallowed) so scanHealth (FR-206) can report the downgrade instead of
|
|
9105
|
+
// looking identical to a scan where deep mode simply wasn't requested.
|
|
9106
|
+
_deepFailure = String((e && e.message) || e);
|
|
8935
9107
|
}
|
|
8936
9108
|
} else if (_deepRequested && _inCi) {
|
|
8937
9109
|
// Operator asked for deep but we're in CI — emit a non-blocking notice
|
|
@@ -8945,6 +9117,23 @@ function _deterministicFileTimings(timings) {
|
|
|
8945
9117
|
confidence: 1.0,
|
|
8946
9118
|
});
|
|
8947
9119
|
}
|
|
9120
|
+
// FR-205/FR-206: make the deep-analysis decision explicit in machine
|
|
9121
|
+
// output, not just as an occasional info-severity finding. Covers the case
|
|
9122
|
+
// the finding-based notice above does not: deep mode never explicitly
|
|
9123
|
+
// requested at all (the common default-unset-in-CI case), which previously
|
|
9124
|
+
// produced no signal of any kind that assurance had quietly narrowed.
|
|
9125
|
+
const _deepStatus = {
|
|
9126
|
+
requested: _deepRequested,
|
|
9127
|
+
enabled: _deepEnabled,
|
|
9128
|
+
inCi: _inCi,
|
|
9129
|
+
ciOverrideAllowed: _deepInCiAllowed,
|
|
9130
|
+
reason: _deepEnabled
|
|
9131
|
+
? null
|
|
9132
|
+
: (_deepRequested
|
|
9133
|
+
? (_inCi ? 'requested, but running in CI without AGENTIC_SECURITY_DEEP_IN_CI=1' : 'unknown')
|
|
9134
|
+
: (_inCi ? 'not requested (deep analysis defaults to off in CI)' : 'not requested')),
|
|
9135
|
+
failure: _deepFailure,
|
|
9136
|
+
};
|
|
8948
9137
|
// Java SCA enrichment: use deep-mode IR call graph to improve Java function reachability
|
|
8949
9138
|
if (_deepCallGraph) {
|
|
8950
9139
|
try {
|
|
@@ -9090,16 +9279,18 @@ function _deterministicFileTimings(timings) {
|
|
|
9090
9279
|
let _executionProofSummary = null, _vulnHistory = null;
|
|
9091
9280
|
let _logicClaims = null;
|
|
9092
9281
|
const _annotatorErrors = [];
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
_runAnnotator(
|
|
9102
|
-
_runAnnotator(
|
|
9282
|
+
// FR-106 (assurance-hardening PRD): Promise-aware, explicitly awaited at
|
|
9283
|
+
// every one of its ~51 call sites below (previously a sync `try{return
|
|
9284
|
+
// fn()}` let an async callback's rejection escape as an unhandled
|
|
9285
|
+
// rejection, and every caller fired-and-forgot regardless). Logic lives in
|
|
9286
|
+
// pipeline/annotator-runner.js, fault-injection tested directly there
|
|
9287
|
+
// since this project's own annotators are deliberately built never to
|
|
9288
|
+
// throw (posture/CLAUDE.md's "no throwing" convention) — this closure just
|
|
9289
|
+
// supplies the local _annotatorErrors array.
|
|
9290
|
+
const _runAnnotator = (phase, fn) => runAnnotatorAsync(_annotatorErrors, phase, fn);
|
|
9291
|
+
await _runAnnotator('annotateStableIds', () => annotateStableIds(finalFindings));
|
|
9292
|
+
await _runAnnotator("clusterByRootCause", () => { finalFindings = clusterByRootCause(finalFindings); });
|
|
9293
|
+
await _runAnnotator("demoteUnreachable", () => {
|
|
9103
9294
|
demoteUnreachable(finalFindings, { routes: aR });
|
|
9104
9295
|
// `type: 'vulnerable_dep'` findings live in supplyChain, not finalFindings
|
|
9105
9296
|
// (src/sca/CLAUDE.md) — demoteUnreachable's SCA-tier branch needs this
|
|
@@ -9108,14 +9299,14 @@ function _deterministicFileTimings(timings) {
|
|
|
9108
9299
|
});
|
|
9109
9300
|
// Premortem #8: backfill parser/family BEFORE confidence and calibration,
|
|
9110
9301
|
// because both consume those fields and silently no-op when they're null.
|
|
9111
|
-
_runAnnotator("backfillFindingDefaults", () => { backfillFindingDefaults(finalFindings); });
|
|
9112
|
-
_runAnnotator("annotateConfidence", () => { annotateConfidence(finalFindings); });
|
|
9302
|
+
await _runAnnotator("backfillFindingDefaults", () => { backfillFindingDefaults(finalFindings); });
|
|
9303
|
+
await _runAnnotator("annotateConfidence", () => { annotateConfidence(finalFindings); });
|
|
9113
9304
|
// Phase-1 next-gen P1.3 (FR-UX-1, FR-UX-2): Brier-calibrated probability +
|
|
9114
9305
|
// 95% Wilson CI from per-family historical TP/FP. Falls back to null with
|
|
9115
9306
|
// an explicit `calibration_reason` when N is below the calibration floor.
|
|
9116
|
-
_runAnnotator("annotateCalibratedConfidence", () => { annotateCalibratedConfidence(finalFindings, { scanRoot }); });
|
|
9307
|
+
await _runAnnotator("annotateCalibratedConfidence", () => { annotateCalibratedConfidence(finalFindings, { scanRoot }); });
|
|
9117
9308
|
const _projectCtx = (() => { try { return detectProjectContext(fc, aR); } catch { return {}; } })();
|
|
9118
|
-
_runAnnotator("annotateExploitability", () => { annotateExploitability(finalFindings, _projectCtx); });
|
|
9309
|
+
await _runAnnotator("annotateExploitability", () => { annotateExploitability(finalFindings, _projectCtx); });
|
|
9119
9310
|
// Roadmap #6 — proof-gate precision pass. Runs AFTER confidence +
|
|
9120
9311
|
// exploitability so it can demote their tiers, and BEFORE mitigation /
|
|
9121
9312
|
// composite-risk so the demotion flows into the canonical ranking. Default
|
|
@@ -9155,8 +9346,8 @@ function _deterministicFileTimings(timings) {
|
|
|
9155
9346
|
if (f.stableId) unsanitizersOnPath[f.stableId] = undo;
|
|
9156
9347
|
}
|
|
9157
9348
|
}
|
|
9158
|
-
_runAnnotator("applySanitizerGate", () => { applySanitizerGate(finalFindings, { sanitizersOnPath, unsanitizersOnPath }); });
|
|
9159
|
-
_runAnnotator("annotateProofGate", () => { annotateProofGate(finalFindings); });
|
|
9349
|
+
await _runAnnotator("applySanitizerGate", () => { applySanitizerGate(finalFindings, { sanitizersOnPath, unsanitizersOnPath }); });
|
|
9350
|
+
await _runAnnotator("annotateProofGate", () => { annotateProofGate(finalFindings); });
|
|
9160
9351
|
}
|
|
9161
9352
|
// Addition #1 — default falsification pass. Actively tries to DISPROVE each
|
|
9162
9353
|
// taint-style finding by locating a context-matched control on the path, and
|
|
@@ -9166,29 +9357,45 @@ function _deterministicFileTimings(timings) {
|
|
|
9166
9357
|
// default; the LLM tier is only wired when an endpoint is configured. Opt out
|
|
9167
9358
|
// with AGENTIC_SECURITY_NO_FALSIFICATION=1.
|
|
9168
9359
|
if (process.env.AGENTIC_SECURITY_NO_FALSIFICATION !== '1') {
|
|
9169
|
-
_runAnnotator("annotateFalsification", () => { annotateFalsification(finalFindings, fc); });
|
|
9360
|
+
await _runAnnotator("annotateFalsification", () => { annotateFalsification(finalFindings, fc); });
|
|
9170
9361
|
}
|
|
9171
9362
|
// Addition #5 — capability-based model routing. Stamp each finding with the
|
|
9172
9363
|
// model tier a cost-sensitive fixer/triager/PoC subagent should be dispatched
|
|
9173
9364
|
// on for THIS vuln class (crypto/auth/critical → strongest; injection → mid;
|
|
9174
9365
|
// low-sev hardening → cheapest). Advisory metadata consumed at dispatch time.
|
|
9175
|
-
_runAnnotator("annotateDispatchModel", () => {
|
|
9366
|
+
await _runAnnotator("annotateDispatchModel", () => {
|
|
9176
9367
|
for (const f of finalFindings) { try { f.dispatchModel = routeModelForFinding(f).model; } catch { /* advisory only */ } }
|
|
9177
9368
|
});
|
|
9178
9369
|
// v3 next-gen: production-aware context ingest (Pillar 9). Must run BEFORE
|
|
9179
9370
|
// the mitigation composite, persona prioritization, and final why-fired
|
|
9180
9371
|
// record so those see the demotion signals.
|
|
9181
|
-
_runAnnotator("annotateWafMitigation", () => { annotateWafMitigation(finalFindings, scanRoot); });
|
|
9182
|
-
_runAnnotator("annotateAuthMitigation", () => { annotateAuthMitigation(finalFindings, scanRoot); });
|
|
9183
|
-
_runAnnotator("annotateNetworkMitigation", () => { annotateNetworkMitigation(finalFindings, scanRoot); });
|
|
9184
|
-
_runAnnotator("annotateTelemetry", () => { annotateTelemetry(finalFindings, scanRoot); });
|
|
9185
|
-
_runAnnotator("annotateFeatureFlagGating", () => { annotateFeatureFlagGating(finalFindings, fc, { scanRoot }); });
|
|
9372
|
+
await _runAnnotator("annotateWafMitigation", () => { annotateWafMitigation(finalFindings, scanRoot); });
|
|
9373
|
+
await _runAnnotator("annotateAuthMitigation", () => { annotateAuthMitigation(finalFindings, scanRoot); });
|
|
9374
|
+
await _runAnnotator("annotateNetworkMitigation", () => { annotateNetworkMitigation(finalFindings, scanRoot); });
|
|
9375
|
+
await _runAnnotator("annotateTelemetry", () => { annotateTelemetry(finalFindings, scanRoot); });
|
|
9376
|
+
await _runAnnotator("annotateFeatureFlagGating", () => { annotateFeatureFlagGating(finalFindings, fc, { scanRoot }); });
|
|
9186
9377
|
// v3 next-gen: composite mitigation verdict consumes every prod signal above.
|
|
9187
|
-
_runAnnotator("annotateMitigationComposite", () => { annotateMitigationComposite(finalFindings); });
|
|
9378
|
+
await _runAnnotator("annotateMitigationComposite", () => { annotateMitigationComposite(finalFindings); });
|
|
9188
9379
|
// Composite risk score (0..100 derived ordinal). Must run AFTER mitigation
|
|
9189
9380
|
// composite + exploitability + toxicityScore so it sees the final values.
|
|
9190
9381
|
// Used by agents and UI as the canonical sort key for "which finding first."
|
|
9191
|
-
_runAnnotator("annotateCompositeRisk", () => { annotateCompositeRisk(finalFindings); });
|
|
9382
|
+
await _runAnnotator("annotateCompositeRisk", () => { annotateCompositeRisk(finalFindings); });
|
|
9383
|
+
|
|
9384
|
+
// FR-405 (assurance-hardening PRD): null means "privacy analysis never
|
|
9385
|
+
// ran at all" (AGENTIC_SECURITY_NO_PRIVACY=1, or the annotator threw
|
|
9386
|
+
// before setting this) — treated the same as false by the gate below,
|
|
9387
|
+
// since neither case has real IR-backed evidence to offer. Declared here,
|
|
9388
|
+
// at function scope, because the annotatePrivacyTaint closure that
|
|
9389
|
+
// assigns to it (inside the AGENTIC_SECURITY_NO_INTEGRATION block below)
|
|
9390
|
+
// runs and exits before that block closes — a block-scoped `let` inside
|
|
9391
|
+
// that if-statement would be unreachable by the later assessPrivacyFramework
|
|
9392
|
+
// call and the final return, both of which are outside the block.
|
|
9393
|
+
let _privacyIrBacked = null;
|
|
9394
|
+
// FR-402: which taxonomy version actually classified this scan's fields —
|
|
9395
|
+
// same scoping constraint as _privacyIrBacked directly above (D-0011):
|
|
9396
|
+
// must be declared before the AGENTIC_SECURITY_NO_INTEGRATION block opens,
|
|
9397
|
+
// not inside it.
|
|
9398
|
+
let _privacyTaxonomyVersion = null;
|
|
9192
9399
|
|
|
9193
9400
|
// ── World-class integration block ─────────────────────────────────────
|
|
9194
9401
|
// Each annotator is opt-in via env var and try/catch wrapped. They run
|
|
@@ -9198,46 +9405,89 @@ function _deterministicFileTimings(timings) {
|
|
|
9198
9405
|
// Cross-service taint annotation reads .agentic-security/services.yml
|
|
9199
9406
|
// and bumps severity on cross-service-reachable findings.
|
|
9200
9407
|
if (process.env.AGENTIC_SECURITY_NO_CROSS_SERVICE !== '1') {
|
|
9201
|
-
_runAnnotator("runCrossServiceTaint", () => { runCrossServiceTaint(scanRoot, finalFindings); });
|
|
9408
|
+
await _runAnnotator("runCrossServiceTaint", () => { runCrossServiceTaint(scanRoot, finalFindings); });
|
|
9202
9409
|
}
|
|
9203
9410
|
// Runtime correlation: demotes findings whose paths were unobserved
|
|
9204
9411
|
// in production eBPF traces (when a trace file is present).
|
|
9205
9412
|
if (process.env.AGENTIC_SECURITY_NO_RUNTIME_CORRELATION !== '1') {
|
|
9206
|
-
_runAnnotator("annotateRuntimeCorrelation", async () => { await annotateRuntimeCorrelation(scanRoot, finalFindings); });
|
|
9413
|
+
await _runAnnotator("annotateRuntimeCorrelation", async () => { await annotateRuntimeCorrelation(scanRoot, finalFindings); });
|
|
9207
9414
|
}
|
|
9208
9415
|
// Triage learning: applies per-(project, family, file-glob) calibration
|
|
9209
9416
|
// from prior wont-fix / false-positive decisions.
|
|
9210
9417
|
if (process.env.AGENTIC_SECURITY_NO_TRIAGE_LEARNING !== '1') {
|
|
9211
|
-
_runAnnotator("applyLearnedCalibration", () => { applyLearnedCalibration(scanRoot, finalFindings); });
|
|
9418
|
+
await _runAnnotator("applyLearnedCalibration", () => { applyLearnedCalibration(scanRoot, finalFindings); });
|
|
9212
9419
|
}
|
|
9213
9420
|
// Formal verification: CBMC for C/C++, MIRI for Rust. Opt-in via
|
|
9214
9421
|
// AGENTIC_SECURITY_FORMAL=1 (off by default — requires external tools).
|
|
9215
9422
|
if (process.env.AGENTIC_SECURITY_FORMAL === '1') {
|
|
9216
|
-
_runAnnotator("annotateFormalVerification", async () => { await annotateFormalVerification(finalFindings, fc, {}); });
|
|
9423
|
+
await _runAnnotator("annotateFormalVerification", async () => { await annotateFormalVerification(finalFindings, fc, {}); });
|
|
9217
9424
|
}
|
|
9218
9425
|
// SMT path feasibility: Z3-backed proof of reachability. Opt-in via
|
|
9219
9426
|
// AGENTIC_SECURITY_SMT_FEASIBILITY=1.
|
|
9220
9427
|
if (process.env.AGENTIC_SECURITY_SMT_FEASIBILITY === '1') {
|
|
9221
|
-
_runAnnotator("annotatePathFeasibility", async () => { await annotatePathFeasibility(finalFindings, {}); });
|
|
9428
|
+
await _runAnnotator("annotatePathFeasibility", async () => { await annotatePathFeasibility(finalFindings, {}); });
|
|
9222
9429
|
}
|
|
9223
9430
|
// Privacy / PII taint: emits pii-exposure findings + DPIA artifact.
|
|
9431
|
+
// FR-405 (assurance-hardening PRD): _privacyIrBacked is declared at
|
|
9432
|
+
// function scope above the AGENTIC_SECURITY_NO_INTEGRATION block, not
|
|
9433
|
+
// here — this closure runs and exits before that block's closing brace,
|
|
9434
|
+
// so a block-scoped declaration would be unreachable from the later
|
|
9435
|
+
// assessPrivacyFramework call and the final return.
|
|
9224
9436
|
if (process.env.AGENTIC_SECURITY_NO_PRIVACY !== '1') {
|
|
9225
|
-
_runAnnotator("annotatePrivacyTaint", () => {
|
|
9226
|
-
//
|
|
9227
|
-
//
|
|
9228
|
-
//
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9437
|
+
await _runAnnotator("annotatePrivacyTaint", () => {
|
|
9438
|
+
// FR-401 (assurance-hardening PRD, A-06): decls/calls used to be
|
|
9439
|
+
// hardcoded empty for every file (the actual root cause, per
|
|
9440
|
+
// decisions.md D-0003, was a missing adapter reconciling
|
|
9441
|
+
// privacy-taint.js's flat per-file shape with the real Layer-1 IR's
|
|
9442
|
+
// per-function/CFG shape — not a missing capability in the IR or
|
|
9443
|
+
// taint engine themselves; see privacy/ir-adapter.js).
|
|
9444
|
+
//
|
|
9445
|
+
// Reuse _sharedIR when deep mode already built it (free — no
|
|
9446
|
+
// second parse pass). When deep mode is OFF (the common default
|
|
9447
|
+
// path), do NOT force a fresh IR build here: buildProjectIR parses
|
|
9448
|
+
// every file and is exactly the cost deep mode is opt-in for
|
|
9449
|
+
// (NFR: "no more than 15% ... overhead ... excluding newly enabled
|
|
9450
|
+
// deep analysis"). In that case privacy analysis still runs, but
|
|
9451
|
+
// honestly marked as not IR-backed (irBacked:false below) rather
|
|
9452
|
+
// than silently claiming the same coverage as the deep-mode path —
|
|
9453
|
+
// this is the signal FR-405's "missing capability -> not_assessed,
|
|
9454
|
+
// never satisfied" contract needs to key off, not yet wired into
|
|
9455
|
+
// privacy-framework.js's own bucketing in this cycle (separate,
|
|
9456
|
+
// later FR-405 work).
|
|
9457
|
+
const irBacked = !!(_sharedIR && _sharedIR.perFile);
|
|
9458
|
+
const adaptedIR = irBacked
|
|
9459
|
+
? adaptIRForPrivacyTaint(_sharedIR.perFile, fc, storedRegistry)
|
|
9460
|
+
: new Map(Object.entries(fc || {})
|
|
9461
|
+
.filter(([, content]) => typeof content === 'string')
|
|
9462
|
+
.map(([fp, content]) => [fp, { _content: content, decls: [], calls: [] }]));
|
|
9463
|
+
// FR-402: pass scanRoot so an operator's .agentic-security/
|
|
9464
|
+
// privacy-taxonomy.json (if any) is actually loaded on a real scan.
|
|
9465
|
+
const r = annotatePrivacyTaint(adaptedIR, { scanRoot });
|
|
9466
|
+
r.irBacked = irBacked;
|
|
9467
|
+
_privacyIrBacked = irBacked;
|
|
9468
|
+
_privacyTaxonomyVersion = r.taxonomyVersion || null;
|
|
9469
|
+
// FR-102 (assurance-hardening PRD): route through the registered-
|
|
9470
|
+
// producer collector rather than a bare push. r is already computed
|
|
9471
|
+
// above (this call also drives the DPIA artifact below), so the
|
|
9472
|
+
// thunk just hands back what's already there.
|
|
9473
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'privacy-taint', () => (r && Array.isArray(r.findings)) ? r.findings : []);
|
|
9236
9474
|
// Persist the DPIA scaffold for compliance review.
|
|
9237
9475
|
if (r && r.piiFields) {
|
|
9238
9476
|
try {
|
|
9239
|
-
|
|
9477
|
+
// FR-407: governance fields are loaded once and threaded into
|
|
9478
|
+
// both the DPIA (per-class subsection) and the RoPA artifact
|
|
9479
|
+
// (the full register) so an operator only maintains one config
|
|
9480
|
+
// file for both.
|
|
9481
|
+
const governanceConfig = loadPrivacyGovernanceConfig(scanRoot);
|
|
9482
|
+
const dpia = emitDpiaArtifact(r.piiFields, r.findings || [], { policyExemptions: r.policyExemptions || [], governanceConfig });
|
|
9240
9483
|
_safeWriteState(_statePath(scanRoot, 'dpia.md'), dpia);
|
|
9484
|
+
const ropa = emitRopaArtifact(r.piiFields, governanceConfig);
|
|
9485
|
+
_safeWriteState(_statePath(scanRoot, 'ropa.md'), ropa);
|
|
9486
|
+
// FR-406: code-derived data inventory + flow graph, built from
|
|
9487
|
+
// the SAME findings/exemptions already computed above.
|
|
9488
|
+
const inventory = buildDataInventory(r.piiFields, r.findings || [], r.policyExemptions || []);
|
|
9489
|
+
_safeWriteState(_statePath(scanRoot, 'data-inventory.json'), emitDataInventoryArtifact(inventory));
|
|
9490
|
+
_safeWriteState(_statePath(scanRoot, 'data-flow-graph.md'), emitDataFlowGraph(inventory));
|
|
9241
9491
|
} catch (_) {}
|
|
9242
9492
|
}
|
|
9243
9493
|
});
|
|
@@ -9246,80 +9496,80 @@ function _deterministicFileTimings(timings) {
|
|
|
9246
9496
|
// ATLAS, D3FEND, kill-chain stage, and CAPEC IDs so downstream SIEM /
|
|
9247
9497
|
// SOAR systems can correlate with existing detection rules.
|
|
9248
9498
|
if (process.env.AGENTIC_SECURITY_NO_ATTACK_TAX !== '1') {
|
|
9249
|
-
_runAnnotator("annotateAttackTaxonomy", () => { annotateAttackTaxonomy(finalFindings); });
|
|
9499
|
+
await _runAnnotator("annotateAttackTaxonomy", () => { annotateAttackTaxonomy(finalFindings); });
|
|
9250
9500
|
}
|
|
9251
9501
|
// Triage memory — demote findings whose (family, dir) bucket was
|
|
9252
9502
|
// previously marked wont-fix or false-positive in this project.
|
|
9253
9503
|
if (process.env.AGENTIC_SECURITY_NO_TRIAGE_MEMORY !== '1') {
|
|
9254
|
-
_runAnnotator("suppressByPastDecisions", () => { suppressByPastDecisions(scanRoot, finalFindings); });
|
|
9504
|
+
await _runAnnotator("suppressByPastDecisions", () => { suppressByPastDecisions(scanRoot, finalFindings); });
|
|
9255
9505
|
}
|
|
9256
9506
|
// Intent-aware FP suppression — demote findings on files marked as
|
|
9257
9507
|
// intentionally vulnerable (sandbox/CTF/tutorial/example/etc.).
|
|
9258
9508
|
if (process.env.AGENTIC_SECURITY_NO_INTENT_CTX !== '1') {
|
|
9259
|
-
_runAnnotator("suppressByIntent", () => { suppressByIntent(scanRoot, finalFindings); });
|
|
9509
|
+
await _runAnnotator("suppressByIntent", () => { suppressByIntent(scanRoot, finalFindings); });
|
|
9260
9510
|
}
|
|
9261
9511
|
// Git history — stamp each finding with introducedBy / introducedIn /
|
|
9262
9512
|
// originatingPrompt by running `git blame` on the finding's line.
|
|
9263
9513
|
if (process.env.AGENTIC_SECURITY_NO_GIT_HISTORY !== '1') {
|
|
9264
|
-
_runAnnotator("annotateGitHistory", () => { annotateGitHistory(scanRoot, finalFindings); });
|
|
9514
|
+
await _runAnnotator("annotateGitHistory", () => { annotateGitHistory(scanRoot, finalFindings); });
|
|
9265
9515
|
}
|
|
9266
9516
|
// Threat-model grounding — bump severity on crown-jewels, demote
|
|
9267
9517
|
// out-of-scope, tag compliance regimes, stamp attacker profile.
|
|
9268
9518
|
if (process.env.AGENTIC_SECURITY_NO_THREAT_MODEL_GROUNDING !== '1') {
|
|
9269
|
-
_runAnnotator("applyThreatModel", () => { applyThreatModel(scanRoot, finalFindings); });
|
|
9519
|
+
await _runAnnotator("applyThreatModel", () => { applyThreatModel(scanRoot, finalFindings); });
|
|
9270
9520
|
}
|
|
9271
9521
|
// Cross-repo pattern propagation — surface sibling-repo fixes and
|
|
9272
9522
|
// triage decisions for the same family from this developer's history.
|
|
9273
9523
|
if (process.env.AGENTIC_SECURITY_NO_CROSS_REPO !== '1') {
|
|
9274
|
-
_runAnnotator("annotateCrossRepoSignals", () => { annotateCrossRepoSignals(scanRoot, finalFindings); });
|
|
9524
|
+
await _runAnnotator("annotateCrossRepoSignals", () => { annotateCrossRepoSignals(scanRoot, finalFindings); });
|
|
9275
9525
|
}
|
|
9276
9526
|
// Risk-in-dollars — combine EPSS + crown-jewel + reachability into an
|
|
9277
9527
|
// expected-value-of-exploitation USD figure per finding.
|
|
9278
9528
|
if (process.env.AGENTIC_SECURITY_NO_RISK_DOLLARS !== '1') {
|
|
9279
|
-
_runAnnotator("annotateRiskDollars", () => { annotateRiskDollars(scanRoot, finalFindings); });
|
|
9529
|
+
await _runAnnotator("annotateRiskDollars", () => { annotateRiskDollars(scanRoot, finalFindings); });
|
|
9280
9530
|
}
|
|
9281
9531
|
// Time-to-fix — estimate engineering hours per finding.
|
|
9282
9532
|
if (process.env.AGENTIC_SECURITY_NO_TIME_TO_FIX !== '1') {
|
|
9283
|
-
_runAnnotator("annotateTimeToFix", () => { annotateTimeToFix(scanRoot, finalFindings); });
|
|
9533
|
+
await _runAnnotator("annotateTimeToFix", () => { annotateTimeToFix(scanRoot, finalFindings); });
|
|
9284
9534
|
}
|
|
9285
9535
|
}
|
|
9286
9536
|
// v3 next-gen: crown-jewel mapping (FR-PROD-5) — score each file/finding by
|
|
9287
9537
|
// business impact. Must run before persona prioritization (which uses it).
|
|
9288
|
-
_runAnnotator("annotateCrownJewelScores", () => { annotateCrownJewelScores(finalFindings, fc); });
|
|
9538
|
+
await _runAnnotator("annotateCrownJewelScores", () => { annotateCrownJewelScores(finalFindings, fc); });
|
|
9289
9539
|
// v3 next-gen: clone clusters (FR-SEM-8) + emit clone-outlier infos.
|
|
9290
|
-
_runAnnotator("annotateCloneClusters", () => { annotateCloneClusters(finalFindings); });
|
|
9540
|
+
await _runAnnotator("annotateCloneClusters", () => { annotateCloneClusters(finalFindings); });
|
|
9291
9541
|
try {
|
|
9292
9542
|
const outliers = findCloneOutliers(finalFindings);
|
|
9293
9543
|
if (outliers && outliers.length) finalFindings.push(...outliers);
|
|
9294
9544
|
} catch(_) {}
|
|
9295
9545
|
// v3 next-gen: AI-generated-code fingerprint (FR-LEARN-10). Property bag tag.
|
|
9296
|
-
_runAnnotator("annotateAiProvenance", () => { annotateAiProvenance(finalFindings, fc); });
|
|
9546
|
+
await _runAnnotator("annotateAiProvenance", () => { annotateAiProvenance(finalFindings, fc); });
|
|
9297
9547
|
// v3 next-gen: whole-program type narrowing (FR-SEM-10) — heuristic
|
|
9298
9548
|
// confidence dampener on findings rooted in functions whose callers all
|
|
9299
9549
|
// pass narrowly-typed values.
|
|
9300
|
-
_runAnnotator("annotateTypeNarrowing", () => { annotateTypeNarrowing(finalFindings, fc); });
|
|
9550
|
+
await _runAnnotator("annotateTypeNarrowing", () => { annotateTypeNarrowing(finalFindings, fc); });
|
|
9301
9551
|
// v3 next-gen: STRIDE classification (FR-LOGIC-10).
|
|
9302
|
-
_runAnnotator("annotateStrideCategory", () => { annotateStrideCategory(finalFindings); });
|
|
9552
|
+
await _runAnnotator("annotateStrideCategory", () => { annotateStrideCategory(finalFindings); });
|
|
9303
9553
|
// v3 next-gen: per-attacker-persona score matrix (FR-ADV-2). Must run AFTER
|
|
9304
9554
|
// crown-jewels + mitigation composite so it sees those signals.
|
|
9305
|
-
_runAnnotator("annotatePersonaScores", () => { annotatePersonaScores(finalFindings); });
|
|
9555
|
+
await _runAnnotator("annotatePersonaScores", () => { annotatePersonaScores(finalFindings); });
|
|
9306
9556
|
// v3 next-gen: SCA reverse-blast-radius enrichment (FR-ADV-5). Annotates
|
|
9307
9557
|
// SCA findings (package-name-keyed) — must run against supplyChain, not
|
|
9308
9558
|
// finalFindings (SAST), which has no package-name field at all.
|
|
9309
|
-
_runAnnotator("annotateScaReverseBlast", () => { annotateScaReverseBlast(supplyChain, fc); });
|
|
9559
|
+
await _runAnnotator("annotateScaReverseBlast", () => { annotateScaReverseBlast(supplyChain, fc); });
|
|
9310
9560
|
// v3 next-gen: bug-bounty payout prediction (FR-ADV-3). Composes with the
|
|
9311
9561
|
// mitigation composite — gated/unreachable findings get the bounty scaled
|
|
9312
9562
|
// down rather than zeroed.
|
|
9313
|
-
_runAnnotator("annotateBountyPrediction", () => { annotateBountyPrediction(finalFindings); });
|
|
9563
|
+
await _runAnnotator("annotateBountyPrediction", () => { annotateBountyPrediction(finalFindings); });
|
|
9314
9564
|
// v3 next-gen: attack-playbook annotation (FR-ADV-4). Only for high+ findings.
|
|
9315
|
-
_runAnnotator("annotateAttackPlaybooks", () => { annotateAttackPlaybooks(finalFindings); });
|
|
9565
|
+
await _runAnnotator("annotateAttackPlaybooks", () => { annotateAttackPlaybooks(finalFindings); });
|
|
9316
9566
|
// Phase-1 next-gen P1.1 (FR-VER-2): attach a runnable PoC to each finding
|
|
9317
9567
|
// when a CWE template covers it. Findings without coverage get f.poc=null.
|
|
9318
9568
|
// Premortem #12: pass fileContents so PoC param-key inference can re-read
|
|
9319
9569
|
// the actual handler line when detector snippets are misattributed.
|
|
9320
|
-
_runAnnotator("annotatePocs", () => { annotatePocs(finalFindings, { routes: aR, fileContents: fc }); });
|
|
9570
|
+
await _runAnnotator("annotatePocs", () => { annotatePocs(finalFindings, { routes: aR, fileContents: fc }); });
|
|
9321
9571
|
// FR-VER-3: regression-test generator (builds on the PoC artifact).
|
|
9322
|
-
_runAnnotator("annotateRegressionTests", () => { annotateRegressionTests(finalFindings); });
|
|
9572
|
+
await _runAnnotator("annotateRegressionTests", () => { annotateRegressionTests(finalFindings); });
|
|
9323
9573
|
// R2 — execution proof. Synthesizes a SANDBOX-RUNNABLE PoC (the HTTP PoCs
|
|
9324
9574
|
// above need a live server, so they can never be executed by the prover)
|
|
9325
9575
|
// and lets R1's sandbox decide the tier. Opt-in via AGENTIC_SECURITY_PROVE=1
|
|
@@ -9332,7 +9582,7 @@ function _deterministicFileTimings(timings) {
|
|
|
9332
9582
|
// Never a finding and never a severity change: those bugs are fixed, and a
|
|
9333
9583
|
// historical fix is not evidence of a present defect. Opt-in because it
|
|
9334
9584
|
// shells out to git over up to 500 commits.
|
|
9335
|
-
_runAnnotator('annotateHistoricalRisk', () => {
|
|
9585
|
+
await _runAnnotator('annotateHistoricalRisk', () => {
|
|
9336
9586
|
if (process.env.AGENTIC_SECURITY_ARCHAEOLOGY !== '1' || !scanRoot) return;
|
|
9337
9587
|
_vulnHistory = mineVulnHistory(scanRoot);
|
|
9338
9588
|
annotateHistoricalRisk(finalFindings, _vulnHistory);
|
|
@@ -9346,65 +9596,38 @@ function _deterministicFileTimings(timings) {
|
|
|
9346
9596
|
// verifier verdict — verified-exploit (live PoC ran), verified-by-llm,
|
|
9347
9597
|
// verified-sanitizer-absence, unverified-by-design, or cannot-verify.
|
|
9348
9598
|
// Fail-closed: any error → cannot-verify, never a silent drop.
|
|
9349
|
-
_runAnnotator("annotateVerifierVerdicts", () => { annotateVerifierVerdicts(finalFindings, { fileContents: fc }); });
|
|
9599
|
+
await _runAnnotator("annotateVerifierVerdicts", () => { annotateVerifierVerdicts(finalFindings, { fileContents: fc }); });
|
|
9350
9600
|
// Cross-language taint (Sentinel-parity FR-DET-3) — five boundary types:
|
|
9351
9601
|
// HTTP/REST via OpenAPI, gRPC via .proto, GraphQL via SDL, SQL/ORM
|
|
9352
9602
|
// round-trip, and IaC → application-code reachability (FR-DET-4).
|
|
9353
9603
|
const _allXlangFiles = { ...fc, ...depFileContents };
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
if (xl && xl.length) finalFindings.push(...xl);
|
|
9365
|
-
} catch(_) {}
|
|
9366
|
-
try {
|
|
9367
|
-
const xl = scanCrossLangOrm(_allXlangFiles, finalFindings);
|
|
9368
|
-
if (xl && xl.length) finalFindings.push(...xl);
|
|
9369
|
-
} catch(_) {}
|
|
9604
|
+
// FR-101/FR-102 (assurance-hardening PRD): each of these 11 producers now
|
|
9605
|
+
// goes through the registered-producer collector (pipeline/producer-
|
|
9606
|
+
// collector.js) instead of a bare try/catch + push — a thrown exception
|
|
9607
|
+
// becomes a diagnostic in _annotatorErrors (previously silently swallowed
|
|
9608
|
+
// by `catch(_) {}` with no trace anywhere) and the collector stamps
|
|
9609
|
+
// producerId provenance on each finding.
|
|
9610
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'cross-lang-openapi', () => scanCrossLangOpenAPI(_allXlangFiles, finalFindings));
|
|
9611
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'cross-lang-grpc', () => scanCrossLangGrpc(_allXlangFiles, finalFindings));
|
|
9612
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'cross-lang-graphql', () => scanCrossLangGraphql(_allXlangFiles, finalFindings));
|
|
9613
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'cross-lang-orm', () => scanCrossLangOrm(_allXlangFiles, finalFindings));
|
|
9370
9614
|
// Phase-1 next-gen P1.5 (FR-XSAT-4): cross-language taint via Kafka, SQS,
|
|
9371
9615
|
// RabbitMQ, Redis streams, and Google Pub/Sub topics.
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
if (xl && xl.length) finalFindings.push(...xl);
|
|
9375
|
-
} catch(_) {}
|
|
9376
|
-
try {
|
|
9377
|
-
const xl = scanIacReachability(_allXlangFiles, finalFindings);
|
|
9378
|
-
if (xl && xl.length) finalFindings.push(...xl);
|
|
9379
|
-
} catch(_) {}
|
|
9616
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'cross-lang-queues', () => scanCrossLangQueues(_allXlangFiles, finalFindings));
|
|
9617
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'iac-reachability', () => scanIacReachability(_allXlangFiles, finalFindings));
|
|
9380
9618
|
// Phase-2.5 next-gen: IAM policy reachability (FR-XSAT-7).
|
|
9381
|
-
|
|
9382
|
-
const ia = scanIamPolicies(_allXlangFiles, finalFindings);
|
|
9383
|
-
if (ia && ia.length) finalFindings.push(...ia);
|
|
9384
|
-
} catch(_) {}
|
|
9619
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'iam-policy', () => scanIamPolicies(_allXlangFiles, finalFindings));
|
|
9385
9620
|
// Phase-2.5 next-gen: container runtime config audit (FR-XSAT-8).
|
|
9386
|
-
|
|
9387
|
-
const cr = scanContainerRuntime(_allXlangFiles);
|
|
9388
|
-
if (cr && cr.length) finalFindings.push(...cr);
|
|
9389
|
-
} catch(_) {}
|
|
9621
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'container-runtime', () => scanContainerRuntime(_allXlangFiles));
|
|
9390
9622
|
// Phase-4 next-gen: business-logic analysis (FR-LOGIC-1, FR-LOGIC-2, FR-LOGIC-7).
|
|
9391
|
-
|
|
9392
|
-
const bl = scanBusinessLogicV2(_allXlangFiles);
|
|
9393
|
-
if (bl && bl.length) finalFindings.push(...bl);
|
|
9394
|
-
} catch(_) {}
|
|
9623
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'business-logic-v2', () => scanBusinessLogicV2(_allXlangFiles));
|
|
9395
9624
|
// v3 next-gen: specification-mining drift detector (FR-LOGIC-8). Emits
|
|
9396
9625
|
// findings for function-name-vs-body mismatches. Low confidence by default;
|
|
9397
9626
|
// active-learning loop tunes per project.
|
|
9398
|
-
|
|
9399
|
-
const sm = scanSpecificationDrift(_allXlangFiles);
|
|
9400
|
-
if (sm && sm.length) finalFindings.push(...sm);
|
|
9401
|
-
} catch(_) {}
|
|
9627
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'specification-drift', () => scanSpecificationDrift(_allXlangFiles));
|
|
9402
9628
|
// v3 next-gen: bounded concurrency-bug detector (FR-SEM-9). Heuristic only;
|
|
9403
9629
|
// catches missed unlocks, fire-and-forget async, and 2-lock deadlock cycles.
|
|
9404
|
-
|
|
9405
|
-
const cc = scanConcurrency(_allXlangFiles);
|
|
9406
|
-
if (cc && cc.length) finalFindings.push(...cc);
|
|
9407
|
-
} catch(_) {}
|
|
9630
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'concurrency', () => scanConcurrency(_allXlangFiles));
|
|
9408
9631
|
// FR-LOGIC-6: LLM-driven flow narration (template fallback when no LLM endpoint).
|
|
9409
9632
|
try { await annotateNarration(finalFindings); }
|
|
9410
9633
|
catch (e) { _annotatorErrors.push({ phase: 'annotateNarration', err: String((e && e.message) || e) }); }
|
|
@@ -9672,13 +9895,13 @@ function _deterministicFileTimings(timings) {
|
|
|
9672
9895
|
// v3 next-gen: capture scan-level reports (counterfactual, threat model,
|
|
9673
9896
|
// trust-boundary diagram, calibration-drift alarms). All best-effort.
|
|
9674
9897
|
let _v3 = {};
|
|
9675
|
-
_runAnnotator("_v3.counterfactual", () => { _v3.counterfactual = runCounterfactual(finalFindings, fc); });
|
|
9676
|
-
_runAnnotator("_v3.threatModel", () => { _v3.threatModel = buildThreatModel(finalFindings, fc); });
|
|
9677
|
-
_runAnnotator("_v3.trustBoundaryDiagram", () => { _v3.trustBoundaryDiagram = buildTrustBoundaryDiagram(finalFindings, fc); });
|
|
9678
|
-
_runAnnotator("_v3.calibrationDrift", () => { _v3.calibrationDrift = computeCalibrationDrift(scanRoot); });
|
|
9898
|
+
await _runAnnotator("_v3.counterfactual", () => { _v3.counterfactual = runCounterfactual(finalFindings, fc); });
|
|
9899
|
+
await _runAnnotator("_v3.threatModel", () => { _v3.threatModel = buildThreatModel(finalFindings, fc); });
|
|
9900
|
+
await _runAnnotator("_v3.trustBoundaryDiagram", () => { _v3.trustBoundaryDiagram = buildTrustBoundaryDiagram(finalFindings, fc); });
|
|
9901
|
+
await _runAnnotator("_v3.calibrationDrift", () => { _v3.calibrationDrift = computeCalibrationDrift(scanRoot); });
|
|
9679
9902
|
// v3 next-gen: why-fired provenance is captured LAST so it reflects the
|
|
9680
9903
|
// final state of each finding after every other annotator has run.
|
|
9681
|
-
_runAnnotator("annotateWhyFired", () => { annotateWhyFired(finalFindings, {}); });
|
|
9904
|
+
await _runAnnotator("annotateWhyFired", () => { annotateWhyFired(finalFindings, {}); });
|
|
9682
9905
|
// SCA-SAST correlation: link SAST findings to SCA vulnerable packages
|
|
9683
9906
|
try{for(const f of finalFindings){if(!f.chain||!f.chain.length)continue;const src=f.chain[0]?.label||'';for(const sc of supplyChain){if(sc.type!=='vulnerable_dep')continue;if(src.includes(sc.name)||f.vuln?.toLowerCase().includes(sc.name)){f.scaCorrelation={osvId:sc.osvId,package:sc.name,version:sc.version,confirmed:true};sc.sastConfirmed=true;break;}}}}catch(_){}
|
|
9684
9907
|
// Multi-sink chain detection: group findings by source variable
|
|
@@ -9787,6 +10010,12 @@ function _deterministicFileTimings(timings) {
|
|
|
9787
10010
|
// filesScanned feeds the vacuous-satisfaction guard: a clean signal
|
|
9788
10011
|
// from a run that read no files is not evidence of compliance.
|
|
9789
10012
|
filesScanned: files.length,
|
|
10013
|
+
// FR-405: feeds the SAME guard, specifically for controls whose
|
|
10014
|
+
// only mapped signal comes from privacy-taint — a non-IR-backed
|
|
10015
|
+
// run (deep mode off, the common default) still emits a "no
|
|
10016
|
+
// findings" pass, which is not real evidence for those controls
|
|
10017
|
+
// even though the scan otherwise examined real files.
|
|
10018
|
+
privacyIrBacked: _privacyIrBacked,
|
|
9790
10019
|
});
|
|
9791
10020
|
if (_privacyFramework) persistPrivacyFramework(scanRoot, _privacyFramework);
|
|
9792
10021
|
if (_privacyFramework && process.env.AGENTIC_SECURITY_PRIVACY_FRAMEWORK === '1') {
|
|
@@ -9812,6 +10041,34 @@ function _deterministicFileTimings(timings) {
|
|
|
9812
10041
|
}
|
|
9813
10042
|
}
|
|
9814
10043
|
|
|
10044
|
+
// FR-103 (assurance-hardening PRD): canonical enrichment completion pass.
|
|
10045
|
+
// This is the LAST point in the function where a finding could still be
|
|
10046
|
+
// missing stableId/confidence/calibration/exploitability because it was
|
|
10047
|
+
// appended by a late producer (see pipeline/enrichment-completion.js's
|
|
10048
|
+
// header for why this is a gap-filling pass rather than relocating ~19
|
|
10049
|
+
// append sites earlier in this function). Must run after every push site
|
|
10050
|
+
// above and before the freeze immediately below.
|
|
10051
|
+
const _projectCtxForCompletion = typeof _projectCtx !== 'undefined' ? _projectCtx : {};
|
|
10052
|
+
completeEnrichment(finalFindings, { scanRoot, projectCtx: _projectCtxForCompletion });
|
|
10053
|
+
|
|
10054
|
+
// FR-104 (assurance-hardening PRD): freeze the finding collection.
|
|
10055
|
+
// Object.freeze on an array blocks push/pop/splice/length changes (this
|
|
10056
|
+
// module and every caller downstream run under ESM strict mode, so a
|
|
10057
|
+
// violation THROWS a TypeError rather than silently no-op'ing) — no
|
|
10058
|
+
// producer may append after this point. This is a SHALLOW freeze: the
|
|
10059
|
+
// remaining read/annotate calls below (countUnmodeledSinkCandidates,
|
|
10060
|
+
// buildEntrypointInventory, annotateRelevance, sweepRootCauses,
|
|
10061
|
+
// proofCoverage) either only read `finalFindings` or, for annotateRelevance,
|
|
10062
|
+
// legitimately mutate FIELDS on individual finding objects — verified by
|
|
10063
|
+
// reading each function's body (none push/splice/pop/shift/unshift onto
|
|
10064
|
+
// the array they're handed). Field mutation is annotation, not production,
|
|
10065
|
+
// and annotateRelevance running this late is an established, deliberate
|
|
10066
|
+
// part of this codebase's architecture (its own header: "runs after every
|
|
10067
|
+
// finding has been appended... so nothing escapes annotation"). What
|
|
10068
|
+
// freezing forecloses is exactly what FR-104 asks for: no MORE findings
|
|
10069
|
+
// can be added after this point.
|
|
10070
|
+
Object.freeze(finalFindings);
|
|
10071
|
+
|
|
9815
10072
|
// Coverage-honesty report (#5 + #6): per-language analysis tier (IR-taint vs
|
|
9816
10073
|
// pattern-only), dense/large/timeout skips, and unmodeled-sink candidates.
|
|
9817
10074
|
let _analysisTier = null, _unmodeledSinks = null;
|
|
@@ -9826,7 +10083,7 @@ function _deterministicFileTimings(timings) {
|
|
|
9826
10083
|
// seen when only N-of-those-candidates were actually analyzed.
|
|
9827
10084
|
// checkpoint.total intentionally keeps files.length — that field means the
|
|
9828
10085
|
// full candidate set for resume bookkeeping, a different, correct meaning.
|
|
9829
|
-
const _scanMeta={filesScanned:Object.keys(fc).length,filesSkipped:_filesSkipped,filesDenseSkipped:_filesDenseSkipped,filesTimedOut:_filesTimedOut,analysisTier:_analysisTier,unmodeledSinkCandidates:_unmodeledSinks,fileTimings:_deterministicFileTimings(_fileTimings),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},checkpoint:{enabled:!!(_ckpt&&_ckpt.enabled),resumed:_ckptResumed,total:files.length}};
|
|
10086
|
+
const _scanMeta={filesScanned:Object.keys(fc).length,filesSkipped:_filesSkipped,filesDenseSkipped:_filesDenseSkipped,filesTimedOut:_filesTimedOut,analysisTier:_analysisTier,unmodeledSinkCandidates:_unmodeledSinks,fileTimings:_deterministicFileTimings(_fileTimings),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},checkpoint:{enabled:!!(_ckpt&&_ckpt.enabled),resumed:_ckptResumed,total:files.length,discarded:!!(_ckpt&&_ckpt.discarded),discardReason:(_ckpt&&_ckpt.discarded)?(_ckpt.reason||null):null,invalidatedFileCount:_ckptInvalidated.length,invalidatedFiles:_ckptInvalidated.slice(0,20),invalidatedFilesTruncated:_ckptInvalidated.length>20}};
|
|
9830
10087
|
// R8: the scan completed, so the checkpoint has been fully consumed — remove
|
|
9831
10088
|
// it. Anything that threw before this point leaves it in place to resume from.
|
|
9832
10089
|
try { closeCheckpoint(_ckpt, { complete: true }); } catch (_) {}
|
|
@@ -9838,7 +10095,7 @@ function _deterministicFileTimings(timings) {
|
|
|
9838
10095
|
// attack surface it is scored against is the complete one. Recall-
|
|
9839
10096
|
// preserving: never removes a finding, never touches severity, and only
|
|
9840
10097
|
// asserts `unreachable` on positive evidence (see posture/relevance.js).
|
|
9841
|
-
_runAnnotator("annotateRelevance", () => {
|
|
10098
|
+
await _runAnnotator("annotateRelevance", () => {
|
|
9842
10099
|
annotateRelevance(finalFindings, {
|
|
9843
10100
|
fileContents: fc,
|
|
9844
10101
|
entrypointInventory: _entrypointInventory,
|
|
@@ -9855,7 +10112,37 @@ function _deterministicFileTimings(timings) {
|
|
|
9855
10112
|
// is the honest shape. Measured on the CVE corpus: 19% / 13% / 68%.
|
|
9856
10113
|
let _proofCoverage = null;
|
|
9857
10114
|
try { _proofCoverage = proofCoverage([...finalFindings, ...aLogic]); } catch { _proofCoverage = null; }
|
|
9858
|
-
|
|
10115
|
+
// FR-203: per-file/per-analyzer coverage ledger, computed from exactly
|
|
10116
|
+
// the signals FR-201 (_detectorErrors) and FR-202 (the _timeout:true
|
|
10117
|
+
// marker finding) already produce -- files actually scanned come from
|
|
10118
|
+
// fc's own keys (skipped-for-size/density files were never added to it).
|
|
10119
|
+
const _timedOutFiles = finalFindings.filter(f => f && f._timeout === true).map(f => f.file);
|
|
10120
|
+
const _coverageLedger = computeCoverageLedger({ files: Object.keys(fc), detectorErrors: _detectorErrors, timedOutFiles: _timedOutFiles });
|
|
10121
|
+
// FR-206 (assurance-hardening PRD, Milestone 0): additive scan-health
|
|
10122
|
+
// summary, computed from signals the engine already collects.
|
|
10123
|
+
// `analyzers` was `null` (see pipeline/scan-health.js's prior comment)
|
|
10124
|
+
// until FR-203's coverage ledger existed to compute it for real.
|
|
10125
|
+
let _scanHealth = computeScanHealth({
|
|
10126
|
+
scanMeta: _scanMeta,
|
|
10127
|
+
annotatorErrors: _annotatorErrors,
|
|
10128
|
+
engineErrors: { cppDataflowParseErrors: _cppDataflowParseErrors.value },
|
|
10129
|
+
deepStatus: _deepStatus,
|
|
10130
|
+
analyzerCoverage: summarizeCoverageForScanHealth(_coverageLedger),
|
|
10131
|
+
});
|
|
10132
|
+
// FR-207: stale vulnerability feeds, calibration data, and compliance
|
|
10133
|
+
// evidence are real assurance gaps, not just findings the feed omits --
|
|
10134
|
+
// surfaced the same way every other scan-health condition is, so
|
|
10135
|
+
// --assurance strict (FR-204) can fail on them. Custom-rule-pack
|
|
10136
|
+
// freshness is the one leg NOT computed here: that mechanism only runs
|
|
10137
|
+
// in bin/agentic-security.js, after this scan object already exists (see
|
|
10138
|
+
// applyFreshness's own header comment in pipeline/scan-health.js).
|
|
10139
|
+
_scanHealth = applyFreshness(_scanHealth, {
|
|
10140
|
+
kev: kevCatalogMeta(),
|
|
10141
|
+
epss: epssLiveMeta(),
|
|
10142
|
+
calibration: calibrationFreshness(),
|
|
10143
|
+
compliance: _complianceReport ? { stale: _complianceReport.summary?.stale || 0 } : null,
|
|
10144
|
+
});
|
|
10145
|
+
return{entrypointInventory:_entrypointInventory,rootCauseSweep:_rootCauseSweep,proofCoverage:_proofCoverage,kevCatalog:kevCatalogMeta(),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,detectorErrors:_detectorErrors,executionProof:_executionProofSummary,logicClaims:_logicClaims,vulnHistory:_vulnHistory,threatModel:_threatModel,privacyFramework:_privacyFramework,privacyIrBacked:_privacyIrBacked,privacyTaxonomyVersion:_privacyTaxonomyVersion,sbomDiff:_sbomDiff,complianceReport:_complianceReport,exploitBundles:_exploitBundles,pqcPlan:_pqcPlan,licenseGraph:_licenseGraph,attributions:_attributions,attackTaxonomy:_taxonomySummary,scanHealth:_scanHealth,coverageLedger:_coverageLedger};}
|
|
9859
10146
|
|
|
9860
10147
|
// Post-aggregation classification: every source becomes "unsafe"|"safe"; every sink becomes "confirmed"|"safe".
|
|
9861
10148
|
// Orphans (no finding linkage) are bucketed by file-local heuristic so the UI shows binary states only.
|