@clear-capabilities/agentic-security-scanner 0.143.0 → 0.145.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 +555 -0
- package/bin/agentic-security.js +770 -49
- package/dist/1.index.js +223 -0
- package/dist/113.index.js +117 -18
- package/dist/144.index.js +163 -0
- package/dist/178.index.js +24 -6
- package/dist/238.index.js +3 -2
- package/dist/265.index.js +191 -0
- package/dist/271.index.js +165 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +187 -52
- package/dist/444.index.js +11 -2
- package/dist/449.index.js +76 -12
- package/dist/526.index.js +117 -18
- package/dist/552.index.js +97 -0
- package/dist/637.index.js +27 -5
- 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 +173 -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 +23 -9
- package/src/compare.js +6 -1
- package/src/dataflow/CLAUDE.md +1 -1
- 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 +1048 -302
- package/src/fix/apply-fix-service.js +404 -0
- package/src/fix/approver-registry.js +157 -0
- package/src/history-scan.js +22 -5
- package/src/ir/CLAUDE.md +1 -1
- package/src/llm-validator/index.js +86 -9
- package/src/llm-validator/model-status.js +66 -0
- package/src/lsp/server.js +49 -2
- package/src/mcp/tools.js +177 -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 +154 -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 +108 -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 +123 -0
- package/src/posture/accuracy-scorecard.js +156 -1
- package/src/posture/adversary-agent.js +15 -3
- package/src/posture/artifact-registry.js +241 -0
- package/src/posture/auditor-walkthrough.js +186 -21
- 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 +324 -17
- package/src/posture/cross-repo-memory.js +7 -2
- 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 +137 -20
- package/src/posture/fix-honesty-gate.js +47 -6
- package/src/posture/fix-verify.js +65 -8
- package/src/posture/fleet.js +0 -0
- package/src/posture/flow-narration.js +7 -2
- package/src/posture/git-history.js +13 -5
- package/src/posture/legal-hold.js +140 -0
- package/src/posture/llm-redteam.js +10 -1
- package/src/posture/material-change.js +111 -2
- package/src/posture/mttr.js +75 -12
- package/src/posture/policy-bundle.js +274 -0
- package/src/posture/pre-incident-archaeology.js +39 -7
- package/src/posture/privacy-framework.js +47 -6
- package/src/posture/production-feedback.js +179 -0
- package/src/posture/provenance/ai-authorship.js +68 -0
- package/src/posture/provenance/branch-entry.js +80 -0
- package/src/posture/provenance/cache.js +143 -0
- package/src/posture/provenance/confidence.js +36 -0
- package/src/posture/provenance/coordinator.js +786 -0
- package/src/posture/provenance/dag-walk.js +249 -0
- package/src/posture/provenance/evidence-attribution.js +59 -0
- package/src/posture/provenance/git-evidence.js +310 -0
- package/src/posture/provenance/lifecycle.js +208 -0
- package/src/posture/provenance/missing-control-resolver.js +137 -0
- package/src/posture/provenance/origin-resolver.js +342 -0
- package/src/posture/provenance/predicate-replay.js +133 -0
- package/src/posture/provenance/providers/config.js +39 -0
- package/src/posture/provenance/providers/github.js +62 -0
- package/src/posture/provenance/providers/gitlab.js +58 -0
- package/src/posture/provenance/repo-lineage.js +74 -0
- package/src/posture/provenance/sca-origin.js +139 -0
- package/src/posture/provenance/schema.js +255 -0
- package/src/posture/provenance/transitive-sca.js +147 -0
- package/src/posture/provenance/validate.js +30 -0
- package/src/posture/provenance-evidence-bundle.js +144 -0
- package/src/posture/retention-policy.js +132 -0
- package/src/posture/risk-dollars.js +216 -26
- package/src/posture/sbom-diff.js +15 -2
- package/src/posture/scan-checkpoint.js +176 -31
- package/src/posture/secret-history.js +10 -2
- package/src/posture/state-dir.js +64 -5
- package/src/posture/state-lifecycle-report.js +77 -0
- package/src/posture/suppressions.js +59 -3
- package/src/posture/vuln-archaeology.js +8 -2
- package/src/pr-delta.js +25 -4
- package/src/privacy/ir-adapter.js +380 -0
- package/src/report/index.js +248 -5
- package/src/report/oscal.js +7 -2
- package/src/runScan.js +34 -5
- package/src/sast/cpp.js +3 -14
- package/src/sast/rate-limit.js +33 -3
- package/src/sca/llm-function-extract.js +6 -0
- package/src/util/git-hardening.js +128 -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';
|
|
@@ -213,6 +260,17 @@ import { annotateAttackTaxonomy, summarizeTaxonomy } from './posture/attack-taxo
|
|
|
213
260
|
import { suppressByPastDecisions } from './posture/triage-memory.js';
|
|
214
261
|
import { suppressByIntent } from './posture/intent-context.js';
|
|
215
262
|
import { annotateGitHistory } from './posture/git-history.js';
|
|
263
|
+
// NOT `annotateProvenance` (sca/sigstore-verify.js's build-attestation
|
|
264
|
+
// annotator) and NOT `annotateFindingProvenance` (posture/provenance.js's
|
|
265
|
+
// AI-code fingerprint annotator). Both are already imported above in this
|
|
266
|
+
// file, so either name here is a duplicate binding — a SyntaxError — and the
|
|
267
|
+
// second is worse still because it also takes a findings array as its first
|
|
268
|
+
// argument, so a wrong import would RUN rather than fail. This one is named
|
|
269
|
+
// for the mechanism that distinguishes it —
|
|
270
|
+
// provenance derived from GIT HISTORY. See provenance/coordinator.js's header.
|
|
271
|
+
import { annotateGitProvenance, PROVENANCE_DEFAULT_TIMEOUT_MS, MAX_PROVIDER_ENRICHMENTS_PER_SCAN } from './posture/provenance/coordinator.js';
|
|
272
|
+
import { updateLifecycle } from './posture/provenance/lifecycle.js';
|
|
273
|
+
import { emptyProvenance, PROVENANCE_STATUS } from './posture/provenance/schema.js';
|
|
216
274
|
import { applyThreatModel } from './posture/threat-model-grounding.js';
|
|
217
275
|
import { annotateCrossRepoSignals } from './posture/pattern-propagation.js';
|
|
218
276
|
import { annotateRiskDollars } from './posture/risk-dollars.js';
|
|
@@ -235,7 +293,7 @@ import { annotateAttackPlaybooks } from './posture/attack-playbooks.js';
|
|
|
235
293
|
// R8: opt-in scan checkpointing/resume for the per-file loop.
|
|
236
294
|
import {
|
|
237
295
|
openCheckpoint, recordFileDone, completedFiles, resumeFindings, closeCheckpoint,
|
|
238
|
-
|
|
296
|
+
computeGlobalKey, globalKeyMeta, invalidatedFiles, bundleShaForRunKey,
|
|
239
297
|
} from './posture/scan-checkpoint.js';
|
|
240
298
|
import { SCANNER_VERSION as _ENGINE_VERSION } from './posture/version.js';
|
|
241
299
|
import { effectiveVersion as _effectiveRulesetVersion } from './posture/ruleset-version.js';
|
|
@@ -2311,7 +2369,8 @@ function _brokenMarkerSecurityPredicate(matchText, ctx) {
|
|
|
2311
2369
|
// SAST findings here are expected by design — suppress to avoid noise.
|
|
2312
2370
|
const _INTENTIONAL_VULN_PATH_RE = /(?:^|\/)(?:codefixes|challenge[_\-]?(?:solution|code|fix|answer)|intentional[_\-]?vuln|ctf[_\-]?solution|vulnerable[_\-]?(?:example|sample|code))(?:\/|$)/i;
|
|
2313
2371
|
|
|
2314
|
-
function scanStructuralVulns(fp, raw) {
|
|
2372
|
+
function scanStructuralVulns(fp, raw, _suppOut) {
|
|
2373
|
+
const _supp = _suppOut || _suppressionLog;
|
|
2315
2374
|
if (_INTENTIONAL_VULN_PATH_RE.test(fp.replace(/\\/g, '/'))) return [];
|
|
2316
2375
|
// Structural patterns vary: some describe code shapes (eval(), child_process.)
|
|
2317
2376
|
// and shouldn't match in strings; others ALSO scan string content (e.g.
|
|
@@ -2324,7 +2383,7 @@ function scanStructuralVulns(fp, raw) {
|
|
|
2324
2383
|
const findings = [];
|
|
2325
2384
|
const ctx = inferFileContext(fp, raw);
|
|
2326
2385
|
for (const pat of STRUCTURAL_VULN_PATTERNS) {
|
|
2327
|
-
if (!_ruleAppliesIn(pat, ctx)) {
|
|
2386
|
+
if (!_ruleAppliesIn(pat, ctx)) { _supp.push({vuln:pat.vuln,file:fp,line:0,snippet:'',reason:'context-mismatch:'+ctx.kind}); continue; }
|
|
2328
2387
|
if (pat.langScope && !pat.langScope.test(fp)) { continue; }
|
|
2329
2388
|
const re = new RegExp(pat.regex.source, pat.regex.flags);
|
|
2330
2389
|
// Default: match against the string-stripped view so rule-library shapes
|
|
@@ -2341,7 +2400,7 @@ function scanStructuralVulns(fp, raw) {
|
|
|
2341
2400
|
if (typeof pat.predicate === 'function') {
|
|
2342
2401
|
const verdict = pat.predicate(m[0], { file: fp, line, snippet, lines, raw, cleanedNoise });
|
|
2343
2402
|
if (verdict && !verdict.fire) {
|
|
2344
|
-
|
|
2403
|
+
_supp.push({vuln:pat.vuln, file:fp, line, snippet, reason:'predicate-pass:'+(verdict.reason||'ok')});
|
|
2345
2404
|
continue;
|
|
2346
2405
|
}
|
|
2347
2406
|
}
|
|
@@ -2349,7 +2408,7 @@ function scanStructuralVulns(fp, raw) {
|
|
|
2349
2408
|
// Used to scope rules like Django DEBUG=True to files that actually
|
|
2350
2409
|
// import / configure Django (avoid mis-firing on Flask's app.debug).
|
|
2351
2410
|
if (pat.contextRe && !pat.contextRe.test(raw)) {
|
|
2352
|
-
|
|
2411
|
+
_supp.push({vuln:pat.vuln, file:fp, line, snippet, reason:'context-mismatch'});
|
|
2353
2412
|
continue;
|
|
2354
2413
|
}
|
|
2355
2414
|
// FP-4: severity classifier — return null to suppress, otherwise overrides pat.severity.
|
|
@@ -2357,7 +2416,7 @@ function scanStructuralVulns(fp, raw) {
|
|
|
2357
2416
|
if (typeof pat.severityFn === 'function') {
|
|
2358
2417
|
const s = pat.severityFn(m[0], { file: fp, line, snippet, lines });
|
|
2359
2418
|
if (s === null) {
|
|
2360
|
-
|
|
2419
|
+
_supp.push({vuln:pat.vuln, file:fp, line, snippet, reason:'severity-fn:non-security-context'});
|
|
2361
2420
|
continue;
|
|
2362
2421
|
}
|
|
2363
2422
|
effectiveSeverity = s;
|
|
@@ -2366,11 +2425,11 @@ function scanStructuralVulns(fp, raw) {
|
|
|
2366
2425
|
// hardened code — an SSRF host allow/deny check, or a path
|
|
2367
2426
|
// basename/containment guard near the sink — is a false positive.
|
|
2368
2427
|
if (/SSRF/.test(pat.vuln) && _hasSsrfHostGuard({ lines, line })) {
|
|
2369
|
-
|
|
2428
|
+
_supp.push({vuln:pat.vuln, file:fp, line, snippet, reason:'ssrf-host-guard'});
|
|
2370
2429
|
continue;
|
|
2371
2430
|
}
|
|
2372
2431
|
if (/Path Traversal/.test(pat.vuln) && _hasPathGuard({ lines, line })) {
|
|
2373
|
-
|
|
2432
|
+
_supp.push({vuln:pat.vuln, file:fp, line, snippet, reason:'path-contained'});
|
|
2374
2433
|
continue;
|
|
2375
2434
|
}
|
|
2376
2435
|
const id = `struct:${fp}:${line}:${pat.vuln.replace(/\s/g, '_')}`;
|
|
@@ -2578,6 +2637,33 @@ function _resetSuppressions(){ _suppressionLog.length = 0; }
|
|
|
2578
2637
|
// so pfr[p] and the aggregates share object identity, exactly as in a normal run.
|
|
2579
2638
|
function _pfrMetaOnly(ta){ if(!ta||typeof ta!=='object')return {}; const o={}; for(const k of Object.keys(ta)){ if(k==='findings'||k==='sources'||k==='sinks'||k==='sanitizers')continue; o[k]=ta[k]; } return o; }
|
|
2580
2639
|
function _getSuppressions(){ return [..._suppressionLog]; }
|
|
2640
|
+
// Task 11 reentrancy fix: `_suppressionLog` is module-level and unconditionally
|
|
2641
|
+
// cleared by `_resetSuppressions()` at the top of every `runFullScan` call.
|
|
2642
|
+
// `predicate-replay.js`'s `replayAt` calls `runFullScan` recursively FROM
|
|
2643
|
+
// WITHIN an outer, still-running scan's provenance resolution (to replay a
|
|
2644
|
+
// finding's predicate at a historical commit) -- before Task 11 wired
|
|
2645
|
+
// scan.secrets/scan.logicVulns into real provenance resolution, that recursive
|
|
2646
|
+
// call was only ever reachable from scan.findings/SCA origin walks, which this
|
|
2647
|
+
// exact fixture (test/fixtures/entropy-fp) never triggered. Wiring secrets in
|
|
2648
|
+
// exposed it for the first time: the nested call's `_resetSuppressions()`
|
|
2649
|
+
// silently wiped the OUTER scan's suppression log before its own return
|
|
2650
|
+
// statement read it via `_getSuppressions()`, so `scan.suppressions` came back
|
|
2651
|
+
// empty for anything that happened to walk deep git history.
|
|
2652
|
+
//
|
|
2653
|
+
// A plain snapshot/restore around ONE `replayAt` call is not sufficient on its
|
|
2654
|
+
// own: `coordinator.js` resolves several findings' origins CONCURRENTLY (its
|
|
2655
|
+
// own comment: "the scheduler runs these four at a time"), and each finding's
|
|
2656
|
+
// resolveOrigin walk can call `replayAt` multiple times sequentially -- so two
|
|
2657
|
+
// DIFFERENT findings' replay calls can be in flight at once, interleaved at
|
|
2658
|
+
// `runFullScan`'s own internal await points. Two overlapping snapshot/restore
|
|
2659
|
+
// pairs racing on the same global array means whichever restores last wins,
|
|
2660
|
+
// discarding whatever the other legitimately wrote in between. Exported so
|
|
2661
|
+
// predicate-replay.js can snapshot/restore its own call boundary AND serialize
|
|
2662
|
+
// that boundary process-wide (see its own comment on the exclusivity queue) --
|
|
2663
|
+
// the nested scan's own suppression output is never read by replayAt, so
|
|
2664
|
+
// nothing is lost by discarding it.
|
|
2665
|
+
function _snapshotSuppressionLog(){ return _suppressionLog.slice(); }
|
|
2666
|
+
function _restoreSuppressionLog(saved){ _suppressionLog.length = 0; if (Array.isArray(saved)) _suppressionLog.push(...saved); }
|
|
2581
2667
|
|
|
2582
2668
|
// ── inline suppression pragma ───────────────────────────────────────────────
|
|
2583
2669
|
//
|
|
@@ -3002,7 +3088,8 @@ function _logicPredicateFor(vuln){
|
|
|
3002
3088
|
return null;
|
|
3003
3089
|
}
|
|
3004
3090
|
|
|
3005
|
-
function scanLogicVulns(fp,raw){
|
|
3091
|
+
function scanLogicVulns(fp,raw,_suppOut){
|
|
3092
|
+
const _supp = _suppOut || _suppressionLog;
|
|
3006
3093
|
// Logic rules generally inspect the surrounding handler block including
|
|
3007
3094
|
// string-literal route paths and key names, so the comment-stripped (but
|
|
3008
3095
|
// string-preserving) view is the right default. Rules that explicitly only
|
|
@@ -3012,7 +3099,7 @@ function scanLogicVulns(fp,raw){
|
|
|
3012
3099
|
const lines=raw.split("\n");const results=[];
|
|
3013
3100
|
const ctx = inferFileContext(fp, raw);
|
|
3014
3101
|
for(const pat of LOGIC_PATTERNS){
|
|
3015
|
-
if (!_ruleAppliesIn(pat, ctx)) {
|
|
3102
|
+
if (!_ruleAppliesIn(pat, ctx)) { _supp.push({vuln:pat.vuln,file:fp,line:0,snippet:'',reason:'context-mismatch:'+ctx.kind}); continue; }
|
|
3016
3103
|
if (pat.langScope && !pat.langScope.test(fp)) { continue; }
|
|
3017
3104
|
const re=new RegExp(pat.regex.source,pat.regex.flags);
|
|
3018
3105
|
const predicate = _logicPredicateFor(pat.vuln);
|
|
@@ -3025,7 +3112,7 @@ function scanLogicVulns(fp,raw){
|
|
|
3025
3112
|
// FP-2: credential FP filter
|
|
3026
3113
|
if(pat.vuln==='Hardcoded Secret'||pat.vuln==='Hardcoded Credential Check'){
|
|
3027
3114
|
const fpCheck=_isFalsePositiveCredential(fp,snippet,m[0]);
|
|
3028
|
-
if(fpCheck.skip){
|
|
3115
|
+
if(fpCheck.skip){_supp.push({vuln:pat.vuln,file:fp,line,snippet,reason:fpCheck.reason});continue;}
|
|
3029
3116
|
// Stage 4 correctness audit (coverage breadth, secrets): same
|
|
3030
3117
|
// unredacted-snippet leak found in engine.js's scanEntropySecrets/
|
|
3031
3118
|
// scanCredentials and sast/secret-concat.js — this is a THIRD,
|
|
@@ -3044,7 +3131,7 @@ function scanLogicVulns(fp,raw){
|
|
|
3044
3131
|
if (predicate) {
|
|
3045
3132
|
const verdict = predicate(m[0], {file:fp, line, snippet, lines});
|
|
3046
3133
|
if (verdict && !verdict.fire) {
|
|
3047
|
-
|
|
3134
|
+
_supp.push({vuln:pat.vuln, file:fp, line, snippet, reason:'logic-gate:'+verdict.reason});
|
|
3048
3135
|
continue;
|
|
3049
3136
|
}
|
|
3050
3137
|
}
|
|
@@ -5834,14 +5921,15 @@ const EXTRA_STRUCTURAL_PATTERNS=[
|
|
|
5834
5921
|
fix:"Delete debug routes from production builds. NODE_ENV is unreliable as a security boundary."},
|
|
5835
5922
|
];
|
|
5836
5923
|
|
|
5837
|
-
function scanExtraStructural(fp,raw){
|
|
5924
|
+
function scanExtraStructural(fp,raw,_suppOut){
|
|
5925
|
+
const _supp = _suppOut || _suppressionLog;
|
|
5838
5926
|
const cleaned=stripNoiseAndStrings(raw);
|
|
5839
5927
|
const cleanedNoise=stripNoise(raw,fp);
|
|
5840
5928
|
const lines=raw.split('\n');
|
|
5841
5929
|
const findings=[];
|
|
5842
5930
|
const ctx = inferFileContext(fp, raw);
|
|
5843
5931
|
for(const pat of EXTRA_STRUCTURAL_PATTERNS){
|
|
5844
|
-
if (!_ruleAppliesIn(pat, ctx)) {
|
|
5932
|
+
if (!_ruleAppliesIn(pat, ctx)) { _supp.push({vuln:pat.vuln,file:fp,line:0,snippet:'',reason:'context-mismatch:'+ctx.kind}); continue; }
|
|
5845
5933
|
if (pat.langScope && !pat.langScope.test(fp)) { continue; }
|
|
5846
5934
|
const re=new RegExp(pat.regex.source,pat.regex.flags);
|
|
5847
5935
|
const haystack = pat.readsStringContent ? cleanedNoise : cleaned;
|
|
@@ -5851,14 +5939,14 @@ function scanExtraStructural(fp,raw){
|
|
|
5851
5939
|
const snippet=lines[line-1]?.trim()||'';
|
|
5852
5940
|
// contextRe: require a context match across the whole file.
|
|
5853
5941
|
if (pat.contextRe && !pat.contextRe.test(raw)) {
|
|
5854
|
-
|
|
5942
|
+
_supp.push({vuln:pat.vuln, file:fp, line, snippet, reason:'context-mismatch'});
|
|
5855
5943
|
continue;
|
|
5856
5944
|
}
|
|
5857
5945
|
// Per-pattern predicate gate (mirrors scanStructuralVulns).
|
|
5858
5946
|
if (typeof pat.predicate === 'function') {
|
|
5859
5947
|
const verdict = pat.predicate(m[0], { file: fp, line, snippet, lines, raw, cleanedNoise });
|
|
5860
5948
|
if (verdict && !verdict.fire) {
|
|
5861
|
-
|
|
5949
|
+
_supp.push({vuln:pat.vuln, file:fp, line, snippet, reason:'predicate-pass:'+(verdict.reason||'ok')});
|
|
5862
5950
|
continue;
|
|
5863
5951
|
}
|
|
5864
5952
|
}
|
|
@@ -5913,7 +6001,8 @@ function _isLikelyNonSecret(v, ctxLine, surroundingLines){
|
|
|
5913
6001
|
return null;
|
|
5914
6002
|
}
|
|
5915
6003
|
|
|
5916
|
-
function scanEntropySecrets(fp,raw){
|
|
6004
|
+
function scanEntropySecrets(fp,raw,_suppOut){
|
|
6005
|
+
const _supp = _suppOut || _suppressionLog;
|
|
5917
6006
|
if(raw.length>400000)return[]; // skip huge blobs
|
|
5918
6007
|
const out=[];
|
|
5919
6008
|
const lines=raw.split("\n");
|
|
@@ -5942,7 +6031,7 @@ function scanEntropySecrets(fp,raw){
|
|
|
5942
6031
|
// Same redaction concern as the main finding below applies to the
|
|
5943
6032
|
// suppression log — it's exposed via --include-suppressed, and a
|
|
5944
6033
|
// heuristic "probably not a real secret" call can be wrong.
|
|
5945
|
-
|
|
6034
|
+
_supp.push({vuln:"High-Entropy Credential Candidate",file:fp,line,snippet:ctx.trim().split(v).join(masked),reason:'entropy-'+nonSecretReason});
|
|
5946
6035
|
continue;
|
|
5947
6036
|
}
|
|
5948
6037
|
// Stage 4 correctness audit (coverage breadth, secrets): `snippet` used
|
|
@@ -7055,6 +7144,15 @@ function _osvCacheSet(key,val){try{sessionStorage.setItem('osv_'+key,JSON.string
|
|
|
7055
7144
|
// Batched: api.first.org accepts up to ~100 CVEs per request via ?cve=A,B,C…
|
|
7056
7145
|
// One HTTP round trip per 100 CVEs instead of one per CVE. Cache lookups
|
|
7057
7146
|
// remain per-CVE so a partial cache-hit still benefits.
|
|
7147
|
+
//
|
|
7148
|
+
// FR-207: EPSS scores decay in relevance (same note as posture/epss.js's
|
|
7149
|
+
// separate implementation) -- track how old the CVE-keyed entries actually
|
|
7150
|
+
// USED by this scan are, mirroring _setKevMeta immediately below, so a
|
|
7151
|
+
// stale feed is visible on scan.scanHealth rather than silently applied.
|
|
7152
|
+
const _EPSS_STALE_AFTER_MS = 7 * 24 * 60 * 60 * 1000;
|
|
7153
|
+
let _epssLiveMeta = { source: 'not-loaded', ageDays: null, stale: null, cvesChecked: 0 };
|
|
7154
|
+
export function epssLiveMeta() { return { ..._epssLiveMeta }; }
|
|
7155
|
+
|
|
7058
7156
|
const _EPSS_BATCH = 100;
|
|
7059
7157
|
async function _fetchEPSSBatch(cveIds){
|
|
7060
7158
|
if (!cveIds || !cveIds.length) return new Map();
|
|
@@ -7069,7 +7167,7 @@ async function _fetchEPSSBatch(cveIds){
|
|
|
7069
7167
|
const res = await fetch(url, { headers: { 'User-Agent': 'agentic-security/0.1' } });
|
|
7070
7168
|
if (!res.ok) {
|
|
7071
7169
|
// 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,
|
|
7170
|
+
for (const c of batch) _osvCacheSet('epss:'+c, { miss: true, ts: Date.now() });
|
|
7073
7171
|
continue;
|
|
7074
7172
|
}
|
|
7075
7173
|
const j = await res.json();
|
|
@@ -7080,7 +7178,7 @@ async function _fetchEPSSBatch(cveIds){
|
|
|
7080
7178
|
const score = parseFloat(row.epss);
|
|
7081
7179
|
const percentile = parseFloat(row.percentile);
|
|
7082
7180
|
if (Number.isFinite(score) && Number.isFinite(percentile)) {
|
|
7083
|
-
const v = { score, percentile };
|
|
7181
|
+
const v = { score, percentile, ts: Date.now() };
|
|
7084
7182
|
out.set(cve, v);
|
|
7085
7183
|
_osvCacheSet('epss:'+cve, v);
|
|
7086
7184
|
seen.add(cve);
|
|
@@ -7088,7 +7186,7 @@ async function _fetchEPSSBatch(cveIds){
|
|
|
7088
7186
|
}
|
|
7089
7187
|
// CVEs in the batch that EPSS does not know — cache the negative so we
|
|
7090
7188
|
// don't retry within this scan run.
|
|
7091
|
-
for (const c of batch) if (!seen.has(c.toUpperCase())) _osvCacheSet('epss:'+c,
|
|
7189
|
+
for (const c of batch) if (!seen.has(c.toUpperCase())) _osvCacheSet('epss:'+c, { miss: true, ts: Date.now() });
|
|
7092
7190
|
} catch { /* network error → caller continues without enrichment */ }
|
|
7093
7191
|
}
|
|
7094
7192
|
return out;
|
|
@@ -7103,14 +7201,43 @@ async function _enrichWithEPSS(supplyChainResults){
|
|
|
7103
7201
|
// Cache lookup pass: keep CVEs we already have, defer the rest to one batched fetch.
|
|
7104
7202
|
const epssByCve = new Map();
|
|
7105
7203
|
const uncached = [];
|
|
7204
|
+
// FR-207: age of every entry actually used this scan, cache hit or fresh
|
|
7205
|
+
// fetch — a fresh fetch is age~0, a cache hit carries whatever `ts` it was
|
|
7206
|
+
// written with. A pre-existing on-disk entry from before this field
|
|
7207
|
+
// existed has no `ts` (`|| 0` → maximally stale), matching this
|
|
7208
|
+
// codebase's established "never-dated == already stale" convention
|
|
7209
|
+
// (compliance-policy.js's `_staleness`).
|
|
7210
|
+
const ages = [];
|
|
7106
7211
|
for (const c of allCves) {
|
|
7107
7212
|
const hit = _osvCacheGet('epss:'+c);
|
|
7108
|
-
if (hit === null) uncached.push(c);
|
|
7109
|
-
|
|
7213
|
+
if (hit === null) { uncached.push(c); continue; }
|
|
7214
|
+
if (hit === false || hit.miss) continue; // tried, no data — leave unset
|
|
7215
|
+
epssByCve.set(c, hit);
|
|
7216
|
+
ages.push(Date.now() - (hit.ts || 0));
|
|
7110
7217
|
}
|
|
7111
7218
|
if (uncached.length) {
|
|
7112
7219
|
const fetched = await _fetchEPSSBatch(uncached);
|
|
7113
|
-
for (const [cve, v] of fetched) epssByCve.set(cve, v);
|
|
7220
|
+
for (const [cve, v] of fetched) { epssByCve.set(cve, v); ages.push(Date.now() - (v.ts || 0)); }
|
|
7221
|
+
}
|
|
7222
|
+
// Offline mode means "we deliberately have no network access this run" --
|
|
7223
|
+
// mirroring _loadKEVCatalog's own offline handling immediately above,
|
|
7224
|
+
// that is a genuine "we cannot vouch for freshness right now" state
|
|
7225
|
+
// (stale: null), not license to label whatever is on disk stale OR
|
|
7226
|
+
// fresh. This matters in practice: a cache entry written before this
|
|
7227
|
+
// field existed has no `ts` at all, and without the offline carve-out
|
|
7228
|
+
// that would report as "56 years old" (age computed against epoch)
|
|
7229
|
+
// under the exact conditions (no network) where that claim is least
|
|
7230
|
+
// verifiable.
|
|
7231
|
+
if (process.env.AGENTIC_SECURITY_OFFLINE === '1') {
|
|
7232
|
+
_epssLiveMeta = { source: 'offline-skipped', ageDays: null, stale: null, cvesChecked: ages.length };
|
|
7233
|
+
} else if (ages.length) {
|
|
7234
|
+
const oldestMs = Math.max(...ages);
|
|
7235
|
+
_epssLiveMeta = {
|
|
7236
|
+
source: 'cache/live',
|
|
7237
|
+
ageDays: Math.floor(oldestMs / 86400000),
|
|
7238
|
+
stale: oldestMs > _EPSS_STALE_AFTER_MS,
|
|
7239
|
+
cvesChecked: ages.length,
|
|
7240
|
+
};
|
|
7114
7241
|
}
|
|
7115
7242
|
for (const r of out) {
|
|
7116
7243
|
const cve = (r.cveAliases || []).find(a => /^CVE-/.test(a));
|
|
@@ -7251,6 +7378,31 @@ function _makePurl(ecosystem,name,version,group){
|
|
|
7251
7378
|
return`pkg:${t}/${ns}${encodeURIComponent(name)}${version?'@'+encodeURIComponent(version):''}`;
|
|
7252
7379
|
}
|
|
7253
7380
|
|
|
7381
|
+
function _findManifestLine(text, sectionKey, depName) {
|
|
7382
|
+
const lines = text.split('\n');
|
|
7383
|
+
let inSection = false;
|
|
7384
|
+
let depth = 0;
|
|
7385
|
+
const escaped = depName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
7386
|
+
const nameRe = new RegExp(`"${escaped}"\\s*:`);
|
|
7387
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7388
|
+
const line = lines[i];
|
|
7389
|
+
if (!inSection) {
|
|
7390
|
+
if (new RegExp(`"${sectionKey}"\\s*:\\s*\\{`).test(line)) {
|
|
7391
|
+
inSection = true;
|
|
7392
|
+
depth = 1 + (line.match(/\{/g) || []).length - 1 - (line.match(/\}/g) || []).length;
|
|
7393
|
+
if (nameRe.test(line)) return i + 1;
|
|
7394
|
+
if (depth <= 0) inSection = false;
|
|
7395
|
+
}
|
|
7396
|
+
continue;
|
|
7397
|
+
}
|
|
7398
|
+
depth += (line.match(/\{/g) || []).length;
|
|
7399
|
+
depth -= (line.match(/\}/g) || []).length;
|
|
7400
|
+
if (depth <= 0) { inSection = false; continue; }
|
|
7401
|
+
if (nameRe.test(line)) return i + 1;
|
|
7402
|
+
}
|
|
7403
|
+
return null;
|
|
7404
|
+
}
|
|
7405
|
+
|
|
7254
7406
|
function _parsePackageJson(text,filePath){
|
|
7255
7407
|
const out=[];try{const d=JSON.parse(text);
|
|
7256
7408
|
for(const[depKey,scope]of[['dependencies','required'],['devDependencies','optional']]){
|
|
@@ -7261,7 +7413,8 @@ function _parsePackageJson(text,filePath){
|
|
|
7261
7413
|
const group=scoped?`@${parts[0]}`:'';
|
|
7262
7414
|
const pkgName=scoped?parts[1]:name;
|
|
7263
7415
|
out.push({name,version:ver,group,scope,purl:_makePurl('npm',pkgName,ver,group),ecosystem:'npm',filePath,
|
|
7264
|
-
isUnpinned:verRange==='*'||verRange==='latest'||verRange===''||verRange==='>=0.0.0'
|
|
7416
|
+
isUnpinned:verRange==='*'||verRange==='latest'||verRange===''||verRange==='>=0.0.0',
|
|
7417
|
+
line:_findManifestLine(text,depKey,name)});
|
|
7265
7418
|
}
|
|
7266
7419
|
}
|
|
7267
7420
|
}catch(_){}return out;
|
|
@@ -7290,12 +7443,13 @@ function _parsePackageLockJson(text,filePath){
|
|
|
7290
7443
|
|
|
7291
7444
|
function _parseRequirementsTxt(text,filePath){
|
|
7292
7445
|
const out=[];
|
|
7293
|
-
|
|
7294
|
-
|
|
7446
|
+
const lines=text.split('\n');
|
|
7447
|
+
for(let i=0;i<lines.length;i++){
|
|
7448
|
+
const t=lines[i].trim();
|
|
7295
7449
|
if(!t||t.startsWith('#')||t.startsWith('-'))continue;
|
|
7296
7450
|
const m=t.match(/^([A-Za-z0-9_.-]+)\s*[=~<>!]+\s*([^\s;#,]*)/);
|
|
7297
7451
|
if(m)out.push({name:m[1],version:m[2],group:'',scope:'required',
|
|
7298
|
-
purl:_makePurl('pypi',m[1].toLowerCase(),m[2],''),ecosystem:'pypi',filePath,isUnpinned:false});
|
|
7452
|
+
purl:_makePurl('pypi',m[1].toLowerCase(),m[2],''),ecosystem:'pypi',filePath,isUnpinned:false,line:i+1});
|
|
7299
7453
|
}return out;
|
|
7300
7454
|
}
|
|
7301
7455
|
|
|
@@ -8091,6 +8245,15 @@ async function queryOSV(components,allFileContents){
|
|
|
8091
8245
|
fixedVersions: vuln.fixedVersions, severity: vuln.severity, cvssVector: vuln.cvssVector,
|
|
8092
8246
|
hasKnownAttackRef: vuln.hasKnownAttackRef, osvVulnFunctions: vuln.osvVulnFunctions || [], reachable: comp.reachable, scope: comp.scope,
|
|
8093
8247
|
file: comp.filePath,
|
|
8248
|
+
// `isDirect` is backfilled onto every component just above the queryOSV
|
|
8249
|
+
// call, but was never carried onto the entry materialized from it — so
|
|
8250
|
+
// every consumer asking "is this a direct dependency" got `undefined`.
|
|
8251
|
+
// The transitive-dedup block's `group.find(s => s.isDirect)` has
|
|
8252
|
+
// therefore always fallen through to `group[0]`, picking an arbitrary
|
|
8253
|
+
// member as the primary instead of the direct one, and the provenance
|
|
8254
|
+
// pass's direct-only filter had nothing to filter on. `line` is Task
|
|
8255
|
+
// 12's declaration line, which the SCA provenance evidence node reads.
|
|
8256
|
+
isDirect: comp.isDirect, line: comp.line, depChain: Array.isArray(comp.depChain) ? comp.depChain.map((s) => s.replace(/\/$/, '')) : [],
|
|
8094
8257
|
// kept for generateRecs() compat
|
|
8095
8258
|
advisory: `${vid}${cveStr}, ${vuln.description}`,
|
|
8096
8259
|
range: fixStr ? `< ${fixStr}` : 'see advisory' });
|
|
@@ -8268,7 +8431,197 @@ async function queryRegistries(components){
|
|
|
8268
8431
|
|
|
8269
8432
|
// Node port: takes { fileContents, depFileContents } maps directly instead of a JSZip object.
|
|
8270
8433
|
// fileContents = code files keyed by relative path; depFileContents = manifest/lockfiles keyed by relative path.
|
|
8271
|
-
|
|
8434
|
+
// FR-202 phase 3b (D-0050): a worker importing this module gets a FRESH, unmutated
|
|
8435
|
+
// copy of SOURCE_PATTERNS/SINK_PATTERNS/SANITIZER_PATTERNS/_projectIndex/
|
|
8436
|
+
// _GLOBAL_JAVA_TAINTED_METHODS -- none of the main thread's pre-loop setup carries
|
|
8437
|
+
// across the worker boundary. This reproduces that exact setup (mirroring
|
|
8438
|
+
// runFullScan's own pre-loop sequence) so a long-lived worker can run it ONCE at
|
|
8439
|
+
// startup, then service many _runFileCascade calls with correctly-populated state,
|
|
8440
|
+
// instead of redoing whole-project work per file.
|
|
8441
|
+
export async function _initCascadeWorkerState(fileContents, scanRoot) {
|
|
8442
|
+
_buildProjectIndex(fileContents);
|
|
8443
|
+
try { _GLOBAL_JAVA_TAINTED_METHODS = _buildGlobalJavaTaintedMethodIndex(fileContents); }
|
|
8444
|
+
catch { _GLOBAL_JAVA_TAINTED_METHODS = new Set(); }
|
|
8445
|
+
await _loadCustomRules(scanRoot);
|
|
8446
|
+
}
|
|
8447
|
+
|
|
8448
|
+
// FR-202 phase 3b (D-0050): the shape a preemptively-KILLED worker task leaves
|
|
8449
|
+
// behind. Unlike the existing post-hoc _perFileTimeoutMs check (which fires only
|
|
8450
|
+
// after a slow-but-completed cascade already populated a real delta), a worker
|
|
8451
|
+
// timeout means the file's analysis was terminated mid-execution -- there is no
|
|
8452
|
+
// partial result to salvage, so every field is empty and only the timeout marker
|
|
8453
|
+
// finding (added by the caller, same as the cooperative path) reflects the file.
|
|
8454
|
+
function _emptyCascadeDelta(c) {
|
|
8455
|
+
return {
|
|
8456
|
+
content: c, pfr: { findings: [], sources: [], sinks: [], sanitizers: [] },
|
|
8457
|
+
routes: [], findings: [], sources: [], sinks: [], sanitizers: [],
|
|
8458
|
+
logic: [], secrets: [], ciphersRest: [], ciphersTransit: [], suppressions: [],
|
|
8459
|
+
};
|
|
8460
|
+
}
|
|
8461
|
+
|
|
8462
|
+
// FR-202 phase 3a (D-0049): the per-file detector cascade returns a self-contained
|
|
8463
|
+
// delta instead of mutating the outer accumulator arrays directly, so the same
|
|
8464
|
+
// function body can later run inside a worker (phase 3b) and post its result back
|
|
8465
|
+
// rather than reaching into shared memory it would not have access to.
|
|
8466
|
+
export function _runFileCascade(p, c, scanRoot, _detectorErrors) {
|
|
8467
|
+
const _aR=[],_aF=[],_aSrc=[],_aSink=[],_aSan=[],_aLogic=[],_aSecrets=[],_aCiphersRest=[],_aCiphersTransit=[],_aSupp=[];
|
|
8468
|
+
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))||[]));
|
|
8469
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanLLM',()=>scanLLM(p,c))||[]));
|
|
8470
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanLLMOwasp',()=>scanLLMOwasp(p,c))||[]));
|
|
8471
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanLlmCost',()=>scanLlmCost(p,c))||[]));
|
|
8472
|
+
_aLogic.push(...(runDetector(_detectorErrors,p,'scanBusinessLogic',()=>scanBusinessLogic(p,cc))||[]));
|
|
8473
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanPipeline',()=>scanPipeline(p,cc))||[]));
|
|
8474
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanContainer',()=>scanContainer(p,cc))||[]));
|
|
8475
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanInstallScripts',()=>scanInstallScripts(p,cc))||[]));
|
|
8476
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanMCP',()=>scanMCP(p,c))||[]));
|
|
8477
|
+
// PRD F5.2 — rug-pull: a tool whose definition changed AFTER approval.
|
|
8478
|
+
// Every scanMCP rule judges the CURRENT content, so a description that is
|
|
8479
|
+
// innocuous today and hostile tomorrow passes both scans. This compares
|
|
8480
|
+
// against a recorded baseline, which is the only way to see a change.
|
|
8481
|
+
// Wired here rather than left as a tested module: a detector with no call
|
|
8482
|
+
// site is a dark detector, which is the exact class this session keeps
|
|
8483
|
+
// finding.
|
|
8484
|
+
if (/(?:^|[\\/])\.?mcp(?:\.[a-z]+)?\.json$|(?:^|[\\/])\.mcp\.json$/i.test(p)) {
|
|
8485
|
+
try {
|
|
8486
|
+
const _cfg = JSON.parse(c);
|
|
8487
|
+
const _rp = _detectRugPull(scanRoot, _cfg, { file: p });
|
|
8488
|
+
_aF.push(..._rp.findings);
|
|
8489
|
+
// Record on first sight so the NEXT scan has something to compare
|
|
8490
|
+
// against; refresh after reporting so a reviewed change is not
|
|
8491
|
+
// re-reported forever.
|
|
8492
|
+
_saveMcpBaseline(scanRoot, _fingerprintMcp(_cfg));
|
|
8493
|
+
} catch (e) {
|
|
8494
|
+
// Only a malformed config is tolerated here — scanMCP already reports
|
|
8495
|
+
// what it can from one. Anything else is a programmer error and must
|
|
8496
|
+
// not be swallowed: a bare `catch {}` around this block hid a
|
|
8497
|
+
// ReferenceError (`root` vs `scanRoot`) that silently disabled the
|
|
8498
|
+
// whole detector while every unit test still passed.
|
|
8499
|
+
if (!(e instanceof SyntaxError)) throw e;
|
|
8500
|
+
}
|
|
8501
|
+
}
|
|
8502
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanClaudeSettings',()=>scanClaudeSettings(p,c))||[]));
|
|
8503
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanClaudeMdPromptInjection',()=>scanClaudeMdPromptInjection(p,c))||[]));
|
|
8504
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanClaudeHookInjection',()=>scanClaudeHookInjection(p,c))||[]));
|
|
8505
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanDjangoHardening',()=>scanDjangoHardening(p,cc))||[]));
|
|
8506
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanDefiDeep',()=>scanDefiDeep(p,cc))||[]));
|
|
8507
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanSpringbootHardening',()=>scanSpringbootHardening(p,cc))||[]));
|
|
8508
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanLaravelHardening',()=>scanLaravelHardening(p,cc))||[]));
|
|
8509
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanSwift',()=>scanSwift(p,cc))||[]));
|
|
8510
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanDartFlutter',()=>scanDartFlutter(p,cc))||[]));
|
|
8511
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanWeakRandomness',()=>scanWeakRandomness(p,cc))||[]));
|
|
8512
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanGraphQLModule',()=>scanGraphQLModule(p,cc))||[]));
|
|
8513
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanSensitiveDataLogging',()=>scanSensitiveDataLogging(p,cc))||[]));
|
|
8514
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanComparisonSafety',()=>scanComparisonSafety(p,cc))||[]));
|
|
8515
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanWeakPasswordHash',()=>scanWeakPasswordHash(p,cc))||[]));
|
|
8516
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCachePoisoning',()=>scanCachePoisoning(p,cc))||[]));
|
|
8517
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanNullByteInjection',()=>scanNullByteInjection(p,cc))||[]));
|
|
8518
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanLlmTradingAgent',()=>scanLlmTradingAgent(p,c))||[]));
|
|
8519
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanMobileManifest',()=>scanMobileManifest(p,cc))||[]));
|
|
8520
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanQuarkusHardening',()=>scanQuarkusHardening(p,cc))||[]));
|
|
8521
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanFastapiHardening',()=>scanFastapiHardening(p,cc))||[]));
|
|
8522
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanAuthZ',()=>scanAuthZ(p,cc))||[]));
|
|
8523
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanModelLoad',()=>scanModelLoad(p,cc))||[]));
|
|
8524
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanPromptTemplate',()=>scanPromptTemplate(p,c))||[]));
|
|
8525
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanXXE',()=>scanXXE(p,cc))||[]));
|
|
8526
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanJNDI',()=>scanJNDI(p,cc))||[]));
|
|
8527
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanJavaDeserialization',()=>scanJavaDeserialization(p,cc))||[]));
|
|
8528
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanJwtExp',()=>scanJwtExp(p,cc))||[]));
|
|
8529
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanZipSlip',()=>scanZipSlip(p,cc))||[]));_aF.push(...(runDetector(_detectorErrors,p,'scanSiblingGuard',()=>scanSiblingGuard(p,cc))||[]));
|
|
8530
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanFileUpload',()=>scanFileUpload(p,cc))||[]));
|
|
8531
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanHostHeader',()=>scanHostHeader(p,cc))||[]));
|
|
8532
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanPythonSinks',()=>scanPythonSinks(p,cc))||[]));
|
|
8533
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCSharp',()=>scanCSharp(p,cc))||[]));
|
|
8534
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCpp',()=>scanCpp(p,cc))||[]));
|
|
8535
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanSolidity',()=>scanSolidity(p,cc))||[]));
|
|
8536
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanRust',()=>scanRust(p,cc))||[]));
|
|
8537
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanGoExtended',()=>scanGoExtended(p,cc))||[]));
|
|
8538
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanDatabaseRLS',()=>scanDatabaseRLS(p,cc))||[]));
|
|
8539
|
+
_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))||[]));
|
|
8540
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanAuthProvider',()=>scanAuthProvider(p,cc))||[]));
|
|
8541
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanEnvHygiene',()=>scanEnvHygiene(p,cc))||[]));
|
|
8542
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanWebhook',()=>scanWebhook(p,cc))||[]));
|
|
8543
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanClientSide',()=>scanClientSide(p,cc))||[]));
|
|
8544
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanPromptFirewall',()=>scanPromptFirewall(p,c))||[]));
|
|
8545
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanLlmRedteam',()=>scanLlmRedteam(p,c))||[]));
|
|
8546
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanJulietShape',()=>scanJulietShape(p,c))||[]));
|
|
8547
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCppDataflow',()=>scanCppDataflow(p,cc))||[]));
|
|
8548
|
+
// Phase 1: new detectors.
|
|
8549
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanMassAssignment',()=>scanMassAssignment(p,cc))||[]));
|
|
8550
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanPrototypePollution',()=>scanPrototypePollution(p,cc))||[]));
|
|
8551
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCSRF',()=>scanCSRF(p,cc))||[]));
|
|
8552
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanTOCTOU',()=>scanTOCTOU(p,cc))||[]));
|
|
8553
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanNoSQLInjection',()=>scanNoSQLInjection(p,cc))||[]));
|
|
8554
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanLDAPInjection',()=>scanLDAPInjection(p,cc))||[]));
|
|
8555
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanXPathInjection',()=>scanXPathInjection(p,cc))||[]));
|
|
8556
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanSSTI',()=>scanSSTI(p,cc))||[]));
|
|
8557
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanOpenRedirect',()=>scanOpenRedirect(p,cc))||[]));
|
|
8558
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanWrongContextSanitizer',()=>scanWrongContextSanitizer(p,cc))||[]));
|
|
8559
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanSanitizerContextMismatch',()=>scanSanitizerContextMismatch(p,cc))||[]));
|
|
8560
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanFrontendHygiene',()=>scanFrontendHygiene(p,cc))||[]));
|
|
8561
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCsvInjection',()=>scanCsvInjection(p,cc))||[]));
|
|
8562
|
+
// R16 — specialist crypto-hygiene classes (constant-time comparison,
|
|
8563
|
+
// secret zeroization). Narrow by design: keyed on the secret-ness of the
|
|
8564
|
+
// identifier, and silent whenever the correct constant-time or
|
|
8565
|
+
// guaranteed-wipe API is already present.
|
|
8566
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCryptoSpecialist',()=>scanCryptoSpecialist(p,cc))||[]));
|
|
8567
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanStoredTaint',()=>scanStoredTaint(p,cc))||[]));
|
|
8568
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanJavaStructural',()=>scanJavaStructural(p,cc))||[]));
|
|
8569
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCsharpStructural',()=>scanCsharpStructural(p,cc))||[]));
|
|
8570
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanJsFrameworkStructural',()=>scanJsFrameworkStructural(p,cc))||[]));
|
|
8571
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanPythonStructural',()=>scanPythonStructural(p,cc))||[]));
|
|
8572
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanGoStructural',()=>scanGoStructural(p,cc))||[]));
|
|
8573
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanSecretConcat',()=>scanSecretConcat(p,cc))||[]));
|
|
8574
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanXssReflectedMultilang',()=>scanXssReflectedMultilang(p,cc))||[]));
|
|
8575
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanCodeInjectionMultilang',()=>scanCodeInjectionMultilang(p,cc))||[]));
|
|
8576
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanResponseSplitting',()=>scanResponseSplitting(p,cc))||[]));
|
|
8577
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanStoredPromptInjection',()=>scanStoredPromptInjection(p,c))||[]));
|
|
8578
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanRAGPoisoning',()=>scanRAGPoisoning(p,c))||[]));
|
|
8579
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanAgentToolEscalation',()=>scanAgentToolEscalation(p,c))||[]));
|
|
8580
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanAgentUntrustedFlow',()=>scanAgentUntrustedFlow(p,c))||[]));
|
|
8581
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanEventEntrypoints',()=>scanEventEntrypoints(p,cc))||[]));
|
|
8582
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanDbTaint',()=>scanDbTaint(p,cc))||[]));
|
|
8583
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanSSRFCloudMetadata',()=>scanSSRFCloudMetadata(p,cc))||[]));
|
|
8584
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanMutationXSS',()=>scanMutationXSS(p,cc))||[]));
|
|
8585
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanKotlin',()=>scanKotlin(p,cc))||[]));
|
|
8586
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanRuby',()=>scanRuby(p,cc))||[]));_aF.push(...(runDetector(_detectorErrors,p,'scanRubyPathJoin',()=>scanRubyPathJoin(p,cc))||[]));
|
|
8587
|
+
_aF.push(...(runDetector(_detectorErrors,p,'scanPhp',()=>scanPhp(p,cc))||[]));
|
|
8588
|
+
// Integration block: scaffolded SAST scanners. Gated by env var.
|
|
8589
|
+
if (process.env.AGENTIC_SECURITY_NO_INTEGRATION !== '1') {
|
|
8590
|
+
if (process.env.AGENTIC_SECURITY_NO_LLM_APP !== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanLlmApp',()=>scanLlmApp(p,c))||[]));
|
|
8591
|
+
if (process.env.AGENTIC_SECURITY_NO_MOBILE !== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanMobile',()=>scanMobile(p,cc))||[]));
|
|
8592
|
+
if (process.env.AGENTIC_SECURITY_NO_PQC !== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanPqc',()=>scanPqc(p,cc))||[]));
|
|
8593
|
+
if (process.env.AGENTIC_SECURITY_NO_WEB3_ADV!== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanWeb3Advanced',()=>scanWeb3Advanced(p,cc))||[]));
|
|
8594
|
+
if (process.env.AGENTIC_SECURITY_NO_DAPP !== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanDappFrontend',()=>scanDappFrontend(p,cc))||[]));
|
|
8595
|
+
if (process.env.AGENTIC_SECURITY_NO_CLOUD_IAM!== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanCloudIam',()=>scanCloudIam(p,cc))||[]));
|
|
8596
|
+
if (process.env.AGENTIC_SECURITY_NO_K8S_ADM !== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanK8sAdmission',()=>scanK8sAdmission(p,cc))||[]));
|
|
8597
|
+
if (process.env.AGENTIC_SECURITY_NO_CRYPTO_PROTO !== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanCryptoProtocol',()=>scanCryptoProtocol(p,cc))||[]));
|
|
8598
|
+
if (process.env.AGENTIC_SECURITY_NO_ML_SUPPLY !== '1') _aF.push(...(runDetector(_detectorErrors,p,'scanMlSupplyChain',()=>scanMlSupplyChain(p,cc))||[]));
|
|
8599
|
+
}
|
|
8600
|
+
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};
|
|
8601
|
+
}
|
|
8602
|
+
|
|
8603
|
+
// `provenance:false` is the RE-ENTRANCY BRAKE, not a feature flag.
|
|
8604
|
+
// posture/provenance/predicate-replay.js answers "did this finding's condition
|
|
8605
|
+
// hold at commit X" by calling runFullScan back on that commit's blobs. Once
|
|
8606
|
+
// runFullScan itself runs the provenance pass, that is an unbounded recursion —
|
|
8607
|
+
// scan → provenance → replay → scan → … — which manifests as a scan that never
|
|
8608
|
+
// returns and spawns `git` forever, because every level of it is synchronous
|
|
8609
|
+
// execFileSync work. The replay's findings are discarded, so it wants no
|
|
8610
|
+
// provenance anyway; and it must NOT touch the lifecycle store, whose events
|
|
8611
|
+
// would otherwise be written from historical blobs as if they were this scan.
|
|
8612
|
+
// Passed explicitly per invocation rather than held in a module-level guard so
|
|
8613
|
+
// concurrent scans in one process cannot disable each other's provenance.
|
|
8614
|
+
//
|
|
8615
|
+
// `completeScan` is a SEPARATE question from `provenance`, and conflating them
|
|
8616
|
+
// is what let the fourth instance of this bug through. `provenance:false` says
|
|
8617
|
+
// "do not run the pass at all"; `completeScan:false` says "the pass may run,
|
|
8618
|
+
// but this file set is a SUBSET of scanRoot, so absence of a finding proves
|
|
8619
|
+
// nothing." Only the lifecycle ledger's remediation pass reads it — that is the
|
|
8620
|
+
// one place a finding's absence is turned into a positive claim. Defaults true
|
|
8621
|
+
// because a direct runFullScan caller supplying no file-subsetting options is
|
|
8622
|
+
// scanning everything it was given; runScan.js narrows it for --changed-since
|
|
8623
|
+
// and for caller-supplied fileContents.
|
|
8624
|
+
async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null, resume=undefined, deep=undefined, deepInCi=undefined, provenance=true, completeScan=true, skipAnnotators=false}, setProgress=()=>{}){_resetSuppressions();_buildProjectIndex(fileContents);await _loadCustomRules(scanRoot);
|
|
8272
8625
|
// Pre-pass: build cross-file Java tainted-method index so per-file taint
|
|
8273
8626
|
// analysis can recognize calls to user-input-returning helper methods
|
|
8274
8627
|
// defined in OTHER files (Juliet's DataflowThruInnerClass / Vector / Stream
|
|
@@ -8295,7 +8648,7 @@ function _deterministicFileTimings(timings) {
|
|
|
8295
8648
|
|
|
8296
8649
|
const _fileTimings = [];
|
|
8297
8650
|
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=[];
|
|
8651
|
+
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
8652
|
// ---- R8: opt-in per-file checkpointing (AGENTIC_SECURITY_RESUME=1, or
|
|
8300
8653
|
// runScan({resume:true})). Default OFF, so existing behaviour is untouched.
|
|
8301
8654
|
// Only this loop is checkpointed; every cross-file pass below re-runs, so
|
|
@@ -8303,17 +8656,28 @@ function _deterministicFileTimings(timings) {
|
|
|
8303
8656
|
const _ckptEnabled = (resume === undefined ? process.env.AGENTIC_SECURITY_RESUME === '1' : !!resume) && !!scanRoot;
|
|
8304
8657
|
let _ckpt = null; const _ckptPayloads = new Map(); let _ckptDone = new Set(); let _ckptResumed = 0, _ckptWrites = 0;
|
|
8305
8658
|
const _ckptAbortAfter = parseInt(process.env.AGENTIC_SECURITY_CHECKPOINT_ABORT_AFTER || '0', 10) || 0;
|
|
8659
|
+
let _ckptInvalidated = [];
|
|
8306
8660
|
if (_ckptEnabled) {
|
|
8307
8661
|
try {
|
|
8308
|
-
const
|
|
8662
|
+
const _ckptIdentity = {
|
|
8309
8663
|
engineVersion: _ENGINE_VERSION,
|
|
8310
8664
|
rulesetVersion: (_effectiveRulesetVersion(scanRoot) || {}).version,
|
|
8311
8665
|
bundleSha: bundleShaForRunKey(),
|
|
8312
|
-
|
|
8666
|
+
depFileContents,
|
|
8667
|
+
};
|
|
8668
|
+
// FR-208: the global key covers everything that would affect how EVERY
|
|
8669
|
+
// file is analysed (engine/ruleset/bundle/deps/env); each scanned
|
|
8670
|
+
// file's OWN content is checked per-record inside openCheckpoint
|
|
8671
|
+
// instead, so changing one file no longer discards every other file's
|
|
8672
|
+
// already-completed work — see scan-checkpoint.js's module header.
|
|
8673
|
+
_ckpt = openCheckpoint(scanRoot, {
|
|
8674
|
+
globalKey: computeGlobalKey(_ckptIdentity),
|
|
8675
|
+
meta: globalKeyMeta(_ckptIdentity),
|
|
8676
|
+
fileContents,
|
|
8313
8677
|
});
|
|
8314
|
-
_ckpt = openCheckpoint(scanRoot, { runKey: _runKey });
|
|
8315
8678
|
for (const r of resumeFindings(_ckpt)) { if (r && r.findings) _ckptPayloads.set(r.file, r.findings); }
|
|
8316
8679
|
_ckptDone = completedFiles(_ckpt);
|
|
8680
|
+
_ckptInvalidated = invalidatedFiles(_ckpt);
|
|
8317
8681
|
} catch (_) { _ckpt = null; }
|
|
8318
8682
|
}
|
|
8319
8683
|
// Replay a checkpointed file's ENTIRE contribution, in the same array order
|
|
@@ -8360,146 +8724,45 @@ function _deterministicFileTimings(timings) {
|
|
|
8360
8724
|
// is what the checkpoint format has to survive. Never set in normal use.
|
|
8361
8725
|
if (_ckptAbortAfter > 0 && _ckptWrites >= _ckptAbortAfter) process.exit(137);
|
|
8362
8726
|
};
|
|
8363
|
-
|
|
8727
|
+
// FR-202 phase 3b (D-0050): opt-in real deadline enforcement for the per-file
|
|
8728
|
+
// cascade via a reused worker pool. Off by default -- the synchronous path above
|
|
8729
|
+
// this loop is completely unchanged when the flag is unset.
|
|
8730
|
+
const _useWorkerCascade = process.env.AGENTIC_SECURITY_WORKER_CASCADE === '1';
|
|
8731
|
+
let _cascadePool = null;
|
|
8732
|
+
if (_useWorkerCascade) {
|
|
8733
|
+
const { createCascadePool } = await import('./pipeline/cascade-worker-pool.js');
|
|
8734
|
+
_cascadePool = createCascadePool({
|
|
8735
|
+
fileContents, scanRoot,
|
|
8736
|
+
modulePath: _resolveEngineModulePathForWorkers(),
|
|
8737
|
+
poolSize: Number(process.env.AGENTIC_SECURITY_WORKER_POOL_SIZE) || 4,
|
|
8738
|
+
});
|
|
8739
|
+
}
|
|
8740
|
+
let i=0;
|
|
8741
|
+
try {
|
|
8742
|
+
for(const p of files){i++;const _ft0=Date.now();setProgress({current:i,total:files.length,file:p.split("/").pop(),phase:"Scanning"});
|
|
8364
8743
|
if(_ckptDone.has(p)&&_ckptReplay(p))continue;
|
|
8365
8744
|
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));
|
|
8745
|
+
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;}
|
|
8746
|
+
let _delta;
|
|
8747
|
+
if (_cascadePool) {
|
|
8748
|
+
const _res = await _cascadePool.runFile(p, c, scanRoot, _detectorErrors, { timeoutMs: _perFileTimeoutMs });
|
|
8749
|
+
if (_res.ok) { _delta = _res.result; }
|
|
8750
|
+
else if (_res.timedOut) { _delta = _emptyCascadeDelta(c); }
|
|
8751
|
+
else { throw new Error(_res.error); }
|
|
8752
|
+
} else {
|
|
8753
|
+
_delta = _runFileCascade(p,c,scanRoot,_detectorErrors);
|
|
8497
8754
|
}
|
|
8755
|
+
fc[p]=_delta.content;pfr[p]=_delta.pfr;
|
|
8756
|
+
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
8757
|
const _ftElapsed=Date.now()-_ft0;
|
|
8499
8758
|
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
8759
|
_fileTimings.push({file:p,ms:_ftElapsed});
|
|
8501
|
-
_ckptRecord(p,_mk,_ftElapsed,
|
|
8502
|
-
|
|
8760
|
+
_ckptRecord(p,_mk,_ftElapsed,_delta.pfr);
|
|
8761
|
+
}catch(_){_fileTimings.push({file:p,ms:Date.now()-_ft0,error:true});}
|
|
8762
|
+
if(i%5===0)await new Promise(r=>setTimeout(r,0));}
|
|
8763
|
+
} finally {
|
|
8764
|
+
if (_cascadePool) await _cascadePool.shutdown();
|
|
8765
|
+
}
|
|
8503
8766
|
// Deserialization-gadget detector runs once with full-tree context (it needs
|
|
8504
8767
|
// manifest contents to know which gadget libs are on the classpath).
|
|
8505
8768
|
try {
|
|
@@ -8507,7 +8770,7 @@ function _deterministicFileTimings(timings) {
|
|
|
8507
8770
|
if (_gadgets.size) {
|
|
8508
8771
|
for (const p of files) {
|
|
8509
8772
|
const c = fc[p]; if (!c) continue;
|
|
8510
|
-
aF.push(...scanDeserializationGadgets(p, c, { gadgets: _gadgets }));
|
|
8773
|
+
aF.push(...(runDetector(_detectorErrors,p,'scanDeserializationGadgets',()=>scanDeserializationGadgets(p, c, { gadgets: _gadgets }))||[]));
|
|
8511
8774
|
}
|
|
8512
8775
|
}
|
|
8513
8776
|
} catch(_) {}
|
|
@@ -8650,17 +8913,17 @@ function _deterministicFileTimings(timings) {
|
|
|
8650
8913
|
// R19 (PRD §5): cross-route BOLA/BFLA over the aggregated route inventory.
|
|
8651
8914
|
// Convention deviation is PROJECT-scoped by design: the convention is a
|
|
8652
8915
|
// property of the codebase, not of one file (see convention-deviation.js).
|
|
8653
|
-
|
|
8654
|
-
|
|
8916
|
+
aF.push(...(runDetector(_detectorErrors,'<project>','scanConventionDeviationProject',()=>scanConventionDeviationProject(fc))||[]));
|
|
8917
|
+
aF.push(...(runDetector(_detectorErrors,'<project>','scanApiBrokenAuthz',()=>scanApiBrokenAuthz(aR))||[]));
|
|
8655
8918
|
// R22 (PRD §5): cross-service edges inferred from code (client call → matched route).
|
|
8656
|
-
|
|
8919
|
+
aF.push(...(runDetector(_detectorErrors,'<project>','scanCrossService',()=>scanCrossService(aR,fc))||[]));
|
|
8657
8920
|
// R21 (PRD §5): RBAC role-tier consistency over the route inventory.
|
|
8658
|
-
|
|
8921
|
+
aF.push(...(runDetector(_detectorErrors,'<project>','scanRbacConsistency',()=>scanRbacConsistency(aR,fc))||[]));
|
|
8659
8922
|
setProgress({current:i,total:files.length,file:"Reachability + guards...",phase:"Linking"});annotateReachability(aF,aR,callGraph,fc);aF.forEach(f=>detectGuardsForFinding(f,fc));
|
|
8660
8923
|
setProgress({current:i,total:files.length,file:"Inferring sanitizers...",phase:"Linking"});const learned=inferSanitizers(fc);applyLearnedSanitizers(aF,learned,fc);
|
|
8661
8924
|
setProgress({current:i,total:files.length,file:"Sanitizer effectiveness...",phase:"Linking"});applySanitizerEffectiveness(aF);
|
|
8662
8925
|
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));
|
|
8926
|
+
setProgress({current:i,total:files.length,file:"Config file cross-ref...",phase:"Linking"});aLogic.push(...(runDetector(_detectorErrors,'<project>','scanConfigFiles',()=>scanConfigFiles(fc))||[]));
|
|
8664
8927
|
setProgress({current:i,total:files.length,file:"OSV vulnerability database...",phase:"SCA"});
|
|
8665
8928
|
const allFileContents={...fc, ...depFileContents};
|
|
8666
8929
|
// PRD F11.4 — malicious install hooks, scanned where package.json actually
|
|
@@ -8675,7 +8938,7 @@ function _deterministicFileTimings(timings) {
|
|
|
8675
8938
|
// A manifest detector belongs on the manifest path.
|
|
8676
8939
|
for (const [mp, mc] of Object.entries(depFileContents)) {
|
|
8677
8940
|
if (!/(?:^|\/)package\.json$/i.test(mp)) continue;
|
|
8678
|
-
|
|
8941
|
+
aF.push(...(runDetector(_detectorErrors,mp,'scanInstallScripts',()=>scanInstallScripts(mp, mc))||[]));
|
|
8679
8942
|
}
|
|
8680
8943
|
const components=parseManifests(allFileContents);
|
|
8681
8944
|
// R8 (PRD §5): OS packages from an extracted container image's package DBs
|
|
@@ -8806,8 +9069,8 @@ function _deterministicFileTimings(timings) {
|
|
|
8806
9069
|
}
|
|
8807
9070
|
}
|
|
8808
9071
|
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
|
-
|
|
9072
|
+
aF.push(...(runDetector(_detectorErrors,'<project>','scanDbTaintCrossFile',()=>scanDbTaintCrossFile(fc))||[]));
|
|
9073
|
+
aF.push(...(runDetector(_detectorErrors,'<project>','scanStoredPromptInjectionCrossFile',()=>scanStoredPromptInjectionCrossFile(fc))||[]));
|
|
8811
9074
|
// Roadmap #8 — tree-sitter sinks for long-tail languages (opt-in,
|
|
8812
9075
|
// AGENTIC_SECURITY_TREE_SITTER=1; degrades to no-op without the optional dep).
|
|
8813
9076
|
if(process.env.AGENTIC_SECURITY_TREE_SITTER==='1'){try{aF.push(...await scanTreeSitterSinks(fc));}catch(_){}}
|
|
@@ -8894,6 +9157,7 @@ function _deterministicFileTimings(timings) {
|
|
|
8894
9157
|
const _deepInCiAllowed = deepInCi === true || process.env.AGENTIC_SECURITY_DEEP_IN_CI === '1';
|
|
8895
9158
|
const _deepEnabled = _deepRequested && (!_inCi || _deepInCiAllowed);
|
|
8896
9159
|
let _deepCallGraph = null;
|
|
9160
|
+
let _deepFailure = null;
|
|
8897
9161
|
if (_deepEnabled) {
|
|
8898
9162
|
const budgetMs = parseInt(process.env.AGENTIC_SECURITY_DEEP_TIMEOUT_MS || '300000', 10);
|
|
8899
9163
|
const t0 = Date.now();
|
|
@@ -8931,7 +9195,10 @@ function _deterministicFileTimings(timings) {
|
|
|
8931
9195
|
aF.push(...irFindings);
|
|
8932
9196
|
} catch (e) {
|
|
8933
9197
|
// Deep mode is best-effort. A parser blowup in one file shouldn't kill
|
|
8934
|
-
// the scan — fall back to the pattern-only result.
|
|
9198
|
+
// the scan — fall back to the pattern-only result. Recorded (not just
|
|
9199
|
+
// swallowed) so scanHealth (FR-206) can report the downgrade instead of
|
|
9200
|
+
// looking identical to a scan where deep mode simply wasn't requested.
|
|
9201
|
+
_deepFailure = String((e && e.message) || e);
|
|
8935
9202
|
}
|
|
8936
9203
|
} else if (_deepRequested && _inCi) {
|
|
8937
9204
|
// Operator asked for deep but we're in CI — emit a non-blocking notice
|
|
@@ -8945,6 +9212,23 @@ function _deterministicFileTimings(timings) {
|
|
|
8945
9212
|
confidence: 1.0,
|
|
8946
9213
|
});
|
|
8947
9214
|
}
|
|
9215
|
+
// FR-205/FR-206: make the deep-analysis decision explicit in machine
|
|
9216
|
+
// output, not just as an occasional info-severity finding. Covers the case
|
|
9217
|
+
// the finding-based notice above does not: deep mode never explicitly
|
|
9218
|
+
// requested at all (the common default-unset-in-CI case), which previously
|
|
9219
|
+
// produced no signal of any kind that assurance had quietly narrowed.
|
|
9220
|
+
const _deepStatus = {
|
|
9221
|
+
requested: _deepRequested,
|
|
9222
|
+
enabled: _deepEnabled,
|
|
9223
|
+
inCi: _inCi,
|
|
9224
|
+
ciOverrideAllowed: _deepInCiAllowed,
|
|
9225
|
+
reason: _deepEnabled
|
|
9226
|
+
? null
|
|
9227
|
+
: (_deepRequested
|
|
9228
|
+
? (_inCi ? 'requested, but running in CI without AGENTIC_SECURITY_DEEP_IN_CI=1' : 'unknown')
|
|
9229
|
+
: (_inCi ? 'not requested (deep analysis defaults to off in CI)' : 'not requested')),
|
|
9230
|
+
failure: _deepFailure,
|
|
9231
|
+
};
|
|
8948
9232
|
// Java SCA enrichment: use deep-mode IR call graph to improve Java function reachability
|
|
8949
9233
|
if (_deepCallGraph) {
|
|
8950
9234
|
try {
|
|
@@ -9087,19 +9371,88 @@ function _deterministicFileTimings(timings) {
|
|
|
9087
9371
|
// Every catch in this block writes into _annotatorErrors so the operator
|
|
9088
9372
|
// can tell "didn't run" from "ran cleanly." The array is surfaced as
|
|
9089
9373
|
// scan.annotatorErrors in the report; an empty array means clean.
|
|
9374
|
+
//
|
|
9375
|
+
// FR-PROV-029 (Finding Provenance PRD): `skipAnnotators` lets a caller skip
|
|
9376
|
+
// this entire ~54-annotator pipeline below. The guard also covers
|
|
9377
|
+
// non-annotator finalization that lives in the same block — entropy-vs-
|
|
9378
|
+
// named secret dedup, orphan classification, supply-chain in-place
|
|
9379
|
+
// filtering, `Object.freeze(finalFindings)`, and closing the resume
|
|
9380
|
+
// checkpoint — so a skipped run leaves all of that undone too; name the
|
|
9381
|
+
// option accordingly if a future caller needs annotators skipped WITHOUT
|
|
9382
|
+
// skipping those steps. predicate-replay.js's `replayAt()` is the only
|
|
9383
|
+
// caller that sets it — it re-runs runFullScan scoped to a historical
|
|
9384
|
+
// commit's blob content purely to recompute `computeStableId()` over the
|
|
9385
|
+
// raw detector output (`scan.findings`/`scan.secrets`); it never reads
|
|
9386
|
+
// anything an annotator sets (verified empirically — see the commit
|
|
9387
|
+
// message). Every binding the pipeline below
|
|
9388
|
+
// populates that the final `return` still references is declared here,
|
|
9389
|
+
// OUTSIDE the guarded block, defaulted to exactly what it was before any
|
|
9390
|
+
// annotator ran. A skipped run returns those fields at their
|
|
9391
|
+
// pre-annotation default instead of throwing a ReferenceError; a normal
|
|
9392
|
+
// (non-skipping) run is byte-for-byte unaffected, since the guarded block
|
|
9393
|
+
// below still assigns the same values to these same bindings — it just no
|
|
9394
|
+
// longer *declares* them, so nothing here changes what a value ends up
|
|
9395
|
+
// being, only where the variable comes into scope.
|
|
9090
9396
|
let _executionProofSummary = null, _vulnHistory = null;
|
|
9091
9397
|
let _logicClaims = null;
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9398
|
+
let _annotatorErrors = [];
|
|
9399
|
+
let _v3 = {};
|
|
9400
|
+
let _privacyIrBacked = null;
|
|
9401
|
+
let _privacyTaxonomyVersion = null;
|
|
9402
|
+
let _privacyFramework = null;
|
|
9403
|
+
let _threatModel = null, _apiContractFindings = [], _sbomDiff = null,
|
|
9404
|
+
_complianceReport = null, _exploitBundles = null, _pqcPlan = null,
|
|
9405
|
+
_licenseGraph = null, _attributions = null, _taxonomySummary = null;
|
|
9406
|
+
let _scanMeta = null;
|
|
9407
|
+
let _entrypointInventory = {};
|
|
9408
|
+
let _rootCauseSweep = null;
|
|
9409
|
+
let _proofCoverage = null;
|
|
9410
|
+
let _coverageLedger = null;
|
|
9411
|
+
let _scanHealth = null;
|
|
9412
|
+
// Task 11 (PRD P0 scope): ruleId backfill for scan.secrets / blameable
|
|
9413
|
+
// scan.logicVulns findings MUST run unconditionally, HERE, outside the
|
|
9414
|
+
// `skipAnnotators` guard below -- not just because the live scan needs it,
|
|
9415
|
+
// but because predicate-replay.js's replayAt() recurses into THIS function
|
|
9416
|
+
// with skipAnnotators:true and recomputes computeStableId() directly on
|
|
9417
|
+
// whatever it finds in the nested scan's own scan.secrets/scan.logicVulns.
|
|
9418
|
+
// If the backfill only ran on the live (skipAnnotators:false) call, the
|
|
9419
|
+
// nested replay scan would compute a DIFFERENT stableId (falling back to
|
|
9420
|
+
// the shared f.cwe -- e.g. every secret type collapsing onto "CWE-798")
|
|
9421
|
+
// than the live scan's already-backfilled finding, so replayAt's
|
|
9422
|
+
// `sid === targetStableId` check would NEVER match -- permanently landing
|
|
9423
|
+
// every secrets/logicVulns finding on status:'partial',
|
|
9424
|
+
// reason:'predicate-never-confirmed-in-candidates'. Caught empirically:
|
|
9425
|
+
// test/fixtures/entropy-fp's AWS-key fixture resolved 'partial' instead of
|
|
9426
|
+
// 'complete' until this moved here from inside the (skipAnnotators-gated)
|
|
9427
|
+
// provenance block further down.
|
|
9428
|
+
//
|
|
9429
|
+
// At this point in the function, aSecrets/aLogic hold every BLAMEABLE
|
|
9430
|
+
// producer's output (scanCredentials/scanEntropySecrets;
|
|
9431
|
+
// scanLogicVulns/scanBusinessLogic/scanMiddlewareOrdering/scanReDoS/
|
|
9432
|
+
// scanRegexReDoS/scanTodosNearSecurity/scanConfigFiles) -- the 3 synthetic
|
|
9433
|
+
// producers (license-policy:/deploy-platform:/stack-playbook:) and
|
|
9434
|
+
// logic-claims.js's ingested claims are pushed LATER, inside the
|
|
9435
|
+
// `skipAnnotators` guard below, so they are never present in a nested
|
|
9436
|
+
// replay scan's aLogic and never need this backfill for replay-matching
|
|
9437
|
+
// purposes. The provenance block further down re-applies this same
|
|
9438
|
+
// idempotent backfill to the full, final `blameableLogic` (which by then
|
|
9439
|
+
// includes logic-claims too) before calling annotateGitProvenance on it.
|
|
9440
|
+
const _slugify = (s) => String(s || 'unknown').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'unknown';
|
|
9441
|
+
for (const f of aSecrets) { if (!f.ruleId) f.ruleId = `secret:${_slugify(f.vuln)}`; }
|
|
9442
|
+
for (const f of aLogic) { if (!f.ruleId) f.ruleId = `logic:${_slugify(f.vuln)}`; }
|
|
9443
|
+
if (!skipAnnotators) {
|
|
9444
|
+
// FR-106 (assurance-hardening PRD): Promise-aware, explicitly awaited at
|
|
9445
|
+
// every one of its ~51 call sites below (previously a sync `try{return
|
|
9446
|
+
// fn()}` let an async callback's rejection escape as an unhandled
|
|
9447
|
+
// rejection, and every caller fired-and-forgot regardless). Logic lives in
|
|
9448
|
+
// pipeline/annotator-runner.js, fault-injection tested directly there
|
|
9449
|
+
// since this project's own annotators are deliberately built never to
|
|
9450
|
+
// throw (posture/CLAUDE.md's "no throwing" convention) — this closure just
|
|
9451
|
+
// supplies the local _annotatorErrors array.
|
|
9452
|
+
const _runAnnotator = (phase, fn) => runAnnotatorAsync(_annotatorErrors, phase, fn);
|
|
9453
|
+
await _runAnnotator('annotateStableIds', () => annotateStableIds(finalFindings));
|
|
9454
|
+
await _runAnnotator("clusterByRootCause", () => { finalFindings = clusterByRootCause(finalFindings); });
|
|
9455
|
+
await _runAnnotator("demoteUnreachable", () => {
|
|
9103
9456
|
demoteUnreachable(finalFindings, { routes: aR });
|
|
9104
9457
|
// `type: 'vulnerable_dep'` findings live in supplyChain, not finalFindings
|
|
9105
9458
|
// (src/sca/CLAUDE.md) — demoteUnreachable's SCA-tier branch needs this
|
|
@@ -9108,14 +9461,14 @@ function _deterministicFileTimings(timings) {
|
|
|
9108
9461
|
});
|
|
9109
9462
|
// Premortem #8: backfill parser/family BEFORE confidence and calibration,
|
|
9110
9463
|
// because both consume those fields and silently no-op when they're null.
|
|
9111
|
-
_runAnnotator("backfillFindingDefaults", () => { backfillFindingDefaults(finalFindings); });
|
|
9112
|
-
_runAnnotator("annotateConfidence", () => { annotateConfidence(finalFindings); });
|
|
9464
|
+
await _runAnnotator("backfillFindingDefaults", () => { backfillFindingDefaults(finalFindings); });
|
|
9465
|
+
await _runAnnotator("annotateConfidence", () => { annotateConfidence(finalFindings); });
|
|
9113
9466
|
// Phase-1 next-gen P1.3 (FR-UX-1, FR-UX-2): Brier-calibrated probability +
|
|
9114
9467
|
// 95% Wilson CI from per-family historical TP/FP. Falls back to null with
|
|
9115
9468
|
// an explicit `calibration_reason` when N is below the calibration floor.
|
|
9116
|
-
_runAnnotator("annotateCalibratedConfidence", () => { annotateCalibratedConfidence(finalFindings, { scanRoot }); });
|
|
9469
|
+
await _runAnnotator("annotateCalibratedConfidence", () => { annotateCalibratedConfidence(finalFindings, { scanRoot }); });
|
|
9117
9470
|
const _projectCtx = (() => { try { return detectProjectContext(fc, aR); } catch { return {}; } })();
|
|
9118
|
-
_runAnnotator("annotateExploitability", () => { annotateExploitability(finalFindings, _projectCtx); });
|
|
9471
|
+
await _runAnnotator("annotateExploitability", () => { annotateExploitability(finalFindings, _projectCtx); });
|
|
9119
9472
|
// Roadmap #6 — proof-gate precision pass. Runs AFTER confidence +
|
|
9120
9473
|
// exploitability so it can demote their tiers, and BEFORE mitigation /
|
|
9121
9474
|
// composite-risk so the demotion flows into the canonical ranking. Default
|
|
@@ -9155,8 +9508,8 @@ function _deterministicFileTimings(timings) {
|
|
|
9155
9508
|
if (f.stableId) unsanitizersOnPath[f.stableId] = undo;
|
|
9156
9509
|
}
|
|
9157
9510
|
}
|
|
9158
|
-
_runAnnotator("applySanitizerGate", () => { applySanitizerGate(finalFindings, { sanitizersOnPath, unsanitizersOnPath }); });
|
|
9159
|
-
_runAnnotator("annotateProofGate", () => { annotateProofGate(finalFindings); });
|
|
9511
|
+
await _runAnnotator("applySanitizerGate", () => { applySanitizerGate(finalFindings, { sanitizersOnPath, unsanitizersOnPath }); });
|
|
9512
|
+
await _runAnnotator("annotateProofGate", () => { annotateProofGate(finalFindings); });
|
|
9160
9513
|
}
|
|
9161
9514
|
// Addition #1 — default falsification pass. Actively tries to DISPROVE each
|
|
9162
9515
|
// taint-style finding by locating a context-matched control on the path, and
|
|
@@ -9166,29 +9519,45 @@ function _deterministicFileTimings(timings) {
|
|
|
9166
9519
|
// default; the LLM tier is only wired when an endpoint is configured. Opt out
|
|
9167
9520
|
// with AGENTIC_SECURITY_NO_FALSIFICATION=1.
|
|
9168
9521
|
if (process.env.AGENTIC_SECURITY_NO_FALSIFICATION !== '1') {
|
|
9169
|
-
_runAnnotator("annotateFalsification", () => { annotateFalsification(finalFindings, fc); });
|
|
9522
|
+
await _runAnnotator("annotateFalsification", () => { annotateFalsification(finalFindings, fc); });
|
|
9170
9523
|
}
|
|
9171
9524
|
// Addition #5 — capability-based model routing. Stamp each finding with the
|
|
9172
9525
|
// model tier a cost-sensitive fixer/triager/PoC subagent should be dispatched
|
|
9173
9526
|
// on for THIS vuln class (crypto/auth/critical → strongest; injection → mid;
|
|
9174
9527
|
// low-sev hardening → cheapest). Advisory metadata consumed at dispatch time.
|
|
9175
|
-
_runAnnotator("annotateDispatchModel", () => {
|
|
9528
|
+
await _runAnnotator("annotateDispatchModel", () => {
|
|
9176
9529
|
for (const f of finalFindings) { try { f.dispatchModel = routeModelForFinding(f).model; } catch { /* advisory only */ } }
|
|
9177
9530
|
});
|
|
9178
9531
|
// v3 next-gen: production-aware context ingest (Pillar 9). Must run BEFORE
|
|
9179
9532
|
// the mitigation composite, persona prioritization, and final why-fired
|
|
9180
9533
|
// 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 }); });
|
|
9534
|
+
await _runAnnotator("annotateWafMitigation", () => { annotateWafMitigation(finalFindings, scanRoot); });
|
|
9535
|
+
await _runAnnotator("annotateAuthMitigation", () => { annotateAuthMitigation(finalFindings, scanRoot); });
|
|
9536
|
+
await _runAnnotator("annotateNetworkMitigation", () => { annotateNetworkMitigation(finalFindings, scanRoot); });
|
|
9537
|
+
await _runAnnotator("annotateTelemetry", () => { annotateTelemetry(finalFindings, scanRoot); });
|
|
9538
|
+
await _runAnnotator("annotateFeatureFlagGating", () => { annotateFeatureFlagGating(finalFindings, fc, { scanRoot }); });
|
|
9186
9539
|
// v3 next-gen: composite mitigation verdict consumes every prod signal above.
|
|
9187
|
-
_runAnnotator("annotateMitigationComposite", () => { annotateMitigationComposite(finalFindings); });
|
|
9540
|
+
await _runAnnotator("annotateMitigationComposite", () => { annotateMitigationComposite(finalFindings); });
|
|
9188
9541
|
// Composite risk score (0..100 derived ordinal). Must run AFTER mitigation
|
|
9189
9542
|
// composite + exploitability + toxicityScore so it sees the final values.
|
|
9190
9543
|
// Used by agents and UI as the canonical sort key for "which finding first."
|
|
9191
|
-
_runAnnotator("annotateCompositeRisk", () => { annotateCompositeRisk(finalFindings); });
|
|
9544
|
+
await _runAnnotator("annotateCompositeRisk", () => { annotateCompositeRisk(finalFindings); });
|
|
9545
|
+
|
|
9546
|
+
// FR-405 (assurance-hardening PRD): null means "privacy analysis never
|
|
9547
|
+
// ran at all" (AGENTIC_SECURITY_NO_PRIVACY=1, or the annotator threw
|
|
9548
|
+
// before setting this) — treated the same as false by the gate below,
|
|
9549
|
+
// since neither case has real IR-backed evidence to offer. Declared at
|
|
9550
|
+
// function scope (now hoisted above the `skipAnnotators` guard, near
|
|
9551
|
+
// _executionProofSummary et al. — same reasoning: FR-PROV-029), because
|
|
9552
|
+
// the annotatePrivacyTaint closure that assigns to it (inside the
|
|
9553
|
+
// AGENTIC_SECURITY_NO_INTEGRATION block below) runs and exits before that
|
|
9554
|
+
// block closes — a block-scoped `let` inside that if-statement would be
|
|
9555
|
+
// unreachable by the later assessPrivacyFramework call and the final
|
|
9556
|
+
// return, both of which are outside the block.
|
|
9557
|
+
//
|
|
9558
|
+
// FR-402: _privacyTaxonomyVersion (which taxonomy version actually
|
|
9559
|
+
// classified this scan's fields) has the same scoping constraint as
|
|
9560
|
+
// _privacyIrBacked directly above (D-0011) and is hoisted alongside it.
|
|
9192
9561
|
|
|
9193
9562
|
// ── World-class integration block ─────────────────────────────────────
|
|
9194
9563
|
// Each annotator is opt-in via env var and try/catch wrapped. They run
|
|
@@ -9198,46 +9567,89 @@ function _deterministicFileTimings(timings) {
|
|
|
9198
9567
|
// Cross-service taint annotation reads .agentic-security/services.yml
|
|
9199
9568
|
// and bumps severity on cross-service-reachable findings.
|
|
9200
9569
|
if (process.env.AGENTIC_SECURITY_NO_CROSS_SERVICE !== '1') {
|
|
9201
|
-
_runAnnotator("runCrossServiceTaint", () => { runCrossServiceTaint(scanRoot, finalFindings); });
|
|
9570
|
+
await _runAnnotator("runCrossServiceTaint", () => { runCrossServiceTaint(scanRoot, finalFindings); });
|
|
9202
9571
|
}
|
|
9203
9572
|
// Runtime correlation: demotes findings whose paths were unobserved
|
|
9204
9573
|
// in production eBPF traces (when a trace file is present).
|
|
9205
9574
|
if (process.env.AGENTIC_SECURITY_NO_RUNTIME_CORRELATION !== '1') {
|
|
9206
|
-
_runAnnotator("annotateRuntimeCorrelation", async () => { await annotateRuntimeCorrelation(scanRoot, finalFindings); });
|
|
9575
|
+
await _runAnnotator("annotateRuntimeCorrelation", async () => { await annotateRuntimeCorrelation(scanRoot, finalFindings); });
|
|
9207
9576
|
}
|
|
9208
9577
|
// Triage learning: applies per-(project, family, file-glob) calibration
|
|
9209
9578
|
// from prior wont-fix / false-positive decisions.
|
|
9210
9579
|
if (process.env.AGENTIC_SECURITY_NO_TRIAGE_LEARNING !== '1') {
|
|
9211
|
-
_runAnnotator("applyLearnedCalibration", () => { applyLearnedCalibration(scanRoot, finalFindings); });
|
|
9580
|
+
await _runAnnotator("applyLearnedCalibration", () => { applyLearnedCalibration(scanRoot, finalFindings); });
|
|
9212
9581
|
}
|
|
9213
9582
|
// Formal verification: CBMC for C/C++, MIRI for Rust. Opt-in via
|
|
9214
9583
|
// AGENTIC_SECURITY_FORMAL=1 (off by default — requires external tools).
|
|
9215
9584
|
if (process.env.AGENTIC_SECURITY_FORMAL === '1') {
|
|
9216
|
-
_runAnnotator("annotateFormalVerification", async () => { await annotateFormalVerification(finalFindings, fc, {}); });
|
|
9585
|
+
await _runAnnotator("annotateFormalVerification", async () => { await annotateFormalVerification(finalFindings, fc, {}); });
|
|
9217
9586
|
}
|
|
9218
9587
|
// SMT path feasibility: Z3-backed proof of reachability. Opt-in via
|
|
9219
9588
|
// AGENTIC_SECURITY_SMT_FEASIBILITY=1.
|
|
9220
9589
|
if (process.env.AGENTIC_SECURITY_SMT_FEASIBILITY === '1') {
|
|
9221
|
-
_runAnnotator("annotatePathFeasibility", async () => { await annotatePathFeasibility(finalFindings, {}); });
|
|
9590
|
+
await _runAnnotator("annotatePathFeasibility", async () => { await annotatePathFeasibility(finalFindings, {}); });
|
|
9222
9591
|
}
|
|
9223
9592
|
// Privacy / PII taint: emits pii-exposure findings + DPIA artifact.
|
|
9593
|
+
// FR-405 (assurance-hardening PRD): _privacyIrBacked is declared at
|
|
9594
|
+
// function scope above the AGENTIC_SECURITY_NO_INTEGRATION block, not
|
|
9595
|
+
// here — this closure runs and exits before that block's closing brace,
|
|
9596
|
+
// so a block-scoped declaration would be unreachable from the later
|
|
9597
|
+
// assessPrivacyFramework call and the final return.
|
|
9224
9598
|
if (process.env.AGENTIC_SECURITY_NO_PRIVACY !== '1') {
|
|
9225
|
-
_runAnnotator("annotatePrivacyTaint", () => {
|
|
9226
|
-
//
|
|
9227
|
-
//
|
|
9228
|
-
//
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9599
|
+
await _runAnnotator("annotatePrivacyTaint", () => {
|
|
9600
|
+
// FR-401 (assurance-hardening PRD, A-06): decls/calls used to be
|
|
9601
|
+
// hardcoded empty for every file (the actual root cause, per
|
|
9602
|
+
// decisions.md D-0003, was a missing adapter reconciling
|
|
9603
|
+
// privacy-taint.js's flat per-file shape with the real Layer-1 IR's
|
|
9604
|
+
// per-function/CFG shape — not a missing capability in the IR or
|
|
9605
|
+
// taint engine themselves; see privacy/ir-adapter.js).
|
|
9606
|
+
//
|
|
9607
|
+
// Reuse _sharedIR when deep mode already built it (free — no
|
|
9608
|
+
// second parse pass). When deep mode is OFF (the common default
|
|
9609
|
+
// path), do NOT force a fresh IR build here: buildProjectIR parses
|
|
9610
|
+
// every file and is exactly the cost deep mode is opt-in for
|
|
9611
|
+
// (NFR: "no more than 15% ... overhead ... excluding newly enabled
|
|
9612
|
+
// deep analysis"). In that case privacy analysis still runs, but
|
|
9613
|
+
// honestly marked as not IR-backed (irBacked:false below) rather
|
|
9614
|
+
// than silently claiming the same coverage as the deep-mode path —
|
|
9615
|
+
// this is the signal FR-405's "missing capability -> not_assessed,
|
|
9616
|
+
// never satisfied" contract needs to key off, not yet wired into
|
|
9617
|
+
// privacy-framework.js's own bucketing in this cycle (separate,
|
|
9618
|
+
// later FR-405 work).
|
|
9619
|
+
const irBacked = !!(_sharedIR && _sharedIR.perFile);
|
|
9620
|
+
const adaptedIR = irBacked
|
|
9621
|
+
? adaptIRForPrivacyTaint(_sharedIR.perFile, fc, storedRegistry)
|
|
9622
|
+
: new Map(Object.entries(fc || {})
|
|
9623
|
+
.filter(([, content]) => typeof content === 'string')
|
|
9624
|
+
.map(([fp, content]) => [fp, { _content: content, decls: [], calls: [] }]));
|
|
9625
|
+
// FR-402: pass scanRoot so an operator's .agentic-security/
|
|
9626
|
+
// privacy-taxonomy.json (if any) is actually loaded on a real scan.
|
|
9627
|
+
const r = annotatePrivacyTaint(adaptedIR, { scanRoot });
|
|
9628
|
+
r.irBacked = irBacked;
|
|
9629
|
+
_privacyIrBacked = irBacked;
|
|
9630
|
+
_privacyTaxonomyVersion = r.taxonomyVersion || null;
|
|
9631
|
+
// FR-102 (assurance-hardening PRD): route through the registered-
|
|
9632
|
+
// producer collector rather than a bare push. r is already computed
|
|
9633
|
+
// above (this call also drives the DPIA artifact below), so the
|
|
9634
|
+
// thunk just hands back what's already there.
|
|
9635
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'privacy-taint', () => (r && Array.isArray(r.findings)) ? r.findings : []);
|
|
9236
9636
|
// Persist the DPIA scaffold for compliance review.
|
|
9237
9637
|
if (r && r.piiFields) {
|
|
9238
9638
|
try {
|
|
9239
|
-
|
|
9639
|
+
// FR-407: governance fields are loaded once and threaded into
|
|
9640
|
+
// both the DPIA (per-class subsection) and the RoPA artifact
|
|
9641
|
+
// (the full register) so an operator only maintains one config
|
|
9642
|
+
// file for both.
|
|
9643
|
+
const governanceConfig = loadPrivacyGovernanceConfig(scanRoot);
|
|
9644
|
+
const dpia = emitDpiaArtifact(r.piiFields, r.findings || [], { policyExemptions: r.policyExemptions || [], governanceConfig });
|
|
9240
9645
|
_safeWriteState(_statePath(scanRoot, 'dpia.md'), dpia);
|
|
9646
|
+
const ropa = emitRopaArtifact(r.piiFields, governanceConfig);
|
|
9647
|
+
_safeWriteState(_statePath(scanRoot, 'ropa.md'), ropa);
|
|
9648
|
+
// FR-406: code-derived data inventory + flow graph, built from
|
|
9649
|
+
// the SAME findings/exemptions already computed above.
|
|
9650
|
+
const inventory = buildDataInventory(r.piiFields, r.findings || [], r.policyExemptions || []);
|
|
9651
|
+
_safeWriteState(_statePath(scanRoot, 'data-inventory.json'), emitDataInventoryArtifact(inventory));
|
|
9652
|
+
_safeWriteState(_statePath(scanRoot, 'data-flow-graph.md'), emitDataFlowGraph(inventory));
|
|
9241
9653
|
} catch (_) {}
|
|
9242
9654
|
}
|
|
9243
9655
|
});
|
|
@@ -9246,80 +9658,80 @@ function _deterministicFileTimings(timings) {
|
|
|
9246
9658
|
// ATLAS, D3FEND, kill-chain stage, and CAPEC IDs so downstream SIEM /
|
|
9247
9659
|
// SOAR systems can correlate with existing detection rules.
|
|
9248
9660
|
if (process.env.AGENTIC_SECURITY_NO_ATTACK_TAX !== '1') {
|
|
9249
|
-
_runAnnotator("annotateAttackTaxonomy", () => { annotateAttackTaxonomy(finalFindings); });
|
|
9661
|
+
await _runAnnotator("annotateAttackTaxonomy", () => { annotateAttackTaxonomy(finalFindings); });
|
|
9250
9662
|
}
|
|
9251
9663
|
// Triage memory — demote findings whose (family, dir) bucket was
|
|
9252
9664
|
// previously marked wont-fix or false-positive in this project.
|
|
9253
9665
|
if (process.env.AGENTIC_SECURITY_NO_TRIAGE_MEMORY !== '1') {
|
|
9254
|
-
_runAnnotator("suppressByPastDecisions", () => { suppressByPastDecisions(scanRoot, finalFindings); });
|
|
9666
|
+
await _runAnnotator("suppressByPastDecisions", () => { suppressByPastDecisions(scanRoot, finalFindings); });
|
|
9255
9667
|
}
|
|
9256
9668
|
// Intent-aware FP suppression — demote findings on files marked as
|
|
9257
9669
|
// intentionally vulnerable (sandbox/CTF/tutorial/example/etc.).
|
|
9258
9670
|
if (process.env.AGENTIC_SECURITY_NO_INTENT_CTX !== '1') {
|
|
9259
|
-
_runAnnotator("suppressByIntent", () => { suppressByIntent(scanRoot, finalFindings); });
|
|
9671
|
+
await _runAnnotator("suppressByIntent", () => { suppressByIntent(scanRoot, finalFindings); });
|
|
9260
9672
|
}
|
|
9261
9673
|
// Git history — stamp each finding with introducedBy / introducedIn /
|
|
9262
9674
|
// originatingPrompt by running `git blame` on the finding's line.
|
|
9263
9675
|
if (process.env.AGENTIC_SECURITY_NO_GIT_HISTORY !== '1') {
|
|
9264
|
-
_runAnnotator("annotateGitHistory", () => { annotateGitHistory(scanRoot, finalFindings); });
|
|
9676
|
+
await _runAnnotator("annotateGitHistory", () => { annotateGitHistory(scanRoot, finalFindings); });
|
|
9265
9677
|
}
|
|
9266
9678
|
// Threat-model grounding — bump severity on crown-jewels, demote
|
|
9267
9679
|
// out-of-scope, tag compliance regimes, stamp attacker profile.
|
|
9268
9680
|
if (process.env.AGENTIC_SECURITY_NO_THREAT_MODEL_GROUNDING !== '1') {
|
|
9269
|
-
_runAnnotator("applyThreatModel", () => { applyThreatModel(scanRoot, finalFindings); });
|
|
9681
|
+
await _runAnnotator("applyThreatModel", () => { applyThreatModel(scanRoot, finalFindings); });
|
|
9270
9682
|
}
|
|
9271
9683
|
// Cross-repo pattern propagation — surface sibling-repo fixes and
|
|
9272
9684
|
// triage decisions for the same family from this developer's history.
|
|
9273
9685
|
if (process.env.AGENTIC_SECURITY_NO_CROSS_REPO !== '1') {
|
|
9274
|
-
_runAnnotator("annotateCrossRepoSignals", () => { annotateCrossRepoSignals(scanRoot, finalFindings); });
|
|
9686
|
+
await _runAnnotator("annotateCrossRepoSignals", () => { annotateCrossRepoSignals(scanRoot, finalFindings); });
|
|
9275
9687
|
}
|
|
9276
9688
|
// Risk-in-dollars — combine EPSS + crown-jewel + reachability into an
|
|
9277
9689
|
// expected-value-of-exploitation USD figure per finding.
|
|
9278
9690
|
if (process.env.AGENTIC_SECURITY_NO_RISK_DOLLARS !== '1') {
|
|
9279
|
-
_runAnnotator("annotateRiskDollars", () => { annotateRiskDollars(scanRoot, finalFindings); });
|
|
9691
|
+
await _runAnnotator("annotateRiskDollars", () => { annotateRiskDollars(scanRoot, finalFindings); });
|
|
9280
9692
|
}
|
|
9281
9693
|
// Time-to-fix — estimate engineering hours per finding.
|
|
9282
9694
|
if (process.env.AGENTIC_SECURITY_NO_TIME_TO_FIX !== '1') {
|
|
9283
|
-
_runAnnotator("annotateTimeToFix", () => { annotateTimeToFix(scanRoot, finalFindings); });
|
|
9695
|
+
await _runAnnotator("annotateTimeToFix", () => { annotateTimeToFix(scanRoot, finalFindings); });
|
|
9284
9696
|
}
|
|
9285
9697
|
}
|
|
9286
9698
|
// v3 next-gen: crown-jewel mapping (FR-PROD-5) — score each file/finding by
|
|
9287
9699
|
// business impact. Must run before persona prioritization (which uses it).
|
|
9288
|
-
_runAnnotator("annotateCrownJewelScores", () => { annotateCrownJewelScores(finalFindings, fc); });
|
|
9700
|
+
await _runAnnotator("annotateCrownJewelScores", () => { annotateCrownJewelScores(finalFindings, fc); });
|
|
9289
9701
|
// v3 next-gen: clone clusters (FR-SEM-8) + emit clone-outlier infos.
|
|
9290
|
-
_runAnnotator("annotateCloneClusters", () => { annotateCloneClusters(finalFindings); });
|
|
9702
|
+
await _runAnnotator("annotateCloneClusters", () => { annotateCloneClusters(finalFindings); });
|
|
9291
9703
|
try {
|
|
9292
9704
|
const outliers = findCloneOutliers(finalFindings);
|
|
9293
9705
|
if (outliers && outliers.length) finalFindings.push(...outliers);
|
|
9294
9706
|
} catch(_) {}
|
|
9295
9707
|
// v3 next-gen: AI-generated-code fingerprint (FR-LEARN-10). Property bag tag.
|
|
9296
|
-
_runAnnotator("annotateAiProvenance", () => { annotateAiProvenance(finalFindings, fc); });
|
|
9708
|
+
await _runAnnotator("annotateAiProvenance", () => { annotateAiProvenance(finalFindings, fc); });
|
|
9297
9709
|
// v3 next-gen: whole-program type narrowing (FR-SEM-10) — heuristic
|
|
9298
9710
|
// confidence dampener on findings rooted in functions whose callers all
|
|
9299
9711
|
// pass narrowly-typed values.
|
|
9300
|
-
_runAnnotator("annotateTypeNarrowing", () => { annotateTypeNarrowing(finalFindings, fc); });
|
|
9712
|
+
await _runAnnotator("annotateTypeNarrowing", () => { annotateTypeNarrowing(finalFindings, fc); });
|
|
9301
9713
|
// v3 next-gen: STRIDE classification (FR-LOGIC-10).
|
|
9302
|
-
_runAnnotator("annotateStrideCategory", () => { annotateStrideCategory(finalFindings); });
|
|
9714
|
+
await _runAnnotator("annotateStrideCategory", () => { annotateStrideCategory(finalFindings); });
|
|
9303
9715
|
// v3 next-gen: per-attacker-persona score matrix (FR-ADV-2). Must run AFTER
|
|
9304
9716
|
// crown-jewels + mitigation composite so it sees those signals.
|
|
9305
|
-
_runAnnotator("annotatePersonaScores", () => { annotatePersonaScores(finalFindings); });
|
|
9717
|
+
await _runAnnotator("annotatePersonaScores", () => { annotatePersonaScores(finalFindings); });
|
|
9306
9718
|
// v3 next-gen: SCA reverse-blast-radius enrichment (FR-ADV-5). Annotates
|
|
9307
9719
|
// SCA findings (package-name-keyed) — must run against supplyChain, not
|
|
9308
9720
|
// finalFindings (SAST), which has no package-name field at all.
|
|
9309
|
-
_runAnnotator("annotateScaReverseBlast", () => { annotateScaReverseBlast(supplyChain, fc); });
|
|
9721
|
+
await _runAnnotator("annotateScaReverseBlast", () => { annotateScaReverseBlast(supplyChain, fc); });
|
|
9310
9722
|
// v3 next-gen: bug-bounty payout prediction (FR-ADV-3). Composes with the
|
|
9311
9723
|
// mitigation composite — gated/unreachable findings get the bounty scaled
|
|
9312
9724
|
// down rather than zeroed.
|
|
9313
|
-
_runAnnotator("annotateBountyPrediction", () => { annotateBountyPrediction(finalFindings); });
|
|
9725
|
+
await _runAnnotator("annotateBountyPrediction", () => { annotateBountyPrediction(finalFindings); });
|
|
9314
9726
|
// v3 next-gen: attack-playbook annotation (FR-ADV-4). Only for high+ findings.
|
|
9315
|
-
_runAnnotator("annotateAttackPlaybooks", () => { annotateAttackPlaybooks(finalFindings); });
|
|
9727
|
+
await _runAnnotator("annotateAttackPlaybooks", () => { annotateAttackPlaybooks(finalFindings); });
|
|
9316
9728
|
// Phase-1 next-gen P1.1 (FR-VER-2): attach a runnable PoC to each finding
|
|
9317
9729
|
// when a CWE template covers it. Findings without coverage get f.poc=null.
|
|
9318
9730
|
// Premortem #12: pass fileContents so PoC param-key inference can re-read
|
|
9319
9731
|
// the actual handler line when detector snippets are misattributed.
|
|
9320
|
-
_runAnnotator("annotatePocs", () => { annotatePocs(finalFindings, { routes: aR, fileContents: fc }); });
|
|
9732
|
+
await _runAnnotator("annotatePocs", () => { annotatePocs(finalFindings, { routes: aR, fileContents: fc }); });
|
|
9321
9733
|
// FR-VER-3: regression-test generator (builds on the PoC artifact).
|
|
9322
|
-
_runAnnotator("annotateRegressionTests", () => { annotateRegressionTests(finalFindings); });
|
|
9734
|
+
await _runAnnotator("annotateRegressionTests", () => { annotateRegressionTests(finalFindings); });
|
|
9323
9735
|
// R2 — execution proof. Synthesizes a SANDBOX-RUNNABLE PoC (the HTTP PoCs
|
|
9324
9736
|
// above need a live server, so they can never be executed by the prover)
|
|
9325
9737
|
// and lets R1's sandbox decide the tier. Opt-in via AGENTIC_SECURITY_PROVE=1
|
|
@@ -9332,7 +9744,7 @@ function _deterministicFileTimings(timings) {
|
|
|
9332
9744
|
// Never a finding and never a severity change: those bugs are fixed, and a
|
|
9333
9745
|
// historical fix is not evidence of a present defect. Opt-in because it
|
|
9334
9746
|
// shells out to git over up to 500 commits.
|
|
9335
|
-
_runAnnotator('annotateHistoricalRisk', () => {
|
|
9747
|
+
await _runAnnotator('annotateHistoricalRisk', () => {
|
|
9336
9748
|
if (process.env.AGENTIC_SECURITY_ARCHAEOLOGY !== '1' || !scanRoot) return;
|
|
9337
9749
|
_vulnHistory = mineVulnHistory(scanRoot);
|
|
9338
9750
|
annotateHistoricalRisk(finalFindings, _vulnHistory);
|
|
@@ -9346,65 +9758,38 @@ function _deterministicFileTimings(timings) {
|
|
|
9346
9758
|
// verifier verdict — verified-exploit (live PoC ran), verified-by-llm,
|
|
9347
9759
|
// verified-sanitizer-absence, unverified-by-design, or cannot-verify.
|
|
9348
9760
|
// Fail-closed: any error → cannot-verify, never a silent drop.
|
|
9349
|
-
_runAnnotator("annotateVerifierVerdicts", () => { annotateVerifierVerdicts(finalFindings, { fileContents: fc }); });
|
|
9761
|
+
await _runAnnotator("annotateVerifierVerdicts", () => { annotateVerifierVerdicts(finalFindings, { fileContents: fc }); });
|
|
9350
9762
|
// Cross-language taint (Sentinel-parity FR-DET-3) — five boundary types:
|
|
9351
9763
|
// HTTP/REST via OpenAPI, gRPC via .proto, GraphQL via SDL, SQL/ORM
|
|
9352
9764
|
// round-trip, and IaC → application-code reachability (FR-DET-4).
|
|
9353
9765
|
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(_) {}
|
|
9766
|
+
// FR-101/FR-102 (assurance-hardening PRD): each of these 11 producers now
|
|
9767
|
+
// goes through the registered-producer collector (pipeline/producer-
|
|
9768
|
+
// collector.js) instead of a bare try/catch + push — a thrown exception
|
|
9769
|
+
// becomes a diagnostic in _annotatorErrors (previously silently swallowed
|
|
9770
|
+
// by `catch(_) {}` with no trace anywhere) and the collector stamps
|
|
9771
|
+
// producerId provenance on each finding.
|
|
9772
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'cross-lang-openapi', () => scanCrossLangOpenAPI(_allXlangFiles, finalFindings));
|
|
9773
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'cross-lang-grpc', () => scanCrossLangGrpc(_allXlangFiles, finalFindings));
|
|
9774
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'cross-lang-graphql', () => scanCrossLangGraphql(_allXlangFiles, finalFindings));
|
|
9775
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'cross-lang-orm', () => scanCrossLangOrm(_allXlangFiles, finalFindings));
|
|
9370
9776
|
// Phase-1 next-gen P1.5 (FR-XSAT-4): cross-language taint via Kafka, SQS,
|
|
9371
9777
|
// 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(_) {}
|
|
9778
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'cross-lang-queues', () => scanCrossLangQueues(_allXlangFiles, finalFindings));
|
|
9779
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'iac-reachability', () => scanIacReachability(_allXlangFiles, finalFindings));
|
|
9380
9780
|
// 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(_) {}
|
|
9781
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'iam-policy', () => scanIamPolicies(_allXlangFiles, finalFindings));
|
|
9385
9782
|
// 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(_) {}
|
|
9783
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'container-runtime', () => scanContainerRuntime(_allXlangFiles));
|
|
9390
9784
|
// 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(_) {}
|
|
9785
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'business-logic-v2', () => scanBusinessLogicV2(_allXlangFiles));
|
|
9395
9786
|
// v3 next-gen: specification-mining drift detector (FR-LOGIC-8). Emits
|
|
9396
9787
|
// findings for function-name-vs-body mismatches. Low confidence by default;
|
|
9397
9788
|
// active-learning loop tunes per project.
|
|
9398
|
-
|
|
9399
|
-
const sm = scanSpecificationDrift(_allXlangFiles);
|
|
9400
|
-
if (sm && sm.length) finalFindings.push(...sm);
|
|
9401
|
-
} catch(_) {}
|
|
9789
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'specification-drift', () => scanSpecificationDrift(_allXlangFiles));
|
|
9402
9790
|
// v3 next-gen: bounded concurrency-bug detector (FR-SEM-9). Heuristic only;
|
|
9403
9791
|
// 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(_) {}
|
|
9792
|
+
collectProducerResult(finalFindings, _annotatorErrors, 'concurrency', () => scanConcurrency(_allXlangFiles));
|
|
9408
9793
|
// FR-LOGIC-6: LLM-driven flow narration (template fallback when no LLM endpoint).
|
|
9409
9794
|
try { await annotateNarration(finalFindings); }
|
|
9410
9795
|
catch (e) { _annotatorErrors.push({ phase: 'annotateNarration', err: String((e && e.message) || e) }); }
|
|
@@ -9671,14 +10056,14 @@ function _deterministicFileTimings(timings) {
|
|
|
9671
10056
|
classifyOrphans(aSrc,aSink,finalFindings,fc);
|
|
9672
10057
|
// v3 next-gen: capture scan-level reports (counterfactual, threat model,
|
|
9673
10058
|
// trust-boundary diagram, calibration-drift alarms). All best-effort.
|
|
9674
|
-
|
|
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); });
|
|
10059
|
+
// (_v3 is hoisted above the `skipAnnotators` guard — FR-PROV-029.)
|
|
10060
|
+
await _runAnnotator("_v3.counterfactual", () => { _v3.counterfactual = runCounterfactual(finalFindings, fc); });
|
|
10061
|
+
await _runAnnotator("_v3.threatModel", () => { _v3.threatModel = buildThreatModel(finalFindings, fc); });
|
|
10062
|
+
await _runAnnotator("_v3.trustBoundaryDiagram", () => { _v3.trustBoundaryDiagram = buildTrustBoundaryDiagram(finalFindings, fc); });
|
|
10063
|
+
await _runAnnotator("_v3.calibrationDrift", () => { _v3.calibrationDrift = computeCalibrationDrift(scanRoot); });
|
|
9679
10064
|
// v3 next-gen: why-fired provenance is captured LAST so it reflects the
|
|
9680
10065
|
// final state of each finding after every other annotator has run.
|
|
9681
|
-
_runAnnotator("annotateWhyFired", () => { annotateWhyFired(finalFindings, {}); });
|
|
10066
|
+
await _runAnnotator("annotateWhyFired", () => { annotateWhyFired(finalFindings, {}); });
|
|
9682
10067
|
// SCA-SAST correlation: link SAST findings to SCA vulnerable packages
|
|
9683
10068
|
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
10069
|
// Multi-sink chain detection: group findings by source variable
|
|
@@ -9689,10 +10074,9 @@ function _deterministicFileTimings(timings) {
|
|
|
9689
10074
|
// Each is opt-in via env var. They produce machine-readable artifacts
|
|
9690
10075
|
// (threat-model.json/.md, dpia.md, compliance-evidence.json/.md,
|
|
9691
10076
|
// sbom-history/<sha>.json, exploit-bundles/) under .agentic-security/.
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
_licenseGraph = null, _attributions = null, _taxonomySummary = null;
|
|
10077
|
+
// (_privacyFramework/_threatModel/_apiContractFindings/_sbomDiff/
|
|
10078
|
+
// _complianceReport/_exploitBundles/_pqcPlan/_licenseGraph/_attributions/
|
|
10079
|
+
// _taxonomySummary are hoisted above the `skipAnnotators` guard — FR-PROV-029.)
|
|
9696
10080
|
if (process.env.AGENTIC_SECURITY_NO_INTEGRATION !== '1') {
|
|
9697
10081
|
// Threat model — STRIDE + entities + attack trees rooted in findings.
|
|
9698
10082
|
if (process.env.AGENTIC_SECURITY_NO_THREAT_MODEL !== '1') {
|
|
@@ -9787,6 +10171,12 @@ function _deterministicFileTimings(timings) {
|
|
|
9787
10171
|
// filesScanned feeds the vacuous-satisfaction guard: a clean signal
|
|
9788
10172
|
// from a run that read no files is not evidence of compliance.
|
|
9789
10173
|
filesScanned: files.length,
|
|
10174
|
+
// FR-405: feeds the SAME guard, specifically for controls whose
|
|
10175
|
+
// only mapped signal comes from privacy-taint — a non-IR-backed
|
|
10176
|
+
// run (deep mode off, the common default) still emits a "no
|
|
10177
|
+
// findings" pass, which is not real evidence for those controls
|
|
10178
|
+
// even though the scan otherwise examined real files.
|
|
10179
|
+
privacyIrBacked: _privacyIrBacked,
|
|
9790
10180
|
});
|
|
9791
10181
|
if (_privacyFramework) persistPrivacyFramework(scanRoot, _privacyFramework);
|
|
9792
10182
|
if (_privacyFramework && process.env.AGENTIC_SECURITY_PRIVACY_FRAMEWORK === '1') {
|
|
@@ -9812,6 +10202,34 @@ function _deterministicFileTimings(timings) {
|
|
|
9812
10202
|
}
|
|
9813
10203
|
}
|
|
9814
10204
|
|
|
10205
|
+
// FR-103 (assurance-hardening PRD): canonical enrichment completion pass.
|
|
10206
|
+
// This is the LAST point in the function where a finding could still be
|
|
10207
|
+
// missing stableId/confidence/calibration/exploitability because it was
|
|
10208
|
+
// appended by a late producer (see pipeline/enrichment-completion.js's
|
|
10209
|
+
// header for why this is a gap-filling pass rather than relocating ~19
|
|
10210
|
+
// append sites earlier in this function). Must run after every push site
|
|
10211
|
+
// above and before the freeze immediately below.
|
|
10212
|
+
const _projectCtxForCompletion = typeof _projectCtx !== 'undefined' ? _projectCtx : {};
|
|
10213
|
+
completeEnrichment(finalFindings, { scanRoot, projectCtx: _projectCtxForCompletion });
|
|
10214
|
+
|
|
10215
|
+
// FR-104 (assurance-hardening PRD): freeze the finding collection.
|
|
10216
|
+
// Object.freeze on an array blocks push/pop/splice/length changes (this
|
|
10217
|
+
// module and every caller downstream run under ESM strict mode, so a
|
|
10218
|
+
// violation THROWS a TypeError rather than silently no-op'ing) — no
|
|
10219
|
+
// producer may append after this point. This is a SHALLOW freeze: the
|
|
10220
|
+
// remaining read/annotate calls below (countUnmodeledSinkCandidates,
|
|
10221
|
+
// buildEntrypointInventory, annotateRelevance, sweepRootCauses,
|
|
10222
|
+
// proofCoverage) either only read `finalFindings` or, for annotateRelevance,
|
|
10223
|
+
// legitimately mutate FIELDS on individual finding objects — verified by
|
|
10224
|
+
// reading each function's body (none push/splice/pop/shift/unshift onto
|
|
10225
|
+
// the array they're handed). Field mutation is annotation, not production,
|
|
10226
|
+
// and annotateRelevance running this late is an established, deliberate
|
|
10227
|
+
// part of this codebase's architecture (its own header: "runs after every
|
|
10228
|
+
// finding has been appended... so nothing escapes annotation"). What
|
|
10229
|
+
// freezing forecloses is exactly what FR-104 asks for: no MORE findings
|
|
10230
|
+
// can be added after this point.
|
|
10231
|
+
Object.freeze(finalFindings);
|
|
10232
|
+
|
|
9815
10233
|
// Coverage-honesty report (#5 + #6): per-language analysis tier (IR-taint vs
|
|
9816
10234
|
// pattern-only), dense/large/timeout skips, and unmodeled-sink candidates.
|
|
9817
10235
|
let _analysisTier = null, _unmodeledSinks = null;
|
|
@@ -9826,19 +10244,314 @@ function _deterministicFileTimings(timings) {
|
|
|
9826
10244
|
// seen when only N-of-those-candidates were actually analyzed.
|
|
9827
10245
|
// checkpoint.total intentionally keeps files.length — that field means the
|
|
9828
10246
|
// full candidate set for resume bookkeeping, a different, correct meaning.
|
|
9829
|
-
|
|
10247
|
+
_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
10248
|
// R8: the scan completed, so the checkpoint has been fully consumed — remove
|
|
9831
10249
|
// it. Anything that threw before this point leaves it in place to resume from.
|
|
9832
10250
|
try { closeCheckpoint(_ckpt, { complete: true }); } catch (_) {}
|
|
10251
|
+
// Finding Provenance (M0/M1) — attaches `finding.findingProvenance`.
|
|
10252
|
+
//
|
|
10253
|
+
// Placed HERE, not immediately after the SCA/multi-sink correlation blocks
|
|
10254
|
+
// where the plan first put it, for the same reason `annotateRelevance` below
|
|
10255
|
+
// runs this late: several post-scan artifact emitters between those blocks
|
|
10256
|
+
// and this point still PUSH findings (`runApiContractScan`, `runSbomDiff`,
|
|
10257
|
+
// the cross-language chain passes). Annotating before them would have left
|
|
10258
|
+
// every finding they produce with no `findingProvenance` at all, which is
|
|
10259
|
+
// precisely the "absent field" state the coordinator's terminal-status
|
|
10260
|
+
// guarantee exists to make unreachable. Running after `Object.freeze` above
|
|
10261
|
+
// is safe and is in fact the point: the finding SET can no longer change, and
|
|
10262
|
+
// a shallow freeze still permits annotating FIELDS on the individual finding
|
|
10263
|
+
// objects. This annotator never appends or drops a finding.
|
|
10264
|
+
if (provenance !== false) {
|
|
10265
|
+
// Task 11 (PRD P0 scope): real origin resolution for scan.secrets and
|
|
10266
|
+
// blameable scan.logicVulns findings. Both channels were previously
|
|
10267
|
+
// stamped an unconditional not_available (see the backstop loop below) —
|
|
10268
|
+
// not because computeStableId can't handle them (its ruleId() fallback
|
|
10269
|
+
// chain already tolerates a missing f.ruleId), but because nobody
|
|
10270
|
+
// backfilled a real per-pattern ruleId and nobody called
|
|
10271
|
+
// annotateGitProvenance on these channels at all. scan.secrets findings
|
|
10272
|
+
// set neither ruleId nor family nor parser, and every secret type shares
|
|
10273
|
+
// the same fixed f.cwe ("CWE-798"), so without a per-pattern backfill
|
|
10274
|
+
// every secret in a scan would collide onto ONE stableId.
|
|
10275
|
+
//
|
|
10276
|
+
// scan.logicVulns is not one detector's output — three of its ~9
|
|
10277
|
+
// producers (license-policy:, deploy-platform:, stack-playbook:) use a
|
|
10278
|
+
// FIXED PLACEHOLDER `line` (0 or 1), not a real diffable source location
|
|
10279
|
+
// — they read scanRoot-level files (package.json, vercel.json, ...)
|
|
10280
|
+
// directly rather than from the scanned fileContents. Routing those
|
|
10281
|
+
// through git-blame-style resolution would fabricate a plausible-looking
|
|
10282
|
+
// but meaningless commit attribution (e.g. "package.json line 1" blamed
|
|
10283
|
+
// on whatever commit last touched that line, unrelated to the actual
|
|
10284
|
+
// license/platform/stack finding). They are excluded by id prefix here
|
|
10285
|
+
// and stay on the honest, PERMANENT not_available path in the backstop
|
|
10286
|
+
// loop below — never routed through resolveOrigin. Declared here, OUTSIDE
|
|
10287
|
+
// the _runAnnotator callback below, because the backstop loop (also
|
|
10288
|
+
// outside that callback — see its own comment on why) needs
|
|
10289
|
+
// `blameableLogic`/`syntheticLogic` too.
|
|
10290
|
+
//
|
|
10291
|
+
// `aSecrets`/`aLogic`'s ruleId backfill for the producers that exist by
|
|
10292
|
+
// this point in the function already happened much earlier (right before
|
|
10293
|
+
// the `skipAnnotators` guard opens — see that comment for why it CANNOT
|
|
10294
|
+
// live here alone). `_slugify` is declared there and is in scope here too.
|
|
10295
|
+
// What's re-applied below is only the SAME idempotent backfill
|
|
10296
|
+
// (`if (!f.ruleId)`), now over the full, final `blameableLogic` — which by
|
|
10297
|
+
// this point additionally includes logic-claims.js's late-pushed ingested
|
|
10298
|
+
// claims, the one blameable producer the early pass could not see.
|
|
10299
|
+
const SYNTHETIC_LOGIC_PREFIXES = ['license-policy:', 'deploy-platform:', 'stack-playbook:'];
|
|
10300
|
+
const isSyntheticLogicFinding = (f) => typeof f?.id === 'string'
|
|
10301
|
+
&& SYNTHETIC_LOGIC_PREFIXES.some((p) => f.id.startsWith(p));
|
|
10302
|
+
const blameableLogic = aLogic.filter((f) => !isSyntheticLogicFinding(f));
|
|
10303
|
+
const syntheticLogic = aLogic.filter(isSyntheticLogicFinding);
|
|
10304
|
+
for (const f of blameableLogic) {
|
|
10305
|
+
if (!f.ruleId) f.ruleId = `logic:${_slugify(f.vuln)}`;
|
|
10306
|
+
}
|
|
10307
|
+
annotateStableIds(aSecrets);
|
|
10308
|
+
annotateStableIds(blameableLogic);
|
|
10309
|
+
await _runAnnotator("annotateGitProvenance", async () => {
|
|
10310
|
+
// ONE deadline for the whole scan's provenance work, computed here and
|
|
10311
|
+
// threaded into all five annotateGitProvenance calls below. Computed per call
|
|
10312
|
+
// inside the coordinator, the effective scan-level budget was 2× the
|
|
10313
|
+
// configured --provenance-timeout: the SAST pass got a fresh window and
|
|
10314
|
+
// then the SCA pass got another one, so an operator asking for a 30s cap
|
|
10315
|
+
// could wait 60s. The spec describes a single global deadline; this is
|
|
10316
|
+
// where "global" has to be established, because this is the only scope
|
|
10317
|
+
// that sees both passes.
|
|
10318
|
+
const provenanceTimeoutMs = process.env.AGENTIC_SECURITY_PROVENANCE_TIMEOUT_MS
|
|
10319
|
+
? parseInt(process.env.AGENTIC_SECURITY_PROVENANCE_TIMEOUT_MS, 10) : undefined;
|
|
10320
|
+
// Deterministic mode promises byte-identical SARIF run-to-run
|
|
10321
|
+
// (posture/deterministic.js), and `observedAt` above is already frozen to
|
|
10322
|
+
// honour that. But `findingProvenance.status` (complete/partial/
|
|
10323
|
+
// budget_exhausted/...) is driven by THIS deadline, which was computed
|
|
10324
|
+
// from a real `Date.now()` even under --deterministic — so two runs of
|
|
10325
|
+
// the identical scan could cross a 60s budget at different points under
|
|
10326
|
+
// machine contention (git subprocess calls slowed by CPU/IO pressure) and
|
|
10327
|
+
// land different findings on `budget_exhausted` vs a resolved status,
|
|
10328
|
+
// changing the emitted SARIF between runs. An explicit
|
|
10329
|
+
// --provenance-timeout / AGENTIC_SECURITY_PROVENANCE_TIMEOUT_MS still wins
|
|
10330
|
+
// (an operator asking for a tight budget gets it regardless of mode);
|
|
10331
|
+
// absent that, deterministic mode gets a much larger fixed ceiling so
|
|
10332
|
+
// resolution has room to finish under realistic load instead of a bound
|
|
10333
|
+
// that's routinely crossed — see test/proof-corpus-lib.test.js's
|
|
10334
|
+
// "produces byte-identical SARIF across two runs".
|
|
10335
|
+
const DETERMINISTIC_PROVENANCE_TIMEOUT_MS = 300000; // 5 minutes
|
|
10336
|
+
const provenanceDeadlineAt = Date.now()
|
|
10337
|
+
+ (Number.isFinite(provenanceTimeoutMs) && provenanceTimeoutMs > 0
|
|
10338
|
+
? provenanceTimeoutMs
|
|
10339
|
+
: (isDeterministic() ? DETERMINISTIC_PROVENANCE_TIMEOUT_MS : PROVENANCE_DEFAULT_TIMEOUT_MS));
|
|
10340
|
+
const provenanceCtx = {
|
|
10341
|
+
scanRoot,
|
|
10342
|
+
deadlineAt: provenanceDeadlineAt,
|
|
10343
|
+
// CLI flags that set these land in Task 17; reading the env directly here
|
|
10344
|
+
// mirrors AGENTIC_SECURITY_NO_GIT_HISTORY's existing pattern in this file.
|
|
10345
|
+
disabled: process.env.AGENTIC_SECURITY_NO_PROVENANCE === '1',
|
|
10346
|
+
scanId: process.env.AGENTIC_SECURITY_SCAN_ID || null,
|
|
10347
|
+
// Frozen under --deterministic so SARIF (which now carries
|
|
10348
|
+
// findingProvenance.firstObserved.observedAt — see report/index.js's
|
|
10349
|
+
// toSARIF) stays byte-identical run-to-run, matching the exact
|
|
10350
|
+
// convention posture/deterministic.js's makeDeterministic() already
|
|
10351
|
+
// uses for meta.startedAt. This value predates that guarantee; it was
|
|
10352
|
+
// invisible before findingProvenance reached any output format.
|
|
10353
|
+
observedAt: isDeterministic() ? '1970-01-01T00:00:00.000Z' : new Date().toISOString(),
|
|
10354
|
+
// FR-PROV-028 / "Evidence integrity": the cache key (coordinator.js's
|
|
10355
|
+
// `makeCacheKey`) and `computeDigest`'s `rulesetVersion` binding both
|
|
10356
|
+
// read this field, so it has to be the REAL effective ruleset version,
|
|
10357
|
+
// not an env var operators essentially never set. `_effectiveRulesetVersion`
|
|
10358
|
+
// (posture/ruleset-version.js's `effectiveVersion`) already resolves
|
|
10359
|
+
// env override > pinned file > CURRENT_RULESET_VERSION (== the running
|
|
10360
|
+
// scanner's own package version) — the same helper the checkpoint
|
|
10361
|
+
// identity above (`_ckptIdentity.rulesetVersion`) already calls for an
|
|
10362
|
+
// unrelated purpose. Computed ONCE here and shared via `provenanceCtx`,
|
|
10363
|
+
// same precedent as `deadlineAt` and `providerEnrichments` below: it's
|
|
10364
|
+
// cheap (an env read plus one small JSON file stat/read), but five
|
|
10365
|
+
// recomputations across the five annotateGitProvenance calls buys
|
|
10366
|
+
// nothing and risks a mid-scan pinned-file edit producing five
|
|
10367
|
+
// different answers in one run.
|
|
10368
|
+
rulesetVersion: (_effectiveRulesetVersion(scanRoot) || {}).version || null,
|
|
10369
|
+
since: process.env.AGENTIC_SECURITY_PROVENANCE_SINCE || null,
|
|
10370
|
+
timeoutMs: provenanceTimeoutMs,
|
|
10371
|
+
mode: process.env.AGENTIC_SECURITY_PROVENANCE_MODE || 'standard',
|
|
10372
|
+
// Fix-round item 2: ONE shared provider-enrichment counter for the
|
|
10373
|
+
// whole scan, same precedent as the single `deadlineAt` above. Object
|
|
10374
|
+
// identity (not the primitive value) is what makes the cap survive the
|
|
10375
|
+
// `{ ...provenanceCtx, findingType: ... }` spreads used by four of the
|
|
10376
|
+
// five annotateGitProvenance calls below — see coordinator.js's
|
|
10377
|
+
// `providerEnrichments` comment for why a bare number would not work.
|
|
10378
|
+
providerEnrichments: { remaining: MAX_PROVIDER_ENRICHMENTS_PER_SCAN },
|
|
10379
|
+
};
|
|
10380
|
+
await annotateGitProvenance(finalFindings, provenanceCtx);
|
|
10381
|
+
// Direct dependencies only: a transitive dep's vulnerable version was never
|
|
10382
|
+
// declared in this repository's manifests, so there is no commit here that
|
|
10383
|
+
// introduced it and `resolveDirectSCAOrigin` would have nothing to walk.
|
|
10384
|
+
//
|
|
10385
|
+
// Keyed on `isDirect`, the same field the transitive-dedup block above
|
|
10386
|
+
// uses. The plan's `!s.isTransitive` was a silent no-op: `isTransitive`
|
|
10387
|
+
// exists on dependency COMPONENTS but was never carried onto the
|
|
10388
|
+
// vulnerable_dep entries, so the negation was true for every entry and the
|
|
10389
|
+
// filter excluded nothing. `isDirect` is now propagated at materialization.
|
|
10390
|
+
const directDeps = (supplyChain || []).filter((s) => s && s.type === 'vulnerable_dep' && s.isDirect);
|
|
10391
|
+
// `resolveDirectSCAOrigin`/`scaStableId` key on `filePath`; the vulnerable_dep
|
|
10392
|
+
// entries built above carry the manifest path as `file` (report/index.js
|
|
10393
|
+
// already reads `sc.filePath || sc.file` for the same reason). Backfill the
|
|
10394
|
+
// alias rather than teaching the SCA resolver a second field name — without
|
|
10395
|
+
// it every direct dependency resolves to `not_available: no-manifest-path`.
|
|
10396
|
+
for (const s of directDeps) { if (!s.filePath && s.file) s.filePath = s.file; }
|
|
10397
|
+
await annotateGitProvenance(directDeps, { ...provenanceCtx, findingType: 'sca' });
|
|
10398
|
+
// M3 §3.2: transitive dependencies now get real origin resolution too,
|
|
10399
|
+
// narrowing what was previously an unconditional not_available backstop
|
|
10400
|
+
// to genuinely unresolvable cases (non-npm lockfiles, no candidate
|
|
10401
|
+
// history) — see transitive-sca.js's own scope note.
|
|
10402
|
+
const transitiveDeps = (supplyChain || []).filter((s) => s && s.type === 'vulnerable_dep' && !s.isDirect);
|
|
10403
|
+
for (const s of transitiveDeps) { if (!s.filePath && s.file) s.filePath = s.file; }
|
|
10404
|
+
await annotateGitProvenance(transitiveDeps, { ...provenanceCtx, findingType: 'sca-transitive' });
|
|
10405
|
+
// Task 11 (PRD P0 scope): `aSecrets`/`blameableLogic` already have real
|
|
10406
|
+
// stableIds backfilled above (outside this callback — see that comment).
|
|
10407
|
+
// findingType 'secret'/'logic' is not consumed by any branch in
|
|
10408
|
+
// coordinator.js's resolveOne (only 'sca'/'sca-transitive' select a
|
|
10409
|
+
// different resolution strategy) — both fall through to the plain SAST
|
|
10410
|
+
// path (file+line blame short-circuit, then origin-resolver.js). Passed
|
|
10411
|
+
// anyway for clarity and future debugging; harmless today.
|
|
10412
|
+
await annotateGitProvenance(aSecrets, { ...provenanceCtx, findingType: 'secret' });
|
|
10413
|
+
await annotateGitProvenance(blameableLogic, { ...provenanceCtx, findingType: 'logic' });
|
|
10414
|
+
// FR-PROV-013 — introduce/remediate/reintroduce events. Best-effort by
|
|
10415
|
+
// design: the lifecycle store is a convenience ledger, and a failed write
|
|
10416
|
+
// (read-only tree, lock contention) must never fail a scan, matching how
|
|
10417
|
+
// every other provenance component degrades.
|
|
10418
|
+
//
|
|
10419
|
+
// Gated on `disabled` as well as on the `provenance` parameter, because
|
|
10420
|
+
// those are two different opt-outs and only one of them was being honoured.
|
|
10421
|
+
// updateLifecycle WRITES to disk; an operator who set
|
|
10422
|
+
// AGENTIC_SECURITY_NO_PROVENANCE=1 has said the feature does nothing, and a
|
|
10423
|
+
// disabled feature that still litters `.agentic-security/provenance/` is
|
|
10424
|
+
// not disabled. (annotateGitProvenance handles `disabled` internally by
|
|
10425
|
+
// stamping not_available, which is why it is still called above — every
|
|
10426
|
+
// finding must keep a terminal status even with the feature off.)
|
|
10427
|
+
//
|
|
10428
|
+
// Gated on `scanRoot` too. Every path into updateLifecycle resolves the
|
|
10429
|
+
// store through statePath(scanRoot, …), and statePath falls back to the
|
|
10430
|
+
// PROCESS CWD when scanRoot is null — so a runFullScan called with no
|
|
10431
|
+
// scanRoot (the in-process test/bench harnesses, predicate replay before
|
|
10432
|
+
// its provenance:false brake, any embedder) wrote a lifecycle ledger into
|
|
10433
|
+
// whatever directory the process happened to be in, keyed on that
|
|
10434
|
+
// unrelated run's findings. That is how this repo's own
|
|
10435
|
+
// scanner/.agentic-security/provenance/lifecycle.json accumulated 374
|
|
10436
|
+
// stableIds and 3000+ spurious remediated/reintroduced events. No
|
|
10437
|
+
// scanRoot means no project to keep a ledger for.
|
|
10438
|
+
//
|
|
10439
|
+
// `completeScan` is the other half, and it is a correctness gate rather
|
|
10440
|
+
// than a hygiene one — see runFullScan's parameter comment and
|
|
10441
|
+
// lifecycle.js's applyScan.
|
|
10442
|
+
//
|
|
10443
|
+
// TRUTHY IS NOT ENOUGH — it has to be a real directory. `resolveProjectRoot`
|
|
10444
|
+
// honours a caller-supplied scanRoot only when it exists on disk AND is a
|
|
10445
|
+
// directory; for anything else (a typo'd path, a `/tmp/...` a test never
|
|
10446
|
+
// created, a file rather than a directory) it silently falls back to walking
|
|
10447
|
+
// UP FROM THE PROCESS CWD for a project marker. So `agentic-security scan
|
|
10448
|
+
// ./typo` run from inside a project writes THAT project's lifecycle ledger
|
|
10449
|
+
// from a scan that never looked at it — and since such a scan finds nothing
|
|
10450
|
+
// while still claiming `completeScan`, the remediation pass closes every
|
|
10451
|
+
// open finding the real project had. Same corruption as the null-scanRoot
|
|
10452
|
+
// case above, reached through a different door: found by tracing the writes
|
|
10453
|
+
// that kept reappearing in this repo's own scanner/.agentic-security AFTER
|
|
10454
|
+
// the null guard was added. All three remaining writers were
|
|
10455
|
+
// truthy-but-nonexistent scanRoots.
|
|
10456
|
+
//
|
|
10457
|
+
// Narrow on purpose. Every other state write in such a scan lands in the
|
|
10458
|
+
// same wrong place, which is a wider `state-dir.js` question; the lifecycle
|
|
10459
|
+
// ledger is singled out here because it is the only one that turns the
|
|
10460
|
+
// mistake into a destructive claim about findings it never saw.
|
|
10461
|
+
let scanRootIsRealDir = false;
|
|
10462
|
+
try { scanRootIsRealDir = !!scanRoot && fs.statSync(scanRoot).isDirectory(); } catch { scanRootIsRealDir = false; }
|
|
10463
|
+
if (scanRootIsRealDir && !provenanceCtx.disabled) {
|
|
10464
|
+
try {
|
|
10465
|
+
await updateLifecycle(scanRoot, finalFindings, {
|
|
10466
|
+
scanId: provenanceCtx.scanId, observedAt: provenanceCtx.observedAt,
|
|
10467
|
+
completeScan,
|
|
10468
|
+
});
|
|
10469
|
+
} catch (_) { /* best-effort */ }
|
|
10470
|
+
}
|
|
10471
|
+
});
|
|
10472
|
+
// Structural backstop for the terminal-status guarantee, for BOTH channels.
|
|
10473
|
+
// Deliberately OUTSIDE the _runAnnotator callback above: _runAnnotator
|
|
10474
|
+
// swallows whatever the callback throws, so anything that depends on the
|
|
10475
|
+
// callback reaching its last line is a convention, not a guarantee. If
|
|
10476
|
+
// annotateGitProvenance threw before its per-finding loop began
|
|
10477
|
+
// (getRepoState blowing up on a corrupt repo, say), everything below the
|
|
10478
|
+
// throw is skipped and every finding and supplyChain entry silently carries
|
|
10479
|
+
// no findingProvenance at all — exactly the absent-field state the status
|
|
10480
|
+
// enum exists to make unreachable. Enforced here so it holds structurally.
|
|
10481
|
+
for (const f of finalFindings) {
|
|
10482
|
+
if (f && typeof f === 'object' && !f.findingProvenance) {
|
|
10483
|
+
f.findingProvenance = emptyProvenance(PROVENANCE_STATUS.ERROR, {
|
|
10484
|
+
limitations: ['provenance annotator did not reach this finding'],
|
|
10485
|
+
});
|
|
10486
|
+
}
|
|
10487
|
+
}
|
|
10488
|
+
// The supply-chain half. report/index.js normalizes EVERY supplyChain entry
|
|
10489
|
+
// into an SCA finding — not just the direct vulnerable_dep ones the resolver
|
|
10490
|
+
// can speak to — and pipeline/finding-schema.js requires findingProvenance on
|
|
10491
|
+
// every channel. Three distinct populations reach this loop and they are not
|
|
10492
|
+
// the same statement, so they do not share a limitation string:
|
|
10493
|
+
//
|
|
10494
|
+
// - transitive vulnerable_deps: resolved by resolveTransitiveSCAOrigin
|
|
10495
|
+
// above (M3 §3.2) — this branch is now reached only if that annotation
|
|
10496
|
+
// pass itself failed to stamp the entry.
|
|
10497
|
+
// - unpinned_dep / no_lockfile and friends: these describe the ABSENCE of a
|
|
10498
|
+
// declaration, so "which commit introduced this version" is not a question
|
|
10499
|
+
// that has an answer to defer.
|
|
10500
|
+
// - anything the annotator failed to reach, as above.
|
|
10501
|
+
//
|
|
10502
|
+
// The first two are honest `not_available` — that is exactly what the status
|
|
10503
|
+
// is for. Only a genuine annotator failure is an `error`, which is why this
|
|
10504
|
+
// loop distinguishes them rather than stamping one status for all three.
|
|
10505
|
+
for (const sc of (supplyChain || [])) {
|
|
10506
|
+
if (!sc || typeof sc !== 'object' || sc.findingProvenance) continue;
|
|
10507
|
+
sc.findingProvenance = emptyProvenance(PROVENANCE_STATUS.NOT_AVAILABLE, {
|
|
10508
|
+
limitations: [sc.type === 'vulnerable_dep'
|
|
10509
|
+
? 'transitive dependency origin resolution failed for this entry (annotator error)'
|
|
10510
|
+
: `origin resolution does not apply to a ${sc.type || 'non-vulnerability'} supply-chain entry`],
|
|
10511
|
+
});
|
|
10512
|
+
}
|
|
10513
|
+
// The OTHER two channels report/index.js normalizes into findings —
|
|
10514
|
+
// `scan.secrets` and `scan.logicVulns`. The same argument that produced the
|
|
10515
|
+
// supplyChain loop above applies verbatim: pipeline/finding-schema.js makes
|
|
10516
|
+
// `findingProvenance` REQUIRED on every channel, and normalizeFindings emits
|
|
10517
|
+
// a finding for each of these, so leaving them unstamped ships a
|
|
10518
|
+
// schema-incomplete finding whose absent field is indistinguishable from
|
|
10519
|
+
// "escaped annotation" — the exact condition the status enum exists to
|
|
10520
|
+
// remove.
|
|
10521
|
+
//
|
|
10522
|
+
// Task 11 (PRD P0 scope): `aSecrets` and `blameableLogic` now go through
|
|
10523
|
+
// REAL resolution above (real stableIds backfilled, real
|
|
10524
|
+
// annotateGitProvenance calls made), so this loop no longer covers them
|
|
10525
|
+
// wholesale — it is now a defensive catch-all for any entry the real call
|
|
10526
|
+
// somehow didn't reach (same precedent as the supplyChain loop above), plus
|
|
10527
|
+
// `syntheticLogic`, which can NEVER get real resolution by design (see the
|
|
10528
|
+
// classification comment above `SYNTHETIC_LOGIC_PREFIXES`) and stays here
|
|
10529
|
+
// permanently and honestly, not as a deferral.
|
|
10530
|
+
for (const x of (syntheticLogic || [])) {
|
|
10531
|
+
if (!x || typeof x !== 'object' || x.findingProvenance) continue;
|
|
10532
|
+
x.findingProvenance = emptyProvenance(PROVENANCE_STATUS.NOT_AVAILABLE, {
|
|
10533
|
+
limitations: ['this finding describes dependency/config/policy state, not a single source line a commit introduced -- origin resolution does not apply'],
|
|
10534
|
+
});
|
|
10535
|
+
}
|
|
10536
|
+
for (const bucket of [aSecrets, blameableLogic]) {
|
|
10537
|
+
for (const x of (bucket || [])) {
|
|
10538
|
+
if (!x || typeof x !== 'object' || x.findingProvenance) continue;
|
|
10539
|
+
x.findingProvenance = emptyProvenance(PROVENANCE_STATUS.NOT_AVAILABLE, {
|
|
10540
|
+
limitations: ['origin resolution annotator did not reach this finding'],
|
|
10541
|
+
});
|
|
10542
|
+
}
|
|
10543
|
+
}
|
|
10544
|
+
}
|
|
9833
10545
|
// Addition #2 — attack-surface completeness inventory (entry points → dispositions).
|
|
9834
|
-
|
|
10546
|
+
// (_entrypointInventory is hoisted above the `skipAnnotators` guard — FR-PROV-029.)
|
|
10547
|
+
try { _entrypointInventory = buildEntrypointInventory(fc, { routes: aR, findings: finalFindings }); } catch { _entrypointInventory = {}; }
|
|
9835
10548
|
// R9 + R6 — relevance scoping. Runs HERE, after every finding has been
|
|
9836
10549
|
// appended (multi-sink chains, cross-language chains) and after the
|
|
9837
10550
|
// entry-point inventory exists, so no finding escapes annotation and the
|
|
9838
10551
|
// attack surface it is scored against is the complete one. Recall-
|
|
9839
10552
|
// preserving: never removes a finding, never touches severity, and only
|
|
9840
10553
|
// asserts `unreachable` on positive evidence (see posture/relevance.js).
|
|
9841
|
-
_runAnnotator("annotateRelevance", () => {
|
|
10554
|
+
await _runAnnotator("annotateRelevance", () => {
|
|
9842
10555
|
annotateRelevance(finalFindings, {
|
|
9843
10556
|
fileContents: fc,
|
|
9844
10557
|
entrypointInventory: _entrypointInventory,
|
|
@@ -9848,14 +10561,46 @@ function _deterministicFileTimings(timings) {
|
|
|
9848
10561
|
});
|
|
9849
10562
|
// Addition #3 — root-cause sweep: from confirmed findings, find sibling instances
|
|
9850
10563
|
// detectors missed, with total-count accounting. Confirmed-only (cheap by default).
|
|
9851
|
-
|
|
10564
|
+
// (_rootCauseSweep/_proofCoverage/_coverageLedger/_scanHealth are hoisted
|
|
10565
|
+
// above the `skipAnnotators` guard — FR-PROV-029.)
|
|
10566
|
+
try { _rootCauseSweep = sweepRootCauses(finalFindings, fc); } catch { _rootCauseSweep = null; }
|
|
9852
10567
|
// PRD F7.2: publish what CANNOT be proven alongside what can. A proof RATE
|
|
9853
10568
|
// computed over the provable subset makes a narrow subset look like strength;
|
|
9854
10569
|
// the three-bucket split (provable / declined-on-purpose / not-yet-classified)
|
|
9855
10570
|
// is the honest shape. Measured on the CVE corpus: 19% / 13% / 68%.
|
|
9856
|
-
let _proofCoverage = null;
|
|
9857
10571
|
try { _proofCoverage = proofCoverage([...finalFindings, ...aLogic]); } catch { _proofCoverage = null; }
|
|
9858
|
-
|
|
10572
|
+
// FR-203: per-file/per-analyzer coverage ledger, computed from exactly
|
|
10573
|
+
// the signals FR-201 (_detectorErrors) and FR-202 (the _timeout:true
|
|
10574
|
+
// marker finding) already produce -- files actually scanned come from
|
|
10575
|
+
// fc's own keys (skipped-for-size/density files were never added to it).
|
|
10576
|
+
const _timedOutFiles = finalFindings.filter(f => f && f._timeout === true).map(f => f.file);
|
|
10577
|
+
_coverageLedger = computeCoverageLedger({ files: Object.keys(fc), detectorErrors: _detectorErrors, timedOutFiles: _timedOutFiles });
|
|
10578
|
+
// FR-206 (assurance-hardening PRD, Milestone 0): additive scan-health
|
|
10579
|
+
// summary, computed from signals the engine already collects.
|
|
10580
|
+
// `analyzers` was `null` (see pipeline/scan-health.js's prior comment)
|
|
10581
|
+
// until FR-203's coverage ledger existed to compute it for real.
|
|
10582
|
+
_scanHealth = computeScanHealth({
|
|
10583
|
+
scanMeta: _scanMeta,
|
|
10584
|
+
annotatorErrors: _annotatorErrors,
|
|
10585
|
+
engineErrors: { cppDataflowParseErrors: _cppDataflowParseErrors.value },
|
|
10586
|
+
deepStatus: _deepStatus,
|
|
10587
|
+
analyzerCoverage: summarizeCoverageForScanHealth(_coverageLedger),
|
|
10588
|
+
});
|
|
10589
|
+
// FR-207: stale vulnerability feeds, calibration data, and compliance
|
|
10590
|
+
// evidence are real assurance gaps, not just findings the feed omits --
|
|
10591
|
+
// surfaced the same way every other scan-health condition is, so
|
|
10592
|
+
// --assurance strict (FR-204) can fail on them. Custom-rule-pack
|
|
10593
|
+
// freshness is the one leg NOT computed here: that mechanism only runs
|
|
10594
|
+
// in bin/agentic-security.js, after this scan object already exists (see
|
|
10595
|
+
// applyFreshness's own header comment in pipeline/scan-health.js).
|
|
10596
|
+
_scanHealth = applyFreshness(_scanHealth, {
|
|
10597
|
+
kev: kevCatalogMeta(),
|
|
10598
|
+
epss: epssLiveMeta(),
|
|
10599
|
+
calibration: calibrationFreshness(),
|
|
10600
|
+
compliance: _complianceReport ? { stale: _complianceReport.summary?.stale || 0 } : null,
|
|
10601
|
+
});
|
|
10602
|
+
} // end if (!skipAnnotators) — FR-PROV-029
|
|
10603
|
+
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
10604
|
|
|
9860
10605
|
// Post-aggregation classification: every source becomes "unsafe"|"safe"; every sink becomes "confirmed"|"safe".
|
|
9861
10606
|
// Orphans (no finding linkage) are bucketed by file-local heuristic so the UI shows binary states only.
|
|
@@ -10322,6 +11067,7 @@ export {
|
|
|
10322
11067
|
classifyOrphans, classifyField, classifyEndpoint, shouldScan,
|
|
10323
11068
|
_isFalsePositiveCredential, _detectSafeSinkShape,
|
|
10324
11069
|
_loadCustomRules, _isCustomSuppressed, _isPathIgnored,
|
|
11070
|
+
_snapshotSuppressionLog, _restoreSuppressionLog,
|
|
10325
11071
|
scanIaC, IAC_PATTERNS, _isIaCFile, isCloudFormationTemplate,
|
|
10326
11072
|
payloadsForFinding, buildProofObligation,
|
|
10327
11073
|
DATA_CLASSES, SOURCE_PATTERNS, SINK_PATTERNS, SANITIZER_PATTERNS,
|