@clear-capabilities/agentic-security-scanner 0.144.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 +251 -0
- package/bin/agentic-security.js +294 -3
- package/dist/113.index.js +11 -3
- package/dist/178.index.js +24 -6
- package/dist/271.index.js +165 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +22 -0
- package/dist/444.index.js +11 -2
- package/dist/449.index.js +76 -12
- package/dist/526.index.js +11 -3
- package/dist/637.index.js +27 -5
- package/dist/970.index.js +65 -1
- package/dist/agentic-security.mjs +9 -9
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +14 -8
- package/src/compare.js +6 -1
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/engine.js +488 -29
- package/src/fix/apply-fix-service.js +1 -0
- package/src/history-scan.js +22 -5
- package/src/ir/CLAUDE.md +1 -1
- package/src/lsp/server.js +49 -2
- package/src/mcp/tools.js +20 -0
- package/src/pipeline/assurance-mode.js +64 -1
- package/src/pipeline/finding-schema.js +8 -1
- package/src/posture/CLAUDE.md +121 -0
- package/src/posture/accuracy-scorecard.js +60 -0
- package/src/posture/artifact-registry.js +24 -0
- package/src/posture/auditor-walkthrough.js +116 -13
- package/src/posture/compliance-policy.js +12 -2
- package/src/posture/cross-repo-memory.js +7 -2
- package/src/posture/fix-history.js +25 -2
- package/src/posture/fix-verify.js +9 -1
- package/src/posture/fleet.js +0 -0
- package/src/posture/git-history.js +13 -5
- package/src/posture/material-change.js +21 -2
- package/src/posture/mttr.js +75 -12
- package/src/posture/pre-incident-archaeology.js +39 -7
- package/src/posture/privacy-framework.js +14 -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/sbom-diff.js +15 -2
- package/src/posture/secret-history.js +10 -2
- package/src/posture/state-dir.js +38 -14
- package/src/posture/vuln-archaeology.js +8 -2
- package/src/pr-delta.js +25 -4
- package/src/report/index.js +197 -3
- package/src/runScan.js +34 -5
- package/src/sast/rate-limit.js +33 -3
- package/src/util/git-hardening.js +128 -0
package/src/engine.js
CHANGED
|
@@ -260,6 +260,17 @@ import { annotateAttackTaxonomy, summarizeTaxonomy } from './posture/attack-taxo
|
|
|
260
260
|
import { suppressByPastDecisions } from './posture/triage-memory.js';
|
|
261
261
|
import { suppressByIntent } from './posture/intent-context.js';
|
|
262
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';
|
|
263
274
|
import { applyThreatModel } from './posture/threat-model-grounding.js';
|
|
264
275
|
import { annotateCrossRepoSignals } from './posture/pattern-propagation.js';
|
|
265
276
|
import { annotateRiskDollars } from './posture/risk-dollars.js';
|
|
@@ -2626,6 +2637,33 @@ function _resetSuppressions(){ _suppressionLog.length = 0; }
|
|
|
2626
2637
|
// so pfr[p] and the aggregates share object identity, exactly as in a normal run.
|
|
2627
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; }
|
|
2628
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); }
|
|
2629
2667
|
|
|
2630
2668
|
// ── inline suppression pragma ───────────────────────────────────────────────
|
|
2631
2669
|
//
|
|
@@ -7340,6 +7378,31 @@ function _makePurl(ecosystem,name,version,group){
|
|
|
7340
7378
|
return`pkg:${t}/${ns}${encodeURIComponent(name)}${version?'@'+encodeURIComponent(version):''}`;
|
|
7341
7379
|
}
|
|
7342
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
|
+
|
|
7343
7406
|
function _parsePackageJson(text,filePath){
|
|
7344
7407
|
const out=[];try{const d=JSON.parse(text);
|
|
7345
7408
|
for(const[depKey,scope]of[['dependencies','required'],['devDependencies','optional']]){
|
|
@@ -7350,7 +7413,8 @@ function _parsePackageJson(text,filePath){
|
|
|
7350
7413
|
const group=scoped?`@${parts[0]}`:'';
|
|
7351
7414
|
const pkgName=scoped?parts[1]:name;
|
|
7352
7415
|
out.push({name,version:ver,group,scope,purl:_makePurl('npm',pkgName,ver,group),ecosystem:'npm',filePath,
|
|
7353
|
-
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)});
|
|
7354
7418
|
}
|
|
7355
7419
|
}
|
|
7356
7420
|
}catch(_){}return out;
|
|
@@ -7379,12 +7443,13 @@ function _parsePackageLockJson(text,filePath){
|
|
|
7379
7443
|
|
|
7380
7444
|
function _parseRequirementsTxt(text,filePath){
|
|
7381
7445
|
const out=[];
|
|
7382
|
-
|
|
7383
|
-
|
|
7446
|
+
const lines=text.split('\n');
|
|
7447
|
+
for(let i=0;i<lines.length;i++){
|
|
7448
|
+
const t=lines[i].trim();
|
|
7384
7449
|
if(!t||t.startsWith('#')||t.startsWith('-'))continue;
|
|
7385
7450
|
const m=t.match(/^([A-Za-z0-9_.-]+)\s*[=~<>!]+\s*([^\s;#,]*)/);
|
|
7386
7451
|
if(m)out.push({name:m[1],version:m[2],group:'',scope:'required',
|
|
7387
|
-
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});
|
|
7388
7453
|
}return out;
|
|
7389
7454
|
}
|
|
7390
7455
|
|
|
@@ -8180,6 +8245,15 @@ async function queryOSV(components,allFileContents){
|
|
|
8180
8245
|
fixedVersions: vuln.fixedVersions, severity: vuln.severity, cvssVector: vuln.cvssVector,
|
|
8181
8246
|
hasKnownAttackRef: vuln.hasKnownAttackRef, osvVulnFunctions: vuln.osvVulnFunctions || [], reachable: comp.reachable, scope: comp.scope,
|
|
8182
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(/\/$/, '')) : [],
|
|
8183
8257
|
// kept for generateRecs() compat
|
|
8184
8258
|
advisory: `${vid}${cveStr}, ${vuln.description}`,
|
|
8185
8259
|
range: fixStr ? `< ${fixStr}` : 'see advisory' });
|
|
@@ -8526,7 +8600,28 @@ async function queryRegistries(components){
|
|
|
8526
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};
|
|
8527
8601
|
}
|
|
8528
8602
|
|
|
8529
|
-
|
|
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);
|
|
8530
8625
|
// Pre-pass: build cross-file Java tainted-method index so per-file taint
|
|
8531
8626
|
// analysis can recognize calls to user-input-returning helper methods
|
|
8532
8627
|
// defined in OTHER files (Juliet's DataflowThruInnerClass / Vector / Stream
|
|
@@ -9276,9 +9371,76 @@ function _deterministicFileTimings(timings) {
|
|
|
9276
9371
|
// Every catch in this block writes into _annotatorErrors so the operator
|
|
9277
9372
|
// can tell "didn't run" from "ran cleanly." The array is surfaced as
|
|
9278
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.
|
|
9279
9396
|
let _executionProofSummary = null, _vulnHistory = null;
|
|
9280
9397
|
let _logicClaims = null;
|
|
9281
|
-
|
|
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) {
|
|
9282
9444
|
// FR-106 (assurance-hardening PRD): Promise-aware, explicitly awaited at
|
|
9283
9445
|
// every one of its ~51 call sites below (previously a sync `try{return
|
|
9284
9446
|
// fn()}` let an async callback's rejection escape as an unhandled
|
|
@@ -9384,18 +9546,18 @@ function _deterministicFileTimings(timings) {
|
|
|
9384
9546
|
// FR-405 (assurance-hardening PRD): null means "privacy analysis never
|
|
9385
9547
|
// ran at all" (AGENTIC_SECURITY_NO_PRIVACY=1, or the annotator threw
|
|
9386
9548
|
// before setting this) — treated the same as false by the gate below,
|
|
9387
|
-
// since neither case has real IR-backed evidence to offer. Declared
|
|
9388
|
-
//
|
|
9389
|
-
//
|
|
9390
|
-
//
|
|
9391
|
-
//
|
|
9392
|
-
//
|
|
9393
|
-
|
|
9394
|
-
//
|
|
9395
|
-
//
|
|
9396
|
-
//
|
|
9397
|
-
//
|
|
9398
|
-
|
|
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.
|
|
9399
9561
|
|
|
9400
9562
|
// ── World-class integration block ─────────────────────────────────────
|
|
9401
9563
|
// Each annotator is opt-in via env var and try/catch wrapped. They run
|
|
@@ -9894,7 +10056,7 @@ function _deterministicFileTimings(timings) {
|
|
|
9894
10056
|
classifyOrphans(aSrc,aSink,finalFindings,fc);
|
|
9895
10057
|
// v3 next-gen: capture scan-level reports (counterfactual, threat model,
|
|
9896
10058
|
// trust-boundary diagram, calibration-drift alarms). All best-effort.
|
|
9897
|
-
|
|
10059
|
+
// (_v3 is hoisted above the `skipAnnotators` guard — FR-PROV-029.)
|
|
9898
10060
|
await _runAnnotator("_v3.counterfactual", () => { _v3.counterfactual = runCounterfactual(finalFindings, fc); });
|
|
9899
10061
|
await _runAnnotator("_v3.threatModel", () => { _v3.threatModel = buildThreatModel(finalFindings, fc); });
|
|
9900
10062
|
await _runAnnotator("_v3.trustBoundaryDiagram", () => { _v3.trustBoundaryDiagram = buildTrustBoundaryDiagram(finalFindings, fc); });
|
|
@@ -9912,10 +10074,9 @@ function _deterministicFileTimings(timings) {
|
|
|
9912
10074
|
// Each is opt-in via env var. They produce machine-readable artifacts
|
|
9913
10075
|
// (threat-model.json/.md, dpia.md, compliance-evidence.json/.md,
|
|
9914
10076
|
// sbom-history/<sha>.json, exploit-bundles/) under .agentic-security/.
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
_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.)
|
|
9919
10080
|
if (process.env.AGENTIC_SECURITY_NO_INTEGRATION !== '1') {
|
|
9920
10081
|
// Threat model — STRIDE + entities + attack trees rooted in findings.
|
|
9921
10082
|
if (process.env.AGENTIC_SECURITY_NO_THREAT_MODEL !== '1') {
|
|
@@ -10083,12 +10244,307 @@ function _deterministicFileTimings(timings) {
|
|
|
10083
10244
|
// seen when only N-of-those-candidates were actually analyzed.
|
|
10084
10245
|
// checkpoint.total intentionally keeps files.length — that field means the
|
|
10085
10246
|
// full candidate set for resume bookkeeping, a different, correct meaning.
|
|
10086
|
-
|
|
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}};
|
|
10087
10248
|
// R8: the scan completed, so the checkpoint has been fully consumed — remove
|
|
10088
10249
|
// it. Anything that threw before this point leaves it in place to resume from.
|
|
10089
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
|
+
}
|
|
10090
10545
|
// Addition #2 — attack-surface completeness inventory (entry points → dispositions).
|
|
10091
|
-
|
|
10546
|
+
// (_entrypointInventory is hoisted above the `skipAnnotators` guard — FR-PROV-029.)
|
|
10547
|
+
try { _entrypointInventory = buildEntrypointInventory(fc, { routes: aR, findings: finalFindings }); } catch { _entrypointInventory = {}; }
|
|
10092
10548
|
// R9 + R6 — relevance scoping. Runs HERE, after every finding has been
|
|
10093
10549
|
// appended (multi-sink chains, cross-language chains) and after the
|
|
10094
10550
|
// entry-point inventory exists, so no finding escapes annotation and the
|
|
@@ -10105,24 +10561,25 @@ function _deterministicFileTimings(timings) {
|
|
|
10105
10561
|
});
|
|
10106
10562
|
// Addition #3 — root-cause sweep: from confirmed findings, find sibling instances
|
|
10107
10563
|
// detectors missed, with total-count accounting. Confirmed-only (cheap by default).
|
|
10108
|
-
|
|
10564
|
+
// (_rootCauseSweep/_proofCoverage/_coverageLedger/_scanHealth are hoisted
|
|
10565
|
+
// above the `skipAnnotators` guard — FR-PROV-029.)
|
|
10566
|
+
try { _rootCauseSweep = sweepRootCauses(finalFindings, fc); } catch { _rootCauseSweep = null; }
|
|
10109
10567
|
// PRD F7.2: publish what CANNOT be proven alongside what can. A proof RATE
|
|
10110
10568
|
// computed over the provable subset makes a narrow subset look like strength;
|
|
10111
10569
|
// the three-bucket split (provable / declined-on-purpose / not-yet-classified)
|
|
10112
10570
|
// is the honest shape. Measured on the CVE corpus: 19% / 13% / 68%.
|
|
10113
|
-
let _proofCoverage = null;
|
|
10114
10571
|
try { _proofCoverage = proofCoverage([...finalFindings, ...aLogic]); } catch { _proofCoverage = null; }
|
|
10115
10572
|
// FR-203: per-file/per-analyzer coverage ledger, computed from exactly
|
|
10116
10573
|
// the signals FR-201 (_detectorErrors) and FR-202 (the _timeout:true
|
|
10117
10574
|
// marker finding) already produce -- files actually scanned come from
|
|
10118
10575
|
// fc's own keys (skipped-for-size/density files were never added to it).
|
|
10119
10576
|
const _timedOutFiles = finalFindings.filter(f => f && f._timeout === true).map(f => f.file);
|
|
10120
|
-
|
|
10577
|
+
_coverageLedger = computeCoverageLedger({ files: Object.keys(fc), detectorErrors: _detectorErrors, timedOutFiles: _timedOutFiles });
|
|
10121
10578
|
// FR-206 (assurance-hardening PRD, Milestone 0): additive scan-health
|
|
10122
10579
|
// summary, computed from signals the engine already collects.
|
|
10123
10580
|
// `analyzers` was `null` (see pipeline/scan-health.js's prior comment)
|
|
10124
10581
|
// until FR-203's coverage ledger existed to compute it for real.
|
|
10125
|
-
|
|
10582
|
+
_scanHealth = computeScanHealth({
|
|
10126
10583
|
scanMeta: _scanMeta,
|
|
10127
10584
|
annotatorErrors: _annotatorErrors,
|
|
10128
10585
|
engineErrors: { cppDataflowParseErrors: _cppDataflowParseErrors.value },
|
|
@@ -10142,6 +10599,7 @@ function _deterministicFileTimings(timings) {
|
|
|
10142
10599
|
calibration: calibrationFreshness(),
|
|
10143
10600
|
compliance: _complianceReport ? { stale: _complianceReport.summary?.stale || 0 } : null,
|
|
10144
10601
|
});
|
|
10602
|
+
} // end if (!skipAnnotators) — FR-PROV-029
|
|
10145
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};}
|
|
10146
10604
|
|
|
10147
10605
|
// Post-aggregation classification: every source becomes "unsafe"|"safe"; every sink becomes "confirmed"|"safe".
|
|
@@ -10609,6 +11067,7 @@ export {
|
|
|
10609
11067
|
classifyOrphans, classifyField, classifyEndpoint, shouldScan,
|
|
10610
11068
|
_isFalsePositiveCredential, _detectSafeSinkShape,
|
|
10611
11069
|
_loadCustomRules, _isCustomSuppressed, _isPathIgnored,
|
|
11070
|
+
_snapshotSuppressionLog, _restoreSuppressionLog,
|
|
10612
11071
|
scanIaC, IAC_PATTERNS, _isIaCFile, isCloudFormationTemplate,
|
|
10613
11072
|
payloadsForFinding, buildProofObligation,
|
|
10614
11073
|
DATA_CLASSES, SOURCE_PATTERNS, SINK_PATTERNS, SANITIZER_PATTERNS,
|