@clear-capabilities/agentic-security-scanner 0.144.0 → 0.147.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 +390 -0
- package/bin/agentic-security.js +3813 -83
- package/dist/1122.index.js +702 -0
- package/dist/{301.index.js → 1301.index.js} +2 -2
- package/dist/1379.index.js +591 -0
- package/dist/{444.index.js → 1444.index.js} +13 -4
- package/dist/{660.index.js → 1660.index.js} +2 -2
- package/dist/{700.index.js → 1700.index.js} +2 -2
- package/dist/{905.index.js → 1905.index.js} +3 -3
- package/dist/{920.index.js → 1920.index.js} +3 -3
- package/dist/{238.index.js → 2238.index.js} +3 -3
- package/dist/2271.index.js +165 -0
- package/dist/{985.index.js → 2376.index.js} +1260 -340
- package/dist/2432.index.js +793 -0
- package/dist/2659.index.js +93 -0
- package/dist/{826.index.js → 2826.index.js} +2 -2
- package/dist/{830.index.js → 2830.index.js} +2 -2
- package/dist/2923.index.js +298 -0
- package/dist/{1.index.js → 3001.index.js} +5 -5
- package/dist/{117.index.js → 3117.index.js} +3 -3
- package/dist/3180.index.js +307 -0
- package/dist/3276.index.js +117 -0
- package/dist/{415.index.js → 3415.index.js} +2 -2
- package/dist/{499.index.js → 3499.index.js} +2 -2
- package/dist/3518.index.js +450 -0
- package/dist/{526.index.js → 3526.index.js} +14 -6
- package/dist/{736.index.js → 3736.index.js} +4 -4
- package/dist/{839.index.js → 3839.index.js} +4 -4
- package/dist/{113.index.js → 4113.index.js} +14 -6
- package/dist/{265.index.js → 4265.index.js} +2 -2
- package/dist/{384.index.js → 4384.index.js} +3 -3
- package/dist/4547.index.js +268 -0
- package/dist/4863.index.js +422 -0
- package/dist/{970.index.js → 4970.index.js} +67 -3
- package/dist/5051.index.js +770 -0
- package/dist/{144.index.js → 5144.index.js} +5 -5
- package/dist/{333.index.js → 5333.index.js} +3 -3
- package/dist/5343.index.js +185 -0
- package/dist/5350.index.js +866 -0
- package/dist/5561.index.js +436 -0
- package/dist/{637.index.js → 5637.index.js} +29 -7
- package/dist/{449.index.js → 5830.index.js} +78 -14
- package/dist/6626.index.js +532 -0
- package/dist/6662.index.js +297 -0
- package/dist/{675.index.js → 6675.index.js} +5 -5
- package/dist/{730.index.js → 6730.index.js} +6 -6
- package/dist/6829.index.js +225 -0
- package/dist/6944.index.js +130 -0
- package/dist/{178.index.js → 7178.index.js} +26 -8
- package/dist/{227.index.js → 7227.index.js} +2 -2
- package/dist/7310.index.js +520 -0
- package/dist/{552.index.js → 7552.index.js} +4 -4
- package/dist/7709.index.js +78 -0
- package/dist/8218.index.js +160 -0
- package/dist/{476.index.js → 8476.index.js} +4 -4
- package/dist/{513.index.js → 8513.index.js} +5 -5
- package/dist/{520.index.js → 8520.index.js} +2 -2
- package/dist/{718.index.js → 8718.index.js} +2 -2
- package/dist/{752.index.js → 8752.index.js} +2 -2
- package/dist/8846.index.js +100 -0
- package/dist/{435.index.js → 9091.index.js} +580 -187
- package/dist/{207.index.js → 9207.index.js} +2 -2
- package/dist/{220.index.js → 9220.index.js} +2 -2
- package/dist/9390.index.js +163 -0
- package/dist/{503.index.js → 9503.index.js} +2 -2
- package/dist/{801.index.js → 9801.index.js} +2 -2
- package/dist/{824.index.js → 9824.index.js} +2 -2
- package/dist/agentic-security.mjs +16 -16
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/package.json +23 -10
- package/src/compare.js +6 -1
- package/src/dataflow/CLAUDE.md +2 -2
- package/src/dataflow/catalog.js +42 -0
- package/src/dataflow/orm-write-catalog.js +175 -0
- package/src/engine.js +580 -30
- package/src/fix/apply-fix-service.js +1 -0
- package/src/history-scan.js +22 -5
- package/src/ir/CLAUDE.md +2 -1
- package/src/ir/chrome-probe.mjs +150 -0
- package/src/ir/parser-js.js +94 -7
- package/src/lineage/CLAUDE.md +1203 -0
- package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
- package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
- package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
- package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
- package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
- package/src/lineage/DESIGN_REGISTRIES.md +880 -0
- package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
- package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
- package/src/lineage/classification.js +56 -0
- package/src/lineage/coverage.js +658 -0
- package/src/lineage/cross-repo-link.js +107 -0
- package/src/lineage/dataflow-graph.schema.json +184 -0
- package/src/lineage/decision-story.js +206 -0
- package/src/lineage/drift-policy.js +279 -0
- package/src/lineage/driver.js +135 -0
- package/src/lineage/engine.js +992 -0
- package/src/lineage/export-briefing.js +628 -0
- package/src/lineage/export-csv.js +62 -0
- package/src/lineage/export-json.js +238 -0
- package/src/lineage/export-privacy.js +258 -0
- package/src/lineage/federation-loader.js +111 -0
- package/src/lineage/field-identity.js +78 -0
- package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
- package/src/lineage/fixtures/flagship-graph.json +1453 -0
- package/src/lineage/flow-grade.js +221 -0
- package/src/lineage/governance-edit.js +169 -0
- package/src/lineage/graph-builder.js +1114 -0
- package/src/lineage/graph-diff.js +431 -0
- package/src/lineage/graph-snapshot.js +180 -0
- package/src/lineage/handling-analyzer.js +168 -0
- package/src/lineage/ids.js +349 -0
- package/src/lineage/impact-assessment.js +76 -0
- package/src/lineage/impact-engine.js +268 -0
- package/src/lineage/index.js +281 -0
- package/src/lineage/language-coverage-tiers.js +58 -0
- package/src/lineage/obligation-mapping.js +126 -0
- package/src/lineage/obligation-predicates.js +235 -0
- package/src/lineage/observation-adapters.js +282 -0
- package/src/lineage/observation-correlation.js +622 -0
- package/src/lineage/observation-store.js +497 -0
- package/src/lineage/path-query.js +410 -0
- package/src/lineage/path-store.js +400 -0
- package/src/lineage/protection.js +53 -0
- package/src/lineage/recipient-profile.js +192 -0
- package/src/lineage/recipient-registry.js +394 -0
- package/src/lineage/redact-graph.js +224 -0
- package/src/lineage/remediation.js +417 -0
- package/src/lineage/resolve-destination.js +91 -0
- package/src/lineage/runtime-observation.js +464 -0
- package/src/lineage/scenario-diff.js +84 -0
- package/src/lineage/scenario-engine.js +251 -0
- package/src/lineage/scenario.js +101 -0
- package/src/lineage/schema.js +167 -0
- package/src/lineage/sink-registry.js +427 -0
- package/src/lineage/source-registry.js +357 -0
- package/src/lineage/source-seeding.js +212 -0
- package/src/lineage/summaries.js +590 -0
- package/src/lineage/transform-catalog.js +397 -0
- package/src/lineage/transit-protection.js +150 -0
- package/src/lineage/validate.js +285 -0
- package/src/lsp/server.js +49 -2
- package/src/mcp/CLAUDE.md +7 -1
- package/src/mcp/dataflow-tools.js +160 -0
- package/src/mcp/server.js +1 -1
- package/src/mcp/tools.js +22 -1
- package/src/pipeline/assurance-mode.js +64 -1
- package/src/pipeline/finding-schema.js +8 -1
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/CLAUDE.md +140 -0
- package/src/posture/accuracy-scorecard.js +60 -0
- package/src/posture/artifact-registry.js +76 -0
- package/src/posture/auditor-walkthrough.js +192 -13
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
- 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/obligation-evidence-pack.js +202 -0
- 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/remediation-ledger.js +337 -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/server/CLAUDE.md +47 -0
- package/src/server/graph-loader.js +141 -0
- package/src/server/http-server.js +325 -0
- package/src/server/routes.js +129 -0
- package/src/server/security.js +111 -0
- package/src/server/static-assets.js +139 -0
- package/src/util/git-hardening.js +128 -0
- package/dist/11.index.js +0 -353
- package/dist/259.index.js +0 -975
- package/dist/317.index.js +0 -300
- package/dist/609.index.js +0 -741
- package/dist/838.index.js +0 -152
package/src/engine.js
CHANGED
|
@@ -206,6 +206,7 @@ import { applyPathConstraints } from './posture/path-predicates.js';
|
|
|
206
206
|
import { buildProjectIR, buildProjectIRAsync } from './ir/index.js';
|
|
207
207
|
import { collectIrStats, irStatsTarget, writeIrStats } from './ir/ir-stats.js';
|
|
208
208
|
import { runDeepAnalysis } from './dataflow/index.js';
|
|
209
|
+
import { buildLineageGraph } from './lineage/index.js';
|
|
209
210
|
// v3 next-gen — Pillars 1, 4, 5, 6, 8, 9.
|
|
210
211
|
import { annotateCloneClusters, findCloneOutliers } from './posture/semantic-clone.js';
|
|
211
212
|
import { annotateAiProvenance } from './posture/ai-code-fingerprint.js';
|
|
@@ -260,6 +261,17 @@ import { annotateAttackTaxonomy, summarizeTaxonomy } from './posture/attack-taxo
|
|
|
260
261
|
import { suppressByPastDecisions } from './posture/triage-memory.js';
|
|
261
262
|
import { suppressByIntent } from './posture/intent-context.js';
|
|
262
263
|
import { annotateGitHistory } from './posture/git-history.js';
|
|
264
|
+
// NOT `annotateProvenance` (sca/sigstore-verify.js's build-attestation
|
|
265
|
+
// annotator) and NOT `annotateFindingProvenance` (posture/provenance.js's
|
|
266
|
+
// AI-code fingerprint annotator). Both are already imported above in this
|
|
267
|
+
// file, so either name here is a duplicate binding — a SyntaxError — and the
|
|
268
|
+
// second is worse still because it also takes a findings array as its first
|
|
269
|
+
// argument, so a wrong import would RUN rather than fail. This one is named
|
|
270
|
+
// for the mechanism that distinguishes it —
|
|
271
|
+
// provenance derived from GIT HISTORY. See provenance/coordinator.js's header.
|
|
272
|
+
import { annotateGitProvenance, PROVENANCE_DEFAULT_TIMEOUT_MS, MAX_PROVIDER_ENRICHMENTS_PER_SCAN } from './posture/provenance/coordinator.js';
|
|
273
|
+
import { updateLifecycle } from './posture/provenance/lifecycle.js';
|
|
274
|
+
import { emptyProvenance, PROVENANCE_STATUS } from './posture/provenance/schema.js';
|
|
263
275
|
import { applyThreatModel } from './posture/threat-model-grounding.js';
|
|
264
276
|
import { annotateCrossRepoSignals } from './posture/pattern-propagation.js';
|
|
265
277
|
import { annotateRiskDollars } from './posture/risk-dollars.js';
|
|
@@ -2626,6 +2638,33 @@ function _resetSuppressions(){ _suppressionLog.length = 0; }
|
|
|
2626
2638
|
// so pfr[p] and the aggregates share object identity, exactly as in a normal run.
|
|
2627
2639
|
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
2640
|
function _getSuppressions(){ return [..._suppressionLog]; }
|
|
2641
|
+
// Task 11 reentrancy fix: `_suppressionLog` is module-level and unconditionally
|
|
2642
|
+
// cleared by `_resetSuppressions()` at the top of every `runFullScan` call.
|
|
2643
|
+
// `predicate-replay.js`'s `replayAt` calls `runFullScan` recursively FROM
|
|
2644
|
+
// WITHIN an outer, still-running scan's provenance resolution (to replay a
|
|
2645
|
+
// finding's predicate at a historical commit) -- before Task 11 wired
|
|
2646
|
+
// scan.secrets/scan.logicVulns into real provenance resolution, that recursive
|
|
2647
|
+
// call was only ever reachable from scan.findings/SCA origin walks, which this
|
|
2648
|
+
// exact fixture (test/fixtures/entropy-fp) never triggered. Wiring secrets in
|
|
2649
|
+
// exposed it for the first time: the nested call's `_resetSuppressions()`
|
|
2650
|
+
// silently wiped the OUTER scan's suppression log before its own return
|
|
2651
|
+
// statement read it via `_getSuppressions()`, so `scan.suppressions` came back
|
|
2652
|
+
// empty for anything that happened to walk deep git history.
|
|
2653
|
+
//
|
|
2654
|
+
// A plain snapshot/restore around ONE `replayAt` call is not sufficient on its
|
|
2655
|
+
// own: `coordinator.js` resolves several findings' origins CONCURRENTLY (its
|
|
2656
|
+
// own comment: "the scheduler runs these four at a time"), and each finding's
|
|
2657
|
+
// resolveOrigin walk can call `replayAt` multiple times sequentially -- so two
|
|
2658
|
+
// DIFFERENT findings' replay calls can be in flight at once, interleaved at
|
|
2659
|
+
// `runFullScan`'s own internal await points. Two overlapping snapshot/restore
|
|
2660
|
+
// pairs racing on the same global array means whichever restores last wins,
|
|
2661
|
+
// discarding whatever the other legitimately wrote in between. Exported so
|
|
2662
|
+
// predicate-replay.js can snapshot/restore its own call boundary AND serialize
|
|
2663
|
+
// that boundary process-wide (see its own comment on the exclusivity queue) --
|
|
2664
|
+
// the nested scan's own suppression output is never read by replayAt, so
|
|
2665
|
+
// nothing is lost by discarding it.
|
|
2666
|
+
function _snapshotSuppressionLog(){ return _suppressionLog.slice(); }
|
|
2667
|
+
function _restoreSuppressionLog(saved){ _suppressionLog.length = 0; if (Array.isArray(saved)) _suppressionLog.push(...saved); }
|
|
2629
2668
|
|
|
2630
2669
|
// ── inline suppression pragma ───────────────────────────────────────────────
|
|
2631
2670
|
//
|
|
@@ -7340,6 +7379,31 @@ function _makePurl(ecosystem,name,version,group){
|
|
|
7340
7379
|
return`pkg:${t}/${ns}${encodeURIComponent(name)}${version?'@'+encodeURIComponent(version):''}`;
|
|
7341
7380
|
}
|
|
7342
7381
|
|
|
7382
|
+
function _findManifestLine(text, sectionKey, depName) {
|
|
7383
|
+
const lines = text.split('\n');
|
|
7384
|
+
let inSection = false;
|
|
7385
|
+
let depth = 0;
|
|
7386
|
+
const escaped = depName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
7387
|
+
const nameRe = new RegExp(`"${escaped}"\\s*:`);
|
|
7388
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7389
|
+
const line = lines[i];
|
|
7390
|
+
if (!inSection) {
|
|
7391
|
+
if (new RegExp(`"${sectionKey}"\\s*:\\s*\\{`).test(line)) {
|
|
7392
|
+
inSection = true;
|
|
7393
|
+
depth = 1 + (line.match(/\{/g) || []).length - 1 - (line.match(/\}/g) || []).length;
|
|
7394
|
+
if (nameRe.test(line)) return i + 1;
|
|
7395
|
+
if (depth <= 0) inSection = false;
|
|
7396
|
+
}
|
|
7397
|
+
continue;
|
|
7398
|
+
}
|
|
7399
|
+
depth += (line.match(/\{/g) || []).length;
|
|
7400
|
+
depth -= (line.match(/\}/g) || []).length;
|
|
7401
|
+
if (depth <= 0) { inSection = false; continue; }
|
|
7402
|
+
if (nameRe.test(line)) return i + 1;
|
|
7403
|
+
}
|
|
7404
|
+
return null;
|
|
7405
|
+
}
|
|
7406
|
+
|
|
7343
7407
|
function _parsePackageJson(text,filePath){
|
|
7344
7408
|
const out=[];try{const d=JSON.parse(text);
|
|
7345
7409
|
for(const[depKey,scope]of[['dependencies','required'],['devDependencies','optional']]){
|
|
@@ -7350,7 +7414,8 @@ function _parsePackageJson(text,filePath){
|
|
|
7350
7414
|
const group=scoped?`@${parts[0]}`:'';
|
|
7351
7415
|
const pkgName=scoped?parts[1]:name;
|
|
7352
7416
|
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'
|
|
7417
|
+
isUnpinned:verRange==='*'||verRange==='latest'||verRange===''||verRange==='>=0.0.0',
|
|
7418
|
+
line:_findManifestLine(text,depKey,name)});
|
|
7354
7419
|
}
|
|
7355
7420
|
}
|
|
7356
7421
|
}catch(_){}return out;
|
|
@@ -7379,12 +7444,13 @@ function _parsePackageLockJson(text,filePath){
|
|
|
7379
7444
|
|
|
7380
7445
|
function _parseRequirementsTxt(text,filePath){
|
|
7381
7446
|
const out=[];
|
|
7382
|
-
|
|
7383
|
-
|
|
7447
|
+
const lines=text.split('\n');
|
|
7448
|
+
for(let i=0;i<lines.length;i++){
|
|
7449
|
+
const t=lines[i].trim();
|
|
7384
7450
|
if(!t||t.startsWith('#')||t.startsWith('-'))continue;
|
|
7385
7451
|
const m=t.match(/^([A-Za-z0-9_.-]+)\s*[=~<>!]+\s*([^\s;#,]*)/);
|
|
7386
7452
|
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});
|
|
7453
|
+
purl:_makePurl('pypi',m[1].toLowerCase(),m[2],''),ecosystem:'pypi',filePath,isUnpinned:false,line:i+1});
|
|
7388
7454
|
}return out;
|
|
7389
7455
|
}
|
|
7390
7456
|
|
|
@@ -8180,6 +8246,15 @@ async function queryOSV(components,allFileContents){
|
|
|
8180
8246
|
fixedVersions: vuln.fixedVersions, severity: vuln.severity, cvssVector: vuln.cvssVector,
|
|
8181
8247
|
hasKnownAttackRef: vuln.hasKnownAttackRef, osvVulnFunctions: vuln.osvVulnFunctions || [], reachable: comp.reachable, scope: comp.scope,
|
|
8182
8248
|
file: comp.filePath,
|
|
8249
|
+
// `isDirect` is backfilled onto every component just above the queryOSV
|
|
8250
|
+
// call, but was never carried onto the entry materialized from it — so
|
|
8251
|
+
// every consumer asking "is this a direct dependency" got `undefined`.
|
|
8252
|
+
// The transitive-dedup block's `group.find(s => s.isDirect)` has
|
|
8253
|
+
// therefore always fallen through to `group[0]`, picking an arbitrary
|
|
8254
|
+
// member as the primary instead of the direct one, and the provenance
|
|
8255
|
+
// pass's direct-only filter had nothing to filter on. `line` is Task
|
|
8256
|
+
// 12's declaration line, which the SCA provenance evidence node reads.
|
|
8257
|
+
isDirect: comp.isDirect, line: comp.line, depChain: Array.isArray(comp.depChain) ? comp.depChain.map((s) => s.replace(/\/$/, '')) : [],
|
|
8183
8258
|
// kept for generateRecs() compat
|
|
8184
8259
|
advisory: `${vid}${cveStr}, ${vuln.description}`,
|
|
8185
8260
|
range: fixStr ? `< ${fixStr}` : 'see advisory' });
|
|
@@ -8526,7 +8601,28 @@ async function queryRegistries(components){
|
|
|
8526
8601
|
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
8602
|
}
|
|
8528
8603
|
|
|
8529
|
-
|
|
8604
|
+
// `provenance:false` is the RE-ENTRANCY BRAKE, not a feature flag.
|
|
8605
|
+
// posture/provenance/predicate-replay.js answers "did this finding's condition
|
|
8606
|
+
// hold at commit X" by calling runFullScan back on that commit's blobs. Once
|
|
8607
|
+
// runFullScan itself runs the provenance pass, that is an unbounded recursion —
|
|
8608
|
+
// scan → provenance → replay → scan → … — which manifests as a scan that never
|
|
8609
|
+
// returns and spawns `git` forever, because every level of it is synchronous
|
|
8610
|
+
// execFileSync work. The replay's findings are discarded, so it wants no
|
|
8611
|
+
// provenance anyway; and it must NOT touch the lifecycle store, whose events
|
|
8612
|
+
// would otherwise be written from historical blobs as if they were this scan.
|
|
8613
|
+
// Passed explicitly per invocation rather than held in a module-level guard so
|
|
8614
|
+
// concurrent scans in one process cannot disable each other's provenance.
|
|
8615
|
+
//
|
|
8616
|
+
// `completeScan` is a SEPARATE question from `provenance`, and conflating them
|
|
8617
|
+
// is what let the fourth instance of this bug through. `provenance:false` says
|
|
8618
|
+
// "do not run the pass at all"; `completeScan:false` says "the pass may run,
|
|
8619
|
+
// but this file set is a SUBSET of scanRoot, so absence of a finding proves
|
|
8620
|
+
// nothing." Only the lifecycle ledger's remediation pass reads it — that is the
|
|
8621
|
+
// one place a finding's absence is turned into a positive claim. Defaults true
|
|
8622
|
+
// because a direct runFullScan caller supplying no file-subsetting options is
|
|
8623
|
+
// scanning everything it was given; runScan.js narrows it for --changed-since
|
|
8624
|
+
// and for caller-supplied fileContents.
|
|
8625
|
+
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
8626
|
// Pre-pass: build cross-file Java tainted-method index so per-file taint
|
|
8531
8627
|
// analysis can recognize calls to user-input-returning helper methods
|
|
8532
8628
|
// defined in OTHER files (Juliet's DataflowThruInnerClass / Vector / Stream
|
|
@@ -9134,6 +9230,95 @@ function _deterministicFileTimings(timings) {
|
|
|
9134
9230
|
: (_inCi ? 'not requested (deep analysis defaults to off in CI)' : 'not requested')),
|
|
9135
9231
|
failure: _deepFailure,
|
|
9136
9232
|
};
|
|
9233
|
+
// Sub-project E, increment 5 — Data Flow Explorer lineage graph.
|
|
9234
|
+
// DELIBERATELY INDEPENDENT of AGENTIC_SECURITY_DEEP/_deepEnabled above
|
|
9235
|
+
// (see src/lineage/DESIGN_GRAPH_BUILDER.md §9.5): lineage analysis has no
|
|
9236
|
+
// degraded/non-IR-backed mode, so gating it on deep mode ALSO being on
|
|
9237
|
+
// would make AGENTIC_SECURITY_LINEAGE_DEEP=1 alone silently produce
|
|
9238
|
+
// nothing whenever deep mode itself was off.
|
|
9239
|
+
//
|
|
9240
|
+
// Undisclosed-until-final-review side effect (N-4): this block populates
|
|
9241
|
+
// the SAME `_sharedIR` memo the privacy-taint block further below reads
|
|
9242
|
+
// (`_privacyIrBacked = !!(_sharedIR && _sharedIR.perFile)`), so setting
|
|
9243
|
+
// AGENTIC_SECURITY_LINEAGE_DEEP=1 also upgrades privacy-taint annotation
|
|
9244
|
+
// to IR-backed (`privacyIrBacked: true` instead of `false`) even with
|
|
9245
|
+
// AGENTIC_SECURITY_DEEP itself off. A real, deliberate-once-noticed
|
|
9246
|
+
// consequence of the memo sharing, not a bug — but worth knowing about
|
|
9247
|
+
// before being surprised by it.
|
|
9248
|
+
const _lineageRequested = process.env.AGENTIC_SECURITY_LINEAGE_DEEP === '1';
|
|
9249
|
+
const _lineageStatus = {
|
|
9250
|
+
requested: _lineageRequested,
|
|
9251
|
+
enabled: false,
|
|
9252
|
+
reason: _lineageRequested ? null : 'not requested',
|
|
9253
|
+
failure: null,
|
|
9254
|
+
};
|
|
9255
|
+
let _lineageGraph = null;
|
|
9256
|
+
if (_lineageRequested) {
|
|
9257
|
+
_lineageStatus.enabled = true;
|
|
9258
|
+
const _lineageBudgetMs = parseInt(process.env.AGENTIC_SECURITY_LINEAGE_TIMEOUT_MS || '300000', 10);
|
|
9259
|
+
try {
|
|
9260
|
+
// Reuses the SAME _sharedIR memo _deepEnabled's own block above uses —
|
|
9261
|
+
// if deep mode already built it, this is free; if not, this is what
|
|
9262
|
+
// first triggers the build (the whole reason this gate is independent).
|
|
9263
|
+
const { perFile, callGraph } = _sharedIR || (_sharedIR = await _buildIR());
|
|
9264
|
+
const _lr = buildLineageGraph(callGraph, {
|
|
9265
|
+
// scanRoot can be null (e.g. an MCP scan_diff caller, or
|
|
9266
|
+
// runFullScan invoked directly with no scanRoot) — fall back to
|
|
9267
|
+
// undefined rather than inventing a repository name from the
|
|
9268
|
+
// process's own CWD basename, which would make lineage node ids
|
|
9269
|
+
// depend on wherever the process happened to be launched from and
|
|
9270
|
+
// collide across two unrelated repos sharing a directory basename.
|
|
9271
|
+
// `undefined` lets graph-builder.js's own `opts.repository ?? 'repo'`
|
|
9272
|
+
// default apply — the single source of truth for that literal.
|
|
9273
|
+
repository: scanRoot ? path.basename(path.resolve(scanRoot)) : undefined,
|
|
9274
|
+
// Milestone 2, Sub-project G, increment 1 (FR-408/AC-09): the real
|
|
9275
|
+
// scan root, threaded straight through — distinct from `repository`
|
|
9276
|
+
// above, which is only a basename. `index.js`'s own `buildLineageGraph`
|
|
9277
|
+
// uses this ONLY to load `.agentic-security/privacy-policy.json`
|
|
9278
|
+
// (never to derive `repository`/node ids), mirroring how `fileContents`
|
|
9279
|
+
// was threaded through for Sub-project B's own transit-evidence scan.
|
|
9280
|
+
scanRoot,
|
|
9281
|
+
deterministic: isDeterministic(),
|
|
9282
|
+
perFile,
|
|
9283
|
+
fileContents,
|
|
9284
|
+
});
|
|
9285
|
+
if (_lr.status === 'complete') {
|
|
9286
|
+
_lineageGraph = _lr.graph;
|
|
9287
|
+
if (_lr.elapsedMs > _lineageBudgetMs) {
|
|
9288
|
+
// Same "measured, not truly interrupted" disclosure _deepEnabled's
|
|
9289
|
+
// own elapsed > budgetMs branch already makes for IR-taint — see
|
|
9290
|
+
// src/lineage/DESIGN_GRAPH_BUILDER.md §9.5.
|
|
9291
|
+
aF.push({
|
|
9292
|
+
id: `lineage-timeout:${scanRoot || ''}`,
|
|
9293
|
+
file: '(lineage-engine)', line: 0,
|
|
9294
|
+
vuln: `Lineage graph build exceeded ${_lineageBudgetMs}ms budget (${_lr.elapsedMs}ms used) — results may be incomplete`,
|
|
9295
|
+
severity: 'info',
|
|
9296
|
+
parser: 'LINEAGE',
|
|
9297
|
+
confidence: 0.5,
|
|
9298
|
+
});
|
|
9299
|
+
}
|
|
9300
|
+
} else if (_lr.status === 'failed') {
|
|
9301
|
+
_lineageStatus.failure = _lr.failure;
|
|
9302
|
+
}
|
|
9303
|
+
// status === 'not_available' needs no extra handling: _lineageGraph
|
|
9304
|
+
// stays null, and enabled:true/failure:null correctly reads as "ran,
|
|
9305
|
+
// produced nothing" (a genuinely empty/malformed callGraph), distinct
|
|
9306
|
+
// from "never ran" (requested:false).
|
|
9307
|
+
} catch (e) {
|
|
9308
|
+
// Lineage is best-effort, mirroring _deepEnabled's own catch above
|
|
9309
|
+
// (engine.js, `_deepFailure = String((e && e.message) || e);`):
|
|
9310
|
+
// anything OUTSIDE buildLineageGraph's own try/catch — the _buildIR()
|
|
9311
|
+
// call itself, path.resolve, etc. — must never kill the scan. Without
|
|
9312
|
+
// this, a throw here (e.g. from buildCallGraph, which
|
|
9313
|
+
// buildProjectIR/buildProjectIRAsync do NOT guard per-file the way
|
|
9314
|
+
// per-file parsing is guarded) previously took down the entire scan
|
|
9315
|
+
// the moment AGENTIC_SECURITY_LINEAGE_DEEP=1 was set — see
|
|
9316
|
+
// src/lineage/DESIGN_GRAPH_BUILDER.md §9.5 item 1 and the Global
|
|
9317
|
+
// Constraint this violated ("a lineage-build failure must never fail
|
|
9318
|
+
// the scan"). Regression-tested in test/lineage-fault-injection.test.js.
|
|
9319
|
+
_lineageStatus.failure = String((e && e.message) || e);
|
|
9320
|
+
}
|
|
9321
|
+
}
|
|
9137
9322
|
// Java SCA enrichment: use deep-mode IR call graph to improve Java function reachability
|
|
9138
9323
|
if (_deepCallGraph) {
|
|
9139
9324
|
try {
|
|
@@ -9276,9 +9461,76 @@ function _deterministicFileTimings(timings) {
|
|
|
9276
9461
|
// Every catch in this block writes into _annotatorErrors so the operator
|
|
9277
9462
|
// can tell "didn't run" from "ran cleanly." The array is surfaced as
|
|
9278
9463
|
// scan.annotatorErrors in the report; an empty array means clean.
|
|
9464
|
+
//
|
|
9465
|
+
// FR-PROV-029 (Finding Provenance PRD): `skipAnnotators` lets a caller skip
|
|
9466
|
+
// this entire ~54-annotator pipeline below. The guard also covers
|
|
9467
|
+
// non-annotator finalization that lives in the same block — entropy-vs-
|
|
9468
|
+
// named secret dedup, orphan classification, supply-chain in-place
|
|
9469
|
+
// filtering, `Object.freeze(finalFindings)`, and closing the resume
|
|
9470
|
+
// checkpoint — so a skipped run leaves all of that undone too; name the
|
|
9471
|
+
// option accordingly if a future caller needs annotators skipped WITHOUT
|
|
9472
|
+
// skipping those steps. predicate-replay.js's `replayAt()` is the only
|
|
9473
|
+
// caller that sets it — it re-runs runFullScan scoped to a historical
|
|
9474
|
+
// commit's blob content purely to recompute `computeStableId()` over the
|
|
9475
|
+
// raw detector output (`scan.findings`/`scan.secrets`); it never reads
|
|
9476
|
+
// anything an annotator sets (verified empirically — see the commit
|
|
9477
|
+
// message). Every binding the pipeline below
|
|
9478
|
+
// populates that the final `return` still references is declared here,
|
|
9479
|
+
// OUTSIDE the guarded block, defaulted to exactly what it was before any
|
|
9480
|
+
// annotator ran. A skipped run returns those fields at their
|
|
9481
|
+
// pre-annotation default instead of throwing a ReferenceError; a normal
|
|
9482
|
+
// (non-skipping) run is byte-for-byte unaffected, since the guarded block
|
|
9483
|
+
// below still assigns the same values to these same bindings — it just no
|
|
9484
|
+
// longer *declares* them, so nothing here changes what a value ends up
|
|
9485
|
+
// being, only where the variable comes into scope.
|
|
9279
9486
|
let _executionProofSummary = null, _vulnHistory = null;
|
|
9280
9487
|
let _logicClaims = null;
|
|
9281
|
-
|
|
9488
|
+
let _annotatorErrors = [];
|
|
9489
|
+
let _v3 = {};
|
|
9490
|
+
let _privacyIrBacked = null;
|
|
9491
|
+
let _privacyTaxonomyVersion = null;
|
|
9492
|
+
let _privacyFramework = null;
|
|
9493
|
+
let _threatModel = null, _apiContractFindings = [], _sbomDiff = null,
|
|
9494
|
+
_complianceReport = null, _exploitBundles = null, _pqcPlan = null,
|
|
9495
|
+
_licenseGraph = null, _attributions = null, _taxonomySummary = null;
|
|
9496
|
+
let _scanMeta = null;
|
|
9497
|
+
let _entrypointInventory = {};
|
|
9498
|
+
let _rootCauseSweep = null;
|
|
9499
|
+
let _proofCoverage = null;
|
|
9500
|
+
let _coverageLedger = null;
|
|
9501
|
+
let _scanHealth = null;
|
|
9502
|
+
// Task 11 (PRD P0 scope): ruleId backfill for scan.secrets / blameable
|
|
9503
|
+
// scan.logicVulns findings MUST run unconditionally, HERE, outside the
|
|
9504
|
+
// `skipAnnotators` guard below -- not just because the live scan needs it,
|
|
9505
|
+
// but because predicate-replay.js's replayAt() recurses into THIS function
|
|
9506
|
+
// with skipAnnotators:true and recomputes computeStableId() directly on
|
|
9507
|
+
// whatever it finds in the nested scan's own scan.secrets/scan.logicVulns.
|
|
9508
|
+
// If the backfill only ran on the live (skipAnnotators:false) call, the
|
|
9509
|
+
// nested replay scan would compute a DIFFERENT stableId (falling back to
|
|
9510
|
+
// the shared f.cwe -- e.g. every secret type collapsing onto "CWE-798")
|
|
9511
|
+
// than the live scan's already-backfilled finding, so replayAt's
|
|
9512
|
+
// `sid === targetStableId` check would NEVER match -- permanently landing
|
|
9513
|
+
// every secrets/logicVulns finding on status:'partial',
|
|
9514
|
+
// reason:'predicate-never-confirmed-in-candidates'. Caught empirically:
|
|
9515
|
+
// test/fixtures/entropy-fp's AWS-key fixture resolved 'partial' instead of
|
|
9516
|
+
// 'complete' until this moved here from inside the (skipAnnotators-gated)
|
|
9517
|
+
// provenance block further down.
|
|
9518
|
+
//
|
|
9519
|
+
// At this point in the function, aSecrets/aLogic hold every BLAMEABLE
|
|
9520
|
+
// producer's output (scanCredentials/scanEntropySecrets;
|
|
9521
|
+
// scanLogicVulns/scanBusinessLogic/scanMiddlewareOrdering/scanReDoS/
|
|
9522
|
+
// scanRegexReDoS/scanTodosNearSecurity/scanConfigFiles) -- the 3 synthetic
|
|
9523
|
+
// producers (license-policy:/deploy-platform:/stack-playbook:) and
|
|
9524
|
+
// logic-claims.js's ingested claims are pushed LATER, inside the
|
|
9525
|
+
// `skipAnnotators` guard below, so they are never present in a nested
|
|
9526
|
+
// replay scan's aLogic and never need this backfill for replay-matching
|
|
9527
|
+
// purposes. The provenance block further down re-applies this same
|
|
9528
|
+
// idempotent backfill to the full, final `blameableLogic` (which by then
|
|
9529
|
+
// includes logic-claims too) before calling annotateGitProvenance on it.
|
|
9530
|
+
const _slugify = (s) => String(s || 'unknown').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'unknown';
|
|
9531
|
+
for (const f of aSecrets) { if (!f.ruleId) f.ruleId = `secret:${_slugify(f.vuln)}`; }
|
|
9532
|
+
for (const f of aLogic) { if (!f.ruleId) f.ruleId = `logic:${_slugify(f.vuln)}`; }
|
|
9533
|
+
if (!skipAnnotators) {
|
|
9282
9534
|
// FR-106 (assurance-hardening PRD): Promise-aware, explicitly awaited at
|
|
9283
9535
|
// every one of its ~51 call sites below (previously a sync `try{return
|
|
9284
9536
|
// fn()}` let an async callback's rejection escape as an unhandled
|
|
@@ -9384,18 +9636,18 @@ function _deterministicFileTimings(timings) {
|
|
|
9384
9636
|
// FR-405 (assurance-hardening PRD): null means "privacy analysis never
|
|
9385
9637
|
// ran at all" (AGENTIC_SECURITY_NO_PRIVACY=1, or the annotator threw
|
|
9386
9638
|
// 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
|
-
|
|
9639
|
+
// since neither case has real IR-backed evidence to offer. Declared at
|
|
9640
|
+
// function scope (now hoisted above the `skipAnnotators` guard, near
|
|
9641
|
+
// _executionProofSummary et al. — same reasoning: FR-PROV-029), because
|
|
9642
|
+
// the annotatePrivacyTaint closure that assigns to it (inside the
|
|
9643
|
+
// AGENTIC_SECURITY_NO_INTEGRATION block below) runs and exits before that
|
|
9644
|
+
// block closes — a block-scoped `let` inside that if-statement would be
|
|
9645
|
+
// unreachable by the later assessPrivacyFramework call and the final
|
|
9646
|
+
// return, both of which are outside the block.
|
|
9647
|
+
//
|
|
9648
|
+
// FR-402: _privacyTaxonomyVersion (which taxonomy version actually
|
|
9649
|
+
// classified this scan's fields) has the same scoping constraint as
|
|
9650
|
+
// _privacyIrBacked directly above (D-0011) and is hoisted alongside it.
|
|
9399
9651
|
|
|
9400
9652
|
// ── World-class integration block ─────────────────────────────────────
|
|
9401
9653
|
// Each annotator is opt-in via env var and try/catch wrapped. They run
|
|
@@ -9894,7 +10146,7 @@ function _deterministicFileTimings(timings) {
|
|
|
9894
10146
|
classifyOrphans(aSrc,aSink,finalFindings,fc);
|
|
9895
10147
|
// v3 next-gen: capture scan-level reports (counterfactual, threat model,
|
|
9896
10148
|
// trust-boundary diagram, calibration-drift alarms). All best-effort.
|
|
9897
|
-
|
|
10149
|
+
// (_v3 is hoisted above the `skipAnnotators` guard — FR-PROV-029.)
|
|
9898
10150
|
await _runAnnotator("_v3.counterfactual", () => { _v3.counterfactual = runCounterfactual(finalFindings, fc); });
|
|
9899
10151
|
await _runAnnotator("_v3.threatModel", () => { _v3.threatModel = buildThreatModel(finalFindings, fc); });
|
|
9900
10152
|
await _runAnnotator("_v3.trustBoundaryDiagram", () => { _v3.trustBoundaryDiagram = buildTrustBoundaryDiagram(finalFindings, fc); });
|
|
@@ -9912,10 +10164,9 @@ function _deterministicFileTimings(timings) {
|
|
|
9912
10164
|
// Each is opt-in via env var. They produce machine-readable artifacts
|
|
9913
10165
|
// (threat-model.json/.md, dpia.md, compliance-evidence.json/.md,
|
|
9914
10166
|
// sbom-history/<sha>.json, exploit-bundles/) under .agentic-security/.
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
_licenseGraph = null, _attributions = null, _taxonomySummary = null;
|
|
10167
|
+
// (_privacyFramework/_threatModel/_apiContractFindings/_sbomDiff/
|
|
10168
|
+
// _complianceReport/_exploitBundles/_pqcPlan/_licenseGraph/_attributions/
|
|
10169
|
+
// _taxonomySummary are hoisted above the `skipAnnotators` guard — FR-PROV-029.)
|
|
9919
10170
|
if (process.env.AGENTIC_SECURITY_NO_INTEGRATION !== '1') {
|
|
9920
10171
|
// Threat model — STRIDE + entities + attack trees rooted in findings.
|
|
9921
10172
|
if (process.env.AGENTIC_SECURITY_NO_THREAT_MODEL !== '1') {
|
|
@@ -10083,12 +10334,307 @@ function _deterministicFileTimings(timings) {
|
|
|
10083
10334
|
// seen when only N-of-those-candidates were actually analyzed.
|
|
10084
10335
|
// checkpoint.total intentionally keeps files.length — that field means the
|
|
10085
10336
|
// full candidate set for resume bookkeeping, a different, correct meaning.
|
|
10086
|
-
|
|
10337
|
+
_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
10338
|
// R8: the scan completed, so the checkpoint has been fully consumed — remove
|
|
10088
10339
|
// it. Anything that threw before this point leaves it in place to resume from.
|
|
10089
10340
|
try { closeCheckpoint(_ckpt, { complete: true }); } catch (_) {}
|
|
10341
|
+
// Finding Provenance (M0/M1) — attaches `finding.findingProvenance`.
|
|
10342
|
+
//
|
|
10343
|
+
// Placed HERE, not immediately after the SCA/multi-sink correlation blocks
|
|
10344
|
+
// where the plan first put it, for the same reason `annotateRelevance` below
|
|
10345
|
+
// runs this late: several post-scan artifact emitters between those blocks
|
|
10346
|
+
// and this point still PUSH findings (`runApiContractScan`, `runSbomDiff`,
|
|
10347
|
+
// the cross-language chain passes). Annotating before them would have left
|
|
10348
|
+
// every finding they produce with no `findingProvenance` at all, which is
|
|
10349
|
+
// precisely the "absent field" state the coordinator's terminal-status
|
|
10350
|
+
// guarantee exists to make unreachable. Running after `Object.freeze` above
|
|
10351
|
+
// is safe and is in fact the point: the finding SET can no longer change, and
|
|
10352
|
+
// a shallow freeze still permits annotating FIELDS on the individual finding
|
|
10353
|
+
// objects. This annotator never appends or drops a finding.
|
|
10354
|
+
if (provenance !== false) {
|
|
10355
|
+
// Task 11 (PRD P0 scope): real origin resolution for scan.secrets and
|
|
10356
|
+
// blameable scan.logicVulns findings. Both channels were previously
|
|
10357
|
+
// stamped an unconditional not_available (see the backstop loop below) —
|
|
10358
|
+
// not because computeStableId can't handle them (its ruleId() fallback
|
|
10359
|
+
// chain already tolerates a missing f.ruleId), but because nobody
|
|
10360
|
+
// backfilled a real per-pattern ruleId and nobody called
|
|
10361
|
+
// annotateGitProvenance on these channels at all. scan.secrets findings
|
|
10362
|
+
// set neither ruleId nor family nor parser, and every secret type shares
|
|
10363
|
+
// the same fixed f.cwe ("CWE-798"), so without a per-pattern backfill
|
|
10364
|
+
// every secret in a scan would collide onto ONE stableId.
|
|
10365
|
+
//
|
|
10366
|
+
// scan.logicVulns is not one detector's output — three of its ~9
|
|
10367
|
+
// producers (license-policy:, deploy-platform:, stack-playbook:) use a
|
|
10368
|
+
// FIXED PLACEHOLDER `line` (0 or 1), not a real diffable source location
|
|
10369
|
+
// — they read scanRoot-level files (package.json, vercel.json, ...)
|
|
10370
|
+
// directly rather than from the scanned fileContents. Routing those
|
|
10371
|
+
// through git-blame-style resolution would fabricate a plausible-looking
|
|
10372
|
+
// but meaningless commit attribution (e.g. "package.json line 1" blamed
|
|
10373
|
+
// on whatever commit last touched that line, unrelated to the actual
|
|
10374
|
+
// license/platform/stack finding). They are excluded by id prefix here
|
|
10375
|
+
// and stay on the honest, PERMANENT not_available path in the backstop
|
|
10376
|
+
// loop below — never routed through resolveOrigin. Declared here, OUTSIDE
|
|
10377
|
+
// the _runAnnotator callback below, because the backstop loop (also
|
|
10378
|
+
// outside that callback — see its own comment on why) needs
|
|
10379
|
+
// `blameableLogic`/`syntheticLogic` too.
|
|
10380
|
+
//
|
|
10381
|
+
// `aSecrets`/`aLogic`'s ruleId backfill for the producers that exist by
|
|
10382
|
+
// this point in the function already happened much earlier (right before
|
|
10383
|
+
// the `skipAnnotators` guard opens — see that comment for why it CANNOT
|
|
10384
|
+
// live here alone). `_slugify` is declared there and is in scope here too.
|
|
10385
|
+
// What's re-applied below is only the SAME idempotent backfill
|
|
10386
|
+
// (`if (!f.ruleId)`), now over the full, final `blameableLogic` — which by
|
|
10387
|
+
// this point additionally includes logic-claims.js's late-pushed ingested
|
|
10388
|
+
// claims, the one blameable producer the early pass could not see.
|
|
10389
|
+
const SYNTHETIC_LOGIC_PREFIXES = ['license-policy:', 'deploy-platform:', 'stack-playbook:'];
|
|
10390
|
+
const isSyntheticLogicFinding = (f) => typeof f?.id === 'string'
|
|
10391
|
+
&& SYNTHETIC_LOGIC_PREFIXES.some((p) => f.id.startsWith(p));
|
|
10392
|
+
const blameableLogic = aLogic.filter((f) => !isSyntheticLogicFinding(f));
|
|
10393
|
+
const syntheticLogic = aLogic.filter(isSyntheticLogicFinding);
|
|
10394
|
+
for (const f of blameableLogic) {
|
|
10395
|
+
if (!f.ruleId) f.ruleId = `logic:${_slugify(f.vuln)}`;
|
|
10396
|
+
}
|
|
10397
|
+
annotateStableIds(aSecrets);
|
|
10398
|
+
annotateStableIds(blameableLogic);
|
|
10399
|
+
await _runAnnotator("annotateGitProvenance", async () => {
|
|
10400
|
+
// ONE deadline for the whole scan's provenance work, computed here and
|
|
10401
|
+
// threaded into all five annotateGitProvenance calls below. Computed per call
|
|
10402
|
+
// inside the coordinator, the effective scan-level budget was 2× the
|
|
10403
|
+
// configured --provenance-timeout: the SAST pass got a fresh window and
|
|
10404
|
+
// then the SCA pass got another one, so an operator asking for a 30s cap
|
|
10405
|
+
// could wait 60s. The spec describes a single global deadline; this is
|
|
10406
|
+
// where "global" has to be established, because this is the only scope
|
|
10407
|
+
// that sees both passes.
|
|
10408
|
+
const provenanceTimeoutMs = process.env.AGENTIC_SECURITY_PROVENANCE_TIMEOUT_MS
|
|
10409
|
+
? parseInt(process.env.AGENTIC_SECURITY_PROVENANCE_TIMEOUT_MS, 10) : undefined;
|
|
10410
|
+
// Deterministic mode promises byte-identical SARIF run-to-run
|
|
10411
|
+
// (posture/deterministic.js), and `observedAt` above is already frozen to
|
|
10412
|
+
// honour that. But `findingProvenance.status` (complete/partial/
|
|
10413
|
+
// budget_exhausted/...) is driven by THIS deadline, which was computed
|
|
10414
|
+
// from a real `Date.now()` even under --deterministic — so two runs of
|
|
10415
|
+
// the identical scan could cross a 60s budget at different points under
|
|
10416
|
+
// machine contention (git subprocess calls slowed by CPU/IO pressure) and
|
|
10417
|
+
// land different findings on `budget_exhausted` vs a resolved status,
|
|
10418
|
+
// changing the emitted SARIF between runs. An explicit
|
|
10419
|
+
// --provenance-timeout / AGENTIC_SECURITY_PROVENANCE_TIMEOUT_MS still wins
|
|
10420
|
+
// (an operator asking for a tight budget gets it regardless of mode);
|
|
10421
|
+
// absent that, deterministic mode gets a much larger fixed ceiling so
|
|
10422
|
+
// resolution has room to finish under realistic load instead of a bound
|
|
10423
|
+
// that's routinely crossed — see test/proof-corpus-lib.test.js's
|
|
10424
|
+
// "produces byte-identical SARIF across two runs".
|
|
10425
|
+
const DETERMINISTIC_PROVENANCE_TIMEOUT_MS = 300000; // 5 minutes
|
|
10426
|
+
const provenanceDeadlineAt = Date.now()
|
|
10427
|
+
+ (Number.isFinite(provenanceTimeoutMs) && provenanceTimeoutMs > 0
|
|
10428
|
+
? provenanceTimeoutMs
|
|
10429
|
+
: (isDeterministic() ? DETERMINISTIC_PROVENANCE_TIMEOUT_MS : PROVENANCE_DEFAULT_TIMEOUT_MS));
|
|
10430
|
+
const provenanceCtx = {
|
|
10431
|
+
scanRoot,
|
|
10432
|
+
deadlineAt: provenanceDeadlineAt,
|
|
10433
|
+
// CLI flags that set these land in Task 17; reading the env directly here
|
|
10434
|
+
// mirrors AGENTIC_SECURITY_NO_GIT_HISTORY's existing pattern in this file.
|
|
10435
|
+
disabled: process.env.AGENTIC_SECURITY_NO_PROVENANCE === '1',
|
|
10436
|
+
scanId: process.env.AGENTIC_SECURITY_SCAN_ID || null,
|
|
10437
|
+
// Frozen under --deterministic so SARIF (which now carries
|
|
10438
|
+
// findingProvenance.firstObserved.observedAt — see report/index.js's
|
|
10439
|
+
// toSARIF) stays byte-identical run-to-run, matching the exact
|
|
10440
|
+
// convention posture/deterministic.js's makeDeterministic() already
|
|
10441
|
+
// uses for meta.startedAt. This value predates that guarantee; it was
|
|
10442
|
+
// invisible before findingProvenance reached any output format.
|
|
10443
|
+
observedAt: isDeterministic() ? '1970-01-01T00:00:00.000Z' : new Date().toISOString(),
|
|
10444
|
+
// FR-PROV-028 / "Evidence integrity": the cache key (coordinator.js's
|
|
10445
|
+
// `makeCacheKey`) and `computeDigest`'s `rulesetVersion` binding both
|
|
10446
|
+
// read this field, so it has to be the REAL effective ruleset version,
|
|
10447
|
+
// not an env var operators essentially never set. `_effectiveRulesetVersion`
|
|
10448
|
+
// (posture/ruleset-version.js's `effectiveVersion`) already resolves
|
|
10449
|
+
// env override > pinned file > CURRENT_RULESET_VERSION (== the running
|
|
10450
|
+
// scanner's own package version) — the same helper the checkpoint
|
|
10451
|
+
// identity above (`_ckptIdentity.rulesetVersion`) already calls for an
|
|
10452
|
+
// unrelated purpose. Computed ONCE here and shared via `provenanceCtx`,
|
|
10453
|
+
// same precedent as `deadlineAt` and `providerEnrichments` below: it's
|
|
10454
|
+
// cheap (an env read plus one small JSON file stat/read), but five
|
|
10455
|
+
// recomputations across the five annotateGitProvenance calls buys
|
|
10456
|
+
// nothing and risks a mid-scan pinned-file edit producing five
|
|
10457
|
+
// different answers in one run.
|
|
10458
|
+
rulesetVersion: (_effectiveRulesetVersion(scanRoot) || {}).version || null,
|
|
10459
|
+
since: process.env.AGENTIC_SECURITY_PROVENANCE_SINCE || null,
|
|
10460
|
+
timeoutMs: provenanceTimeoutMs,
|
|
10461
|
+
mode: process.env.AGENTIC_SECURITY_PROVENANCE_MODE || 'standard',
|
|
10462
|
+
// Fix-round item 2: ONE shared provider-enrichment counter for the
|
|
10463
|
+
// whole scan, same precedent as the single `deadlineAt` above. Object
|
|
10464
|
+
// identity (not the primitive value) is what makes the cap survive the
|
|
10465
|
+
// `{ ...provenanceCtx, findingType: ... }` spreads used by four of the
|
|
10466
|
+
// five annotateGitProvenance calls below — see coordinator.js's
|
|
10467
|
+
// `providerEnrichments` comment for why a bare number would not work.
|
|
10468
|
+
providerEnrichments: { remaining: MAX_PROVIDER_ENRICHMENTS_PER_SCAN },
|
|
10469
|
+
};
|
|
10470
|
+
await annotateGitProvenance(finalFindings, provenanceCtx);
|
|
10471
|
+
// Direct dependencies only: a transitive dep's vulnerable version was never
|
|
10472
|
+
// declared in this repository's manifests, so there is no commit here that
|
|
10473
|
+
// introduced it and `resolveDirectSCAOrigin` would have nothing to walk.
|
|
10474
|
+
//
|
|
10475
|
+
// Keyed on `isDirect`, the same field the transitive-dedup block above
|
|
10476
|
+
// uses. The plan's `!s.isTransitive` was a silent no-op: `isTransitive`
|
|
10477
|
+
// exists on dependency COMPONENTS but was never carried onto the
|
|
10478
|
+
// vulnerable_dep entries, so the negation was true for every entry and the
|
|
10479
|
+
// filter excluded nothing. `isDirect` is now propagated at materialization.
|
|
10480
|
+
const directDeps = (supplyChain || []).filter((s) => s && s.type === 'vulnerable_dep' && s.isDirect);
|
|
10481
|
+
// `resolveDirectSCAOrigin`/`scaStableId` key on `filePath`; the vulnerable_dep
|
|
10482
|
+
// entries built above carry the manifest path as `file` (report/index.js
|
|
10483
|
+
// already reads `sc.filePath || sc.file` for the same reason). Backfill the
|
|
10484
|
+
// alias rather than teaching the SCA resolver a second field name — without
|
|
10485
|
+
// it every direct dependency resolves to `not_available: no-manifest-path`.
|
|
10486
|
+
for (const s of directDeps) { if (!s.filePath && s.file) s.filePath = s.file; }
|
|
10487
|
+
await annotateGitProvenance(directDeps, { ...provenanceCtx, findingType: 'sca' });
|
|
10488
|
+
// M3 §3.2: transitive dependencies now get real origin resolution too,
|
|
10489
|
+
// narrowing what was previously an unconditional not_available backstop
|
|
10490
|
+
// to genuinely unresolvable cases (non-npm lockfiles, no candidate
|
|
10491
|
+
// history) — see transitive-sca.js's own scope note.
|
|
10492
|
+
const transitiveDeps = (supplyChain || []).filter((s) => s && s.type === 'vulnerable_dep' && !s.isDirect);
|
|
10493
|
+
for (const s of transitiveDeps) { if (!s.filePath && s.file) s.filePath = s.file; }
|
|
10494
|
+
await annotateGitProvenance(transitiveDeps, { ...provenanceCtx, findingType: 'sca-transitive' });
|
|
10495
|
+
// Task 11 (PRD P0 scope): `aSecrets`/`blameableLogic` already have real
|
|
10496
|
+
// stableIds backfilled above (outside this callback — see that comment).
|
|
10497
|
+
// findingType 'secret'/'logic' is not consumed by any branch in
|
|
10498
|
+
// coordinator.js's resolveOne (only 'sca'/'sca-transitive' select a
|
|
10499
|
+
// different resolution strategy) — both fall through to the plain SAST
|
|
10500
|
+
// path (file+line blame short-circuit, then origin-resolver.js). Passed
|
|
10501
|
+
// anyway for clarity and future debugging; harmless today.
|
|
10502
|
+
await annotateGitProvenance(aSecrets, { ...provenanceCtx, findingType: 'secret' });
|
|
10503
|
+
await annotateGitProvenance(blameableLogic, { ...provenanceCtx, findingType: 'logic' });
|
|
10504
|
+
// FR-PROV-013 — introduce/remediate/reintroduce events. Best-effort by
|
|
10505
|
+
// design: the lifecycle store is a convenience ledger, and a failed write
|
|
10506
|
+
// (read-only tree, lock contention) must never fail a scan, matching how
|
|
10507
|
+
// every other provenance component degrades.
|
|
10508
|
+
//
|
|
10509
|
+
// Gated on `disabled` as well as on the `provenance` parameter, because
|
|
10510
|
+
// those are two different opt-outs and only one of them was being honoured.
|
|
10511
|
+
// updateLifecycle WRITES to disk; an operator who set
|
|
10512
|
+
// AGENTIC_SECURITY_NO_PROVENANCE=1 has said the feature does nothing, and a
|
|
10513
|
+
// disabled feature that still litters `.agentic-security/provenance/` is
|
|
10514
|
+
// not disabled. (annotateGitProvenance handles `disabled` internally by
|
|
10515
|
+
// stamping not_available, which is why it is still called above — every
|
|
10516
|
+
// finding must keep a terminal status even with the feature off.)
|
|
10517
|
+
//
|
|
10518
|
+
// Gated on `scanRoot` too. Every path into updateLifecycle resolves the
|
|
10519
|
+
// store through statePath(scanRoot, …), and statePath falls back to the
|
|
10520
|
+
// PROCESS CWD when scanRoot is null — so a runFullScan called with no
|
|
10521
|
+
// scanRoot (the in-process test/bench harnesses, predicate replay before
|
|
10522
|
+
// its provenance:false brake, any embedder) wrote a lifecycle ledger into
|
|
10523
|
+
// whatever directory the process happened to be in, keyed on that
|
|
10524
|
+
// unrelated run's findings. That is how this repo's own
|
|
10525
|
+
// scanner/.agentic-security/provenance/lifecycle.json accumulated 374
|
|
10526
|
+
// stableIds and 3000+ spurious remediated/reintroduced events. No
|
|
10527
|
+
// scanRoot means no project to keep a ledger for.
|
|
10528
|
+
//
|
|
10529
|
+
// `completeScan` is the other half, and it is a correctness gate rather
|
|
10530
|
+
// than a hygiene one — see runFullScan's parameter comment and
|
|
10531
|
+
// lifecycle.js's applyScan.
|
|
10532
|
+
//
|
|
10533
|
+
// TRUTHY IS NOT ENOUGH — it has to be a real directory. `resolveProjectRoot`
|
|
10534
|
+
// honours a caller-supplied scanRoot only when it exists on disk AND is a
|
|
10535
|
+
// directory; for anything else (a typo'd path, a `/tmp/...` a test never
|
|
10536
|
+
// created, a file rather than a directory) it silently falls back to walking
|
|
10537
|
+
// UP FROM THE PROCESS CWD for a project marker. So `agentic-security scan
|
|
10538
|
+
// ./typo` run from inside a project writes THAT project's lifecycle ledger
|
|
10539
|
+
// from a scan that never looked at it — and since such a scan finds nothing
|
|
10540
|
+
// while still claiming `completeScan`, the remediation pass closes every
|
|
10541
|
+
// open finding the real project had. Same corruption as the null-scanRoot
|
|
10542
|
+
// case above, reached through a different door: found by tracing the writes
|
|
10543
|
+
// that kept reappearing in this repo's own scanner/.agentic-security AFTER
|
|
10544
|
+
// the null guard was added. All three remaining writers were
|
|
10545
|
+
// truthy-but-nonexistent scanRoots.
|
|
10546
|
+
//
|
|
10547
|
+
// Narrow on purpose. Every other state write in such a scan lands in the
|
|
10548
|
+
// same wrong place, which is a wider `state-dir.js` question; the lifecycle
|
|
10549
|
+
// ledger is singled out here because it is the only one that turns the
|
|
10550
|
+
// mistake into a destructive claim about findings it never saw.
|
|
10551
|
+
let scanRootIsRealDir = false;
|
|
10552
|
+
try { scanRootIsRealDir = !!scanRoot && fs.statSync(scanRoot).isDirectory(); } catch { scanRootIsRealDir = false; }
|
|
10553
|
+
if (scanRootIsRealDir && !provenanceCtx.disabled) {
|
|
10554
|
+
try {
|
|
10555
|
+
await updateLifecycle(scanRoot, finalFindings, {
|
|
10556
|
+
scanId: provenanceCtx.scanId, observedAt: provenanceCtx.observedAt,
|
|
10557
|
+
completeScan,
|
|
10558
|
+
});
|
|
10559
|
+
} catch (_) { /* best-effort */ }
|
|
10560
|
+
}
|
|
10561
|
+
});
|
|
10562
|
+
// Structural backstop for the terminal-status guarantee, for BOTH channels.
|
|
10563
|
+
// Deliberately OUTSIDE the _runAnnotator callback above: _runAnnotator
|
|
10564
|
+
// swallows whatever the callback throws, so anything that depends on the
|
|
10565
|
+
// callback reaching its last line is a convention, not a guarantee. If
|
|
10566
|
+
// annotateGitProvenance threw before its per-finding loop began
|
|
10567
|
+
// (getRepoState blowing up on a corrupt repo, say), everything below the
|
|
10568
|
+
// throw is skipped and every finding and supplyChain entry silently carries
|
|
10569
|
+
// no findingProvenance at all — exactly the absent-field state the status
|
|
10570
|
+
// enum exists to make unreachable. Enforced here so it holds structurally.
|
|
10571
|
+
for (const f of finalFindings) {
|
|
10572
|
+
if (f && typeof f === 'object' && !f.findingProvenance) {
|
|
10573
|
+
f.findingProvenance = emptyProvenance(PROVENANCE_STATUS.ERROR, {
|
|
10574
|
+
limitations: ['provenance annotator did not reach this finding'],
|
|
10575
|
+
});
|
|
10576
|
+
}
|
|
10577
|
+
}
|
|
10578
|
+
// The supply-chain half. report/index.js normalizes EVERY supplyChain entry
|
|
10579
|
+
// into an SCA finding — not just the direct vulnerable_dep ones the resolver
|
|
10580
|
+
// can speak to — and pipeline/finding-schema.js requires findingProvenance on
|
|
10581
|
+
// every channel. Three distinct populations reach this loop and they are not
|
|
10582
|
+
// the same statement, so they do not share a limitation string:
|
|
10583
|
+
//
|
|
10584
|
+
// - transitive vulnerable_deps: resolved by resolveTransitiveSCAOrigin
|
|
10585
|
+
// above (M3 §3.2) — this branch is now reached only if that annotation
|
|
10586
|
+
// pass itself failed to stamp the entry.
|
|
10587
|
+
// - unpinned_dep / no_lockfile and friends: these describe the ABSENCE of a
|
|
10588
|
+
// declaration, so "which commit introduced this version" is not a question
|
|
10589
|
+
// that has an answer to defer.
|
|
10590
|
+
// - anything the annotator failed to reach, as above.
|
|
10591
|
+
//
|
|
10592
|
+
// The first two are honest `not_available` — that is exactly what the status
|
|
10593
|
+
// is for. Only a genuine annotator failure is an `error`, which is why this
|
|
10594
|
+
// loop distinguishes them rather than stamping one status for all three.
|
|
10595
|
+
for (const sc of (supplyChain || [])) {
|
|
10596
|
+
if (!sc || typeof sc !== 'object' || sc.findingProvenance) continue;
|
|
10597
|
+
sc.findingProvenance = emptyProvenance(PROVENANCE_STATUS.NOT_AVAILABLE, {
|
|
10598
|
+
limitations: [sc.type === 'vulnerable_dep'
|
|
10599
|
+
? 'transitive dependency origin resolution failed for this entry (annotator error)'
|
|
10600
|
+
: `origin resolution does not apply to a ${sc.type || 'non-vulnerability'} supply-chain entry`],
|
|
10601
|
+
});
|
|
10602
|
+
}
|
|
10603
|
+
// The OTHER two channels report/index.js normalizes into findings —
|
|
10604
|
+
// `scan.secrets` and `scan.logicVulns`. The same argument that produced the
|
|
10605
|
+
// supplyChain loop above applies verbatim: pipeline/finding-schema.js makes
|
|
10606
|
+
// `findingProvenance` REQUIRED on every channel, and normalizeFindings emits
|
|
10607
|
+
// a finding for each of these, so leaving them unstamped ships a
|
|
10608
|
+
// schema-incomplete finding whose absent field is indistinguishable from
|
|
10609
|
+
// "escaped annotation" — the exact condition the status enum exists to
|
|
10610
|
+
// remove.
|
|
10611
|
+
//
|
|
10612
|
+
// Task 11 (PRD P0 scope): `aSecrets` and `blameableLogic` now go through
|
|
10613
|
+
// REAL resolution above (real stableIds backfilled, real
|
|
10614
|
+
// annotateGitProvenance calls made), so this loop no longer covers them
|
|
10615
|
+
// wholesale — it is now a defensive catch-all for any entry the real call
|
|
10616
|
+
// somehow didn't reach (same precedent as the supplyChain loop above), plus
|
|
10617
|
+
// `syntheticLogic`, which can NEVER get real resolution by design (see the
|
|
10618
|
+
// classification comment above `SYNTHETIC_LOGIC_PREFIXES`) and stays here
|
|
10619
|
+
// permanently and honestly, not as a deferral.
|
|
10620
|
+
for (const x of (syntheticLogic || [])) {
|
|
10621
|
+
if (!x || typeof x !== 'object' || x.findingProvenance) continue;
|
|
10622
|
+
x.findingProvenance = emptyProvenance(PROVENANCE_STATUS.NOT_AVAILABLE, {
|
|
10623
|
+
limitations: ['this finding describes dependency/config/policy state, not a single source line a commit introduced -- origin resolution does not apply'],
|
|
10624
|
+
});
|
|
10625
|
+
}
|
|
10626
|
+
for (const bucket of [aSecrets, blameableLogic]) {
|
|
10627
|
+
for (const x of (bucket || [])) {
|
|
10628
|
+
if (!x || typeof x !== 'object' || x.findingProvenance) continue;
|
|
10629
|
+
x.findingProvenance = emptyProvenance(PROVENANCE_STATUS.NOT_AVAILABLE, {
|
|
10630
|
+
limitations: ['origin resolution annotator did not reach this finding'],
|
|
10631
|
+
});
|
|
10632
|
+
}
|
|
10633
|
+
}
|
|
10634
|
+
}
|
|
10090
10635
|
// Addition #2 — attack-surface completeness inventory (entry points → dispositions).
|
|
10091
|
-
|
|
10636
|
+
// (_entrypointInventory is hoisted above the `skipAnnotators` guard — FR-PROV-029.)
|
|
10637
|
+
try { _entrypointInventory = buildEntrypointInventory(fc, { routes: aR, findings: finalFindings }); } catch { _entrypointInventory = {}; }
|
|
10092
10638
|
// R9 + R6 — relevance scoping. Runs HERE, after every finding has been
|
|
10093
10639
|
// appended (multi-sink chains, cross-language chains) and after the
|
|
10094
10640
|
// entry-point inventory exists, so no finding escapes annotation and the
|
|
@@ -10105,29 +10651,31 @@ function _deterministicFileTimings(timings) {
|
|
|
10105
10651
|
});
|
|
10106
10652
|
// Addition #3 — root-cause sweep: from confirmed findings, find sibling instances
|
|
10107
10653
|
// detectors missed, with total-count accounting. Confirmed-only (cheap by default).
|
|
10108
|
-
|
|
10654
|
+
// (_rootCauseSweep/_proofCoverage/_coverageLedger/_scanHealth are hoisted
|
|
10655
|
+
// above the `skipAnnotators` guard — FR-PROV-029.)
|
|
10656
|
+
try { _rootCauseSweep = sweepRootCauses(finalFindings, fc); } catch { _rootCauseSweep = null; }
|
|
10109
10657
|
// PRD F7.2: publish what CANNOT be proven alongside what can. A proof RATE
|
|
10110
10658
|
// computed over the provable subset makes a narrow subset look like strength;
|
|
10111
10659
|
// the three-bucket split (provable / declined-on-purpose / not-yet-classified)
|
|
10112
10660
|
// is the honest shape. Measured on the CVE corpus: 19% / 13% / 68%.
|
|
10113
|
-
let _proofCoverage = null;
|
|
10114
10661
|
try { _proofCoverage = proofCoverage([...finalFindings, ...aLogic]); } catch { _proofCoverage = null; }
|
|
10115
10662
|
// FR-203: per-file/per-analyzer coverage ledger, computed from exactly
|
|
10116
10663
|
// the signals FR-201 (_detectorErrors) and FR-202 (the _timeout:true
|
|
10117
10664
|
// marker finding) already produce -- files actually scanned come from
|
|
10118
10665
|
// fc's own keys (skipped-for-size/density files were never added to it).
|
|
10119
10666
|
const _timedOutFiles = finalFindings.filter(f => f && f._timeout === true).map(f => f.file);
|
|
10120
|
-
|
|
10667
|
+
_coverageLedger = computeCoverageLedger({ files: Object.keys(fc), detectorErrors: _detectorErrors, timedOutFiles: _timedOutFiles });
|
|
10121
10668
|
// FR-206 (assurance-hardening PRD, Milestone 0): additive scan-health
|
|
10122
10669
|
// summary, computed from signals the engine already collects.
|
|
10123
10670
|
// `analyzers` was `null` (see pipeline/scan-health.js's prior comment)
|
|
10124
10671
|
// until FR-203's coverage ledger existed to compute it for real.
|
|
10125
|
-
|
|
10672
|
+
_scanHealth = computeScanHealth({
|
|
10126
10673
|
scanMeta: _scanMeta,
|
|
10127
10674
|
annotatorErrors: _annotatorErrors,
|
|
10128
10675
|
engineErrors: { cppDataflowParseErrors: _cppDataflowParseErrors.value },
|
|
10129
10676
|
deepStatus: _deepStatus,
|
|
10130
10677
|
analyzerCoverage: summarizeCoverageForScanHealth(_coverageLedger),
|
|
10678
|
+
lineageStatus: _lineageStatus,
|
|
10131
10679
|
});
|
|
10132
10680
|
// FR-207: stale vulnerability feeds, calibration data, and compliance
|
|
10133
10681
|
// evidence are real assurance gaps, not just findings the feed omits --
|
|
@@ -10142,7 +10690,8 @@ function _deterministicFileTimings(timings) {
|
|
|
10142
10690
|
calibration: calibrationFreshness(),
|
|
10143
10691
|
compliance: _complianceReport ? { stale: _complianceReport.summary?.stale || 0 } : null,
|
|
10144
10692
|
});
|
|
10145
|
-
|
|
10693
|
+
} // end if (!skipAnnotators) — FR-PROV-029
|
|
10694
|
+
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,lineageGraph:_lineageGraph,lineageStatus:_lineageStatus};}
|
|
10146
10695
|
|
|
10147
10696
|
// Post-aggregation classification: every source becomes "unsafe"|"safe"; every sink becomes "confirmed"|"safe".
|
|
10148
10697
|
// Orphans (no finding linkage) are bucketed by file-local heuristic so the UI shows binary states only.
|
|
@@ -10609,6 +11158,7 @@ export {
|
|
|
10609
11158
|
classifyOrphans, classifyField, classifyEndpoint, shouldScan,
|
|
10610
11159
|
_isFalsePositiveCredential, _detectSafeSinkShape,
|
|
10611
11160
|
_loadCustomRules, _isCustomSuppressed, _isPathIgnored,
|
|
11161
|
+
_snapshotSuppressionLog, _restoreSuppressionLog,
|
|
10612
11162
|
scanIaC, IAC_PATTERNS, _isIaCFile, isCloudFormationTemplate,
|
|
10613
11163
|
payloadsForFinding, buildProofObligation,
|
|
10614
11164
|
DATA_CLASSES, SOURCE_PATTERNS, SINK_PATTERNS, SANITIZER_PATTERNS,
|