@clear-capabilities/agentic-security-scanner 0.143.0 → 0.145.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +555 -0
- package/bin/agentic-security.js +770 -49
- package/dist/1.index.js +223 -0
- package/dist/113.index.js +117 -18
- package/dist/144.index.js +163 -0
- package/dist/178.index.js +24 -6
- package/dist/238.index.js +3 -2
- package/dist/265.index.js +191 -0
- package/dist/271.index.js +165 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +187 -52
- package/dist/444.index.js +11 -2
- package/dist/449.index.js +76 -12
- package/dist/526.index.js +117 -18
- package/dist/552.index.js +97 -0
- package/dist/637.index.js +27 -5
- package/dist/730.index.js +311 -0
- package/dist/736.index.js +301 -0
- package/dist/824.index.js +7 -0
- package/dist/905.index.js +88 -22
- package/dist/920.index.js +491 -0
- package/dist/970.index.js +173 -0
- package/dist/agentic-security.mjs +13 -13
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/calibration-seed.json +2 -0
- package/package.json +23 -9
- package/src/compare.js +6 -1
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/index.js +18 -0
- package/src/dataflow/privacy-catalog.js +290 -0
- package/src/dataflow/privacy-deep-walker.js +515 -0
- package/src/dataflow/privacy-governance.js +126 -0
- package/src/dataflow/privacy-inventory.js +154 -0
- package/src/dataflow/privacy-sink-policy.js +125 -0
- package/src/dataflow/privacy-taint.js +115 -54
- package/src/dataflow/privacy-taxonomy.js +233 -0
- package/src/discovery/disprove.js +7 -3
- package/src/discovery/hunter.js +9 -5
- package/src/discovery/index.js +2 -2
- package/src/discovery/llm-invoke.js +69 -13
- package/src/egress/audit.js +147 -0
- package/src/egress/policy.js +313 -0
- package/src/egress/redact.js +180 -0
- package/src/engine.js +1048 -302
- package/src/fix/apply-fix-service.js +404 -0
- package/src/fix/approver-registry.js +157 -0
- package/src/history-scan.js +22 -5
- package/src/ir/CLAUDE.md +1 -1
- package/src/llm-validator/index.js +86 -9
- package/src/llm-validator/model-status.js +66 -0
- package/src/lsp/server.js +49 -2
- package/src/mcp/tools.js +177 -50
- package/src/pipeline/analyzer-supervisor.js +93 -0
- package/src/pipeline/analyzer-worker.js +26 -0
- package/src/pipeline/annotator-runner.js +33 -0
- package/src/pipeline/assurance-mode.js +154 -0
- package/src/pipeline/cascade-worker-pool.js +172 -0
- package/src/pipeline/cascade-worker.js +43 -0
- package/src/pipeline/coverage-ledger.js +0 -0
- package/src/pipeline/detector-runner.js +51 -0
- package/src/pipeline/enrichment-completion.js +58 -0
- package/src/pipeline/evidence-provenance.js +91 -0
- package/src/pipeline/finding-schema.js +108 -0
- package/src/pipeline/legacy-compat.js +101 -0
- package/src/pipeline/producer-collector.js +48 -0
- package/src/pipeline/producer-registry.js +112 -0
- package/src/pipeline/scan-health.js +144 -0
- package/src/posture/CLAUDE.md +123 -0
- package/src/posture/accuracy-scorecard.js +156 -1
- package/src/posture/adversary-agent.js +15 -3
- package/src/posture/artifact-registry.js +241 -0
- package/src/posture/auditor-walkthrough.js +186 -21
- package/src/posture/calibration-feedback.js +201 -0
- package/src/posture/calibration-seed.json +2 -0
- package/src/posture/calibration.js +25 -0
- package/src/posture/compliance-evidence-signing.js +131 -0
- package/src/posture/compliance-policy.js +324 -17
- package/src/posture/cross-repo-memory.js +7 -2
- package/src/posture/custom-rules.js +36 -0
- package/src/posture/deterministic.js +8 -1
- package/src/posture/encryption-provider.js +205 -0
- package/src/posture/evidence-grade-wording.js +71 -0
- package/src/posture/fix-history.js +137 -20
- package/src/posture/fix-honesty-gate.js +47 -6
- package/src/posture/fix-verify.js +65 -8
- package/src/posture/fleet.js +0 -0
- package/src/posture/flow-narration.js +7 -2
- package/src/posture/git-history.js +13 -5
- package/src/posture/legal-hold.js +140 -0
- package/src/posture/llm-redteam.js +10 -1
- package/src/posture/material-change.js +111 -2
- package/src/posture/mttr.js +75 -12
- package/src/posture/policy-bundle.js +274 -0
- package/src/posture/pre-incident-archaeology.js +39 -7
- package/src/posture/privacy-framework.js +47 -6
- package/src/posture/production-feedback.js +179 -0
- package/src/posture/provenance/ai-authorship.js +68 -0
- package/src/posture/provenance/branch-entry.js +80 -0
- package/src/posture/provenance/cache.js +143 -0
- package/src/posture/provenance/confidence.js +36 -0
- package/src/posture/provenance/coordinator.js +786 -0
- package/src/posture/provenance/dag-walk.js +249 -0
- package/src/posture/provenance/evidence-attribution.js +59 -0
- package/src/posture/provenance/git-evidence.js +310 -0
- package/src/posture/provenance/lifecycle.js +208 -0
- package/src/posture/provenance/missing-control-resolver.js +137 -0
- package/src/posture/provenance/origin-resolver.js +342 -0
- package/src/posture/provenance/predicate-replay.js +133 -0
- package/src/posture/provenance/providers/config.js +39 -0
- package/src/posture/provenance/providers/github.js +62 -0
- package/src/posture/provenance/providers/gitlab.js +58 -0
- package/src/posture/provenance/repo-lineage.js +74 -0
- package/src/posture/provenance/sca-origin.js +139 -0
- package/src/posture/provenance/schema.js +255 -0
- package/src/posture/provenance/transitive-sca.js +147 -0
- package/src/posture/provenance/validate.js +30 -0
- package/src/posture/provenance-evidence-bundle.js +144 -0
- package/src/posture/retention-policy.js +132 -0
- package/src/posture/risk-dollars.js +216 -26
- package/src/posture/sbom-diff.js +15 -2
- package/src/posture/scan-checkpoint.js +176 -31
- package/src/posture/secret-history.js +10 -2
- package/src/posture/state-dir.js +64 -5
- package/src/posture/state-lifecycle-report.js +77 -0
- package/src/posture/suppressions.js +59 -3
- package/src/posture/vuln-archaeology.js +8 -2
- package/src/pr-delta.js +25 -4
- package/src/privacy/ir-adapter.js +380 -0
- package/src/report/index.js +248 -5
- package/src/report/oscal.js +7 -2
- package/src/runScan.js +34 -5
- package/src/sast/cpp.js +3 -14
- package/src/sast/rate-limit.js +33 -3
- package/src/sca/llm-function-extract.js +6 -0
- package/src/util/git-hardening.js +128 -0
package/src/mcp/tools.js
CHANGED
|
@@ -16,12 +16,20 @@ import * as fs from 'node:fs';
|
|
|
16
16
|
import * as fsp from 'node:fs/promises';
|
|
17
17
|
import * as path from 'node:path';
|
|
18
18
|
import * as crypto from 'node:crypto';
|
|
19
|
-
import { applyFix as applyFixHistory, fixAcceptanceRate } from '../posture/fix-history.js';
|
|
19
|
+
import { applyFix as applyFixHistory, fixAcceptanceRate, revertEntryById as revertFixEntry } from '../posture/fix-history.js';
|
|
20
|
+
import { applyVerifiedFix } from '../fix/apply-fix-service.js';
|
|
21
|
+
import { classifyFixMaterialRisk } from '../posture/material-change.js';
|
|
22
|
+
import { loadApproverRegistry, verifyApprover, requiredRolesFor, checkSeparationOfDuties } from '../fix/approver-registry.js';
|
|
20
23
|
import { synthesizeDeterministicPatch } from '../posture/deterministic-fix.js';
|
|
21
24
|
import { verifyLastScan } from '../posture/integrity.js';
|
|
25
|
+
import { withStateWritesDisabled } from '../posture/state-dir.js';
|
|
22
26
|
import { analyzeTranscript, formatCacheReport, renderCacheStatusLine } from '../posture/cache-economics.js';
|
|
23
27
|
import { redactString, redactFinding } from './redact.js';
|
|
24
|
-
import { _remediationOf } from '../report/index.js';
|
|
28
|
+
import { _remediationOf, normalizeFindings } from '../report/index.js';
|
|
29
|
+
// Git-origin provenance (Finding Provenance M0/M1). Distinct from
|
|
30
|
+
// `finding.provenance` (AI-authorship) and from an SCA entry's `provenance`
|
|
31
|
+
// (Sigstore/SLSA attestation) — see report/index.js's import comment.
|
|
32
|
+
import { redactFindingProvenance } from '../posture/provenance/schema.js';
|
|
25
33
|
|
|
26
34
|
// Lazy-loaded: these transitively pull in npm packages (@babel/core and
|
|
27
35
|
// friends) that aren't available in the plugin-cache install path
|
|
@@ -387,26 +395,44 @@ export const scan_diff = {
|
|
|
387
395
|
// / MAX_TOTAL_SCAN_BYTES, so this does not turn scan_diff into a
|
|
388
396
|
// full-project deep scan).
|
|
389
397
|
const runScan = await getRunScan();
|
|
390
|
-
|
|
398
|
+
// FR-704 (assurance-hardening PRD): this tool's own description promises
|
|
399
|
+
// "runs scan in memory" — without this, runFullScan's own state writers
|
|
400
|
+
// (dpia.md, ropa.md, privacy-framework.json, threat-model.json, and
|
|
401
|
+
// others) fire unconditionally on every call, silently mutating the
|
|
402
|
+
// user's real project on every pre-write self-correction scan. Confirmed
|
|
403
|
+
// by direct execution before this fix (11 state artifacts written by a
|
|
404
|
+
// single scan_diff-shaped call).
|
|
405
|
+
const result = await withStateWritesDisabled(() =>
|
|
406
|
+
runScan(sessionRoot, { network: false, fileContents, deep: true, deepInCi: true }));
|
|
391
407
|
const wantSet = new Set(Object.keys(fileContents));
|
|
392
408
|
const sevRank = { info: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
393
409
|
const min = sevRank[severity] ?? 0;
|
|
394
|
-
// Stage 6 correctness audit: this only
|
|
395
|
-
// (the SAST channel) — scan.secrets and
|
|
396
|
-
// arrays on the raw runScan() result
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
// the
|
|
404
|
-
|
|
410
|
+
// Stage 6 correctness audit (historical): this used to only read
|
|
411
|
+
// result.scan.findings (the SAST channel) — scan.secrets and
|
|
412
|
+
// scan.logicVulns are separate arrays on the raw runScan() result, and a
|
|
413
|
+
// hand-rolled 3-channel concat here was a second, divergent copy of the
|
|
414
|
+
// merge report/index.js's normalizeFindings() already does (four
|
|
415
|
+
// channels, plus per-channel defaulting and remediation-string
|
|
416
|
+
// resolution the old concat re-implemented separately and could drift
|
|
417
|
+
// from). Assurance-hardening PRD FR-105 ("JSON, SARIF, HTML, CSV, JUnit,
|
|
418
|
+
// and MCP outputs derive from the same validated object"): route through
|
|
419
|
+
// the same canonical merge every other output format uses.
|
|
420
|
+
//
|
|
421
|
+
// This closes the field-mapping/dedup divergence, but does NOT make
|
|
422
|
+
// scan_diff surface SCA/supply-chain findings end to end: this handler
|
|
423
|
+
// never builds a `depFileContents` map (everything a caller passes in
|
|
424
|
+
// `files`, manifests included, lands in `fileContents`), and manifest-
|
|
425
|
+
// based supply-chain detection in engine.js reads only `depFileContents`
|
|
426
|
+
// — so `result.scan.supplyChain` is always empty for this tool today
|
|
427
|
+
// regardless of this fix. That is a separate, real limitation (scan_diff
|
|
428
|
+
// was designed for pre-write code self-correction, not manifest
|
|
429
|
+
// scanning), left as-is rather than silently claimed fixed here.
|
|
430
|
+
const findings = normalizeFindings(result.scan)
|
|
405
431
|
.filter(f => wantSet.has(String(f.file || '').replace(/\\/g, '/')) && (sevRank[f.severity] ?? 0) >= min)
|
|
406
432
|
.map(f => redactFinding({
|
|
407
433
|
id: f.id, severity: f.severity, file: f.file, line: f.line,
|
|
408
|
-
title: f.
|
|
409
|
-
description: f.description, remediation:
|
|
434
|
+
title: f.vuln, cwe: f.cwe,
|
|
435
|
+
description: f.description, remediation: f.remediation,
|
|
410
436
|
}));
|
|
411
437
|
// Harness-anatomy #1: offload when the result exceeds OFFLOAD_THRESHOLD.
|
|
412
438
|
// The agent gets a head+tail preview plus a path it can page through;
|
|
@@ -549,6 +575,21 @@ export const explain_finding = {
|
|
|
549
575
|
epssScore: typeof f.epssScore === 'number' ? f.epssScore : null,
|
|
550
576
|
epssPercentile: typeof f.epssPercentile === 'number' ? f.epssPercentile : null,
|
|
551
577
|
exploitedNow: !!f.exploitedNow,
|
|
578
|
+
// Which commit introduced this finding. `includeEmail` stays at its
|
|
579
|
+
// DEFAULT (false) unconditionally — unlike the JSON report there is no
|
|
580
|
+
// operator-set env escape for it here, because the consumer is an
|
|
581
|
+
// agent that has no business receiving a committer's email address.
|
|
582
|
+
// `pseudonymize`, by contrast, IS read back from the same env var
|
|
583
|
+
// report/index.js's `_normalizedProvenance` reads
|
|
584
|
+
// (AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS=1 / --pseudonymize-authors) —
|
|
585
|
+
// fix-round item 4: an operator who set that policy was still getting
|
|
586
|
+
// raw committer names (and, via providerEnrichment, raw reviewer
|
|
587
|
+
// logins/CODEOWNERS lines) through this MCP surface because this call
|
|
588
|
+
// passed no options object at all, silently defeating their policy at
|
|
589
|
+
// this one output boundary while report/index.js honoured it.
|
|
590
|
+
findingProvenance: f.findingProvenance ? redactFindingProvenance(f.findingProvenance, {
|
|
591
|
+
pseudonymize: process.env.AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS === '1',
|
|
592
|
+
}) : null,
|
|
552
593
|
};
|
|
553
594
|
},
|
|
554
595
|
};
|
|
@@ -556,7 +597,7 @@ export const explain_finding = {
|
|
|
556
597
|
// ─── apply_fix ───────────────────────────────────────────────────────────────
|
|
557
598
|
export const apply_fix = {
|
|
558
599
|
name: 'apply_fix',
|
|
559
|
-
description: 'Apply a fix for a finding. Two modes: (1) the stored fix.replacement, or (2) a caller-supplied `patch` (a files map) which is RE-VERIFIED inline (rescan-clean + no new ≥medium + lint) before any write — this unblocks findings that ship only a template or description. Refuses if last-scan.json fails its HMAC check, if the finding is shadow-marked, or if a path escapes the session root via lexical traversal OR a symlink. Requires confirm:true. Supports dry_run:true to preview without writing.',
|
|
600
|
+
description: 'Apply a fix for a finding. Two modes: (1) the stored fix.replacement, or (2) a caller-supplied `patch` (a files map) which is RE-VERIFIED inline (rescan-clean + no new ≥medium + lint) before any write — this unblocks findings that ship only a template or description. Refuses if last-scan.json fails its HMAC check, if the finding is shadow-marked, or if a path escapes the session root via lexical traversal OR a symlink. Requires confirm:true. Supports dry_run:true to preview without writing. On success, `verified:true` means verification passed but `verifiedFull:true` is the honest signal that every required leg (lint when configured, tests when a runner exists) genuinely ran — a false `verifiedFull` with `verified:true` means the pass is real but degraded (see `verify.degradedLegs`), not a full verification.',
|
|
560
601
|
inputSchema: {
|
|
561
602
|
type: 'object',
|
|
562
603
|
additionalProperties: false,
|
|
@@ -597,6 +638,27 @@ export const apply_fix = {
|
|
|
597
638
|
partialSanitization: { type: 'boolean' },
|
|
598
639
|
},
|
|
599
640
|
},
|
|
641
|
+
// FR-307/FR-1002: this schema had `additionalProperties: false`
|
|
642
|
+
// and never declared `approval` — the property apply-fix-
|
|
643
|
+
// service.js's high-impact-change gate has required since FR-307
|
|
644
|
+
// was built. A real MCP caller supplying fixMeta.approval was
|
|
645
|
+
// rejected by validate.js at the schema layer before the handler
|
|
646
|
+
// ever ran, silently making the approval gate (and FR-1002's
|
|
647
|
+
// identity check layered on it) unreachable from this tool's
|
|
648
|
+
// only real production entry point. See D-0024.
|
|
649
|
+
approval: {
|
|
650
|
+
type: 'object',
|
|
651
|
+
additionalProperties: false,
|
|
652
|
+
properties: {
|
|
653
|
+
approvedBy: { type: 'string', minLength: 1, maxLength: 200 },
|
|
654
|
+
reason: { type: 'string', minLength: 1, maxLength: 1000 },
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
// FR-1003: separation-of-duties. Self-reported the same way
|
|
658
|
+
// approvedBy is — this tool has no way to determine who actually
|
|
659
|
+
// wrote a patch, so `author` is a claim, checked against a
|
|
660
|
+
// configurable policy the same way `approval` is.
|
|
661
|
+
author: { type: 'string', minLength: 1, maxLength: 200 },
|
|
600
662
|
},
|
|
601
663
|
},
|
|
602
664
|
},
|
|
@@ -671,20 +733,79 @@ export const apply_fix = {
|
|
|
671
733
|
verify: { rescan: verdict.rescan, lint: { runner: verdict.lint?.runner, ok: verdict.lint?.ok }, honesty: verdict.honesty || null },
|
|
672
734
|
};
|
|
673
735
|
}
|
|
736
|
+
// FR-307/FR-1002/D-0024: this caller-supplied-patch branch writes via
|
|
737
|
+
// applyFixHistory() directly and never called applyVerifiedFix() — so
|
|
738
|
+
// the high-impact-change approval gate (auth/authZ/crypto/PII/schema/
|
|
739
|
+
// infra-privilege/public-API) built for the OTHER apply_fix branch
|
|
740
|
+
// (stored fix.replacement) never ran here at all, for any input. Since
|
|
741
|
+
// this is the branch the tool's own description calls the one that
|
|
742
|
+
// covers "~100% of findings that ship only a template," that gap was
|
|
743
|
+
// the larger of the two found this cycle. Same before/after content
|
|
744
|
+
// shape `apply-fix-service.js` already uses — read-first-in-try/catch
|
|
745
|
+
// (D-0012), never existsSync-then-readFileSync.
|
|
746
|
+
const filesForMaterialClassification = {};
|
|
747
|
+
for (const [rel, v] of Object.entries(confinedAbs)) {
|
|
748
|
+
let before = '';
|
|
749
|
+
try { before = await fsp.readFile(v.abs, 'utf8'); } catch { /* new file — before stays '' */ }
|
|
750
|
+
filesForMaterialClassification[rel] = { before, after: v.content };
|
|
751
|
+
}
|
|
752
|
+
const materialClassification = classifyFixMaterialRisk(filesForMaterialClassification);
|
|
674
753
|
if (dry_run) {
|
|
675
|
-
return { _meta: META, applied: false, dryRun: true, verified: true, files: Object.keys(confinedAbs), summary: verdict.summary };
|
|
754
|
+
return { _meta: META, applied: false, dryRun: true, verified: true, files: Object.keys(confinedAbs), summary: verdict.summary, materialClassification };
|
|
755
|
+
}
|
|
756
|
+
if (materialClassification.highImpactCategories.length) {
|
|
757
|
+
const approval = fixMeta && typeof fixMeta === 'object' ? fixMeta.approval : null;
|
|
758
|
+
const hasApprovalEvidence = !!(approval && typeof approval === 'object' &&
|
|
759
|
+
typeof approval.approvedBy === 'string' && approval.approvedBy.trim().length > 0 &&
|
|
760
|
+
typeof approval.reason === 'string' && approval.reason.trim().length > 0);
|
|
761
|
+
if (!hasApprovalEvidence) {
|
|
762
|
+
return {
|
|
763
|
+
_meta: META, applied: false,
|
|
764
|
+
reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) requires approval evidence — pass fixMeta.approval: {approvedBy, reason} — before it can be applied`,
|
|
765
|
+
materialClassification,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
const approverRegistry = loadApproverRegistry(ctx.sessionRoot);
|
|
769
|
+
const requiredRoles = requiredRolesFor(approverRegistry, materialClassification.highImpactCategories);
|
|
770
|
+
const identityCheck = verifyApprover(approverRegistry, approval.approvedBy, requiredRoles);
|
|
771
|
+
if (!identityCheck.verified) {
|
|
772
|
+
return {
|
|
773
|
+
_meta: META, applied: false,
|
|
774
|
+
reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) approval rejected: ${identityCheck.reason}`,
|
|
775
|
+
materialClassification,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
// FR-1003: separation-of-duties, same no-op-unless-configured gate
|
|
779
|
+
// as apply-fix-service.js's own copy — see approver-registry.js.
|
|
780
|
+
const sodCheck = checkSeparationOfDuties(approverRegistry, fixMeta?.author, approval.approvedBy);
|
|
781
|
+
if (!sodCheck.ok) {
|
|
782
|
+
return {
|
|
783
|
+
_meta: META, applied: false,
|
|
784
|
+
reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) approval rejected: ${sodCheck.reason}`,
|
|
785
|
+
materialClassification,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
676
788
|
}
|
|
677
789
|
const written = [];
|
|
678
790
|
try {
|
|
679
791
|
for (const [rel, v] of Object.entries(confinedAbs)) {
|
|
680
|
-
const
|
|
792
|
+
const fileExisted = fs.existsSync(v.abs);
|
|
793
|
+
const originalContent = fileExisted ? await fsp.readFile(v.abs, 'utf8') : '';
|
|
681
794
|
const entry = await applyFixHistory({
|
|
682
|
-
scanRoot: ctx.sessionRoot, file: rel, originalContent, newContent: v.content,
|
|
795
|
+
scanRoot: ctx.sessionRoot, file: rel, originalContent, newContent: v.content, fileExisted,
|
|
683
796
|
findingId: f.id, stableId: f.stableId, ruleId: f.ruleId || f.cwe || f.family || null, vuln: f.vuln || f.title || null,
|
|
797
|
+
findingProvenance: f.findingProvenance || null,
|
|
684
798
|
});
|
|
685
799
|
written.push({ file: rel, historyId: entry.id, backupPath: entry.backupPath });
|
|
686
800
|
}
|
|
687
801
|
} catch (e) {
|
|
802
|
+
// FR-306: roll back every file THIS batch already wrote before the
|
|
803
|
+
// failure — applyFixHistory already restored the one file that just
|
|
804
|
+
// failed; this covers the rest, so a multi-file patch never leaves
|
|
805
|
+
// some files patched and others not.
|
|
806
|
+
for (const w of written) {
|
|
807
|
+
try { await revertFixEntry(ctx.sessionRoot, w.historyId); } catch { /* best-effort; original error still propagates below */ }
|
|
808
|
+
}
|
|
688
809
|
if (e && e.name === 'FixAttemptBudgetExceededError') {
|
|
689
810
|
return { _meta: META, applied: false, reason: `budget-exceeded: ${e.message}`, budgetExceeded: true, attempts: e.attempts, maxAttempts: e.max, key: e.key };
|
|
690
811
|
}
|
|
@@ -692,7 +813,7 @@ export const apply_fix = {
|
|
|
692
813
|
}
|
|
693
814
|
let acceptance = null;
|
|
694
815
|
try { acceptance = fixAcceptanceRate(ctx.sessionRoot); } catch { /* best-effort */ }
|
|
695
|
-
return { _meta: META, applied: true, verified: true, patched: written, integrity: status, verify: { summary: verdict.summary }, acceptance };
|
|
816
|
+
return { _meta: META, applied: true, verified: true, patched: written, integrity: status, verify: { summary: verdict.summary }, acceptance, materialClassification };
|
|
696
817
|
}
|
|
697
818
|
|
|
698
819
|
if (typeof f.fix?.replacement !== 'string') {
|
|
@@ -729,41 +850,47 @@ export const apply_fix = {
|
|
|
729
850
|
};
|
|
730
851
|
}
|
|
731
852
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
maxAttempts: e.max,
|
|
757
|
-
key: e.key,
|
|
758
|
-
};
|
|
853
|
+
// FR-301/A-08 (assurance-hardening PRD): this branch used to write
|
|
854
|
+
// f.fix.replacement straight to disk with NO fresh verification — no
|
|
855
|
+
// rescan, no lint, nothing confirming the stored replacement actually
|
|
856
|
+
// closes the finding it claims to fix. The caller-patch branch above
|
|
857
|
+
// already required this; there is no reason a STORED fix should be
|
|
858
|
+
// trusted more than a caller-supplied one just because it shipped with
|
|
859
|
+
// the finding. Routed through the same applyVerifiedFix() service the
|
|
860
|
+
// CLI's `fix --apply` now also uses (src/fix/apply-fix-service.js) —
|
|
861
|
+
// confinement/reserved-path are re-checked there too (harmless
|
|
862
|
+
// redundancy with the dry_run preview above, kept for that preview's
|
|
863
|
+
// size-diff shape) but the load-bearing addition is the verification
|
|
864
|
+
// gate before the write.
|
|
865
|
+
if (!f.stableId) {
|
|
866
|
+
return { _meta: META, applied: false, reason: 'finding has no stableId — cannot verify a stored fix against it' };
|
|
867
|
+
}
|
|
868
|
+
const result = await applyVerifiedFix({
|
|
869
|
+
scanRoot: ctx.sessionRoot,
|
|
870
|
+
finding: f,
|
|
871
|
+
files: { [f.file]: f.fix.replacement },
|
|
872
|
+
fixMeta,
|
|
873
|
+
});
|
|
874
|
+
if (!result.ok) {
|
|
875
|
+
if (result.budgetExceeded) {
|
|
876
|
+
return { _meta: META, applied: false, reason: result.reason, budgetExceeded: true, attempts: result.attempts, maxAttempts: result.maxAttempts, key: result.key };
|
|
759
877
|
}
|
|
760
|
-
|
|
878
|
+
return { _meta: META, applied: false, reason: result.reason, verify: result.verify || null };
|
|
761
879
|
}
|
|
762
880
|
// R25 (PRD §5): surface the running auto-fix acceptance rate after each
|
|
763
881
|
// applied fix, so the closed loop reports its own success metric.
|
|
764
882
|
let acceptance = null;
|
|
765
883
|
try { acceptance = fixAcceptanceRate(ctx.sessionRoot); } catch { /* metric is best-effort */ }
|
|
766
|
-
|
|
884
|
+
const entry = result.written[0];
|
|
885
|
+
return {
|
|
886
|
+
// FR-305: verifiedFull distinguishes "every required leg (lint, tests)
|
|
887
|
+
// genuinely ran and passed" from "passed, but a required leg was
|
|
888
|
+
// skipped or unavailable" — verified:true alone conflates them.
|
|
889
|
+
_meta: META, applied: true, verified: true, verifiedFull: result.verifiedFull,
|
|
890
|
+
historyId: entry.historyId, file: entry.file, backupPath: entry.backupPath,
|
|
891
|
+
integrity: status, attemptOrdinal: entry.attemptOrdinal, acceptance,
|
|
892
|
+
verify: result.verify,
|
|
893
|
+
};
|
|
767
894
|
},
|
|
768
895
|
};
|
|
769
896
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Real, preemptive deadline enforcement via worker_threads (assurance-
|
|
2
|
+
// hardening PRD FR-202, D-0046 phase 1).
|
|
3
|
+
//
|
|
4
|
+
// FR-201's runDetector (detector-runner.js) catches a THROWN exception —
|
|
5
|
+
// it does nothing for a detector that hangs (infinite loop, catastrophic
|
|
6
|
+
// regex backtracking), because JavaScript is single-threaded: nothing
|
|
7
|
+
// running in the same event loop as a synchronous hang can preempt it. The
|
|
8
|
+
// existing _perFileTimeoutMs (engine.js) and AGENTIC_SECURITY_DEEP_TIMEOUT_MS
|
|
9
|
+
// (dataflow/engine.js) are both COOPERATIVE — they measure elapsed time
|
|
10
|
+
// after work already finished, or poll the clock between iterations of an
|
|
11
|
+
// already-bounded loop. Neither can stop a genuinely hung synchronous call.
|
|
12
|
+
//
|
|
13
|
+
// runWithDeadline is the one piece that can: it races a worker_thread
|
|
14
|
+
// against a real timer and calls worker.terminate() — an OS-level
|
|
15
|
+
// preemptive kill — if the timer wins. This is phase 1 of D-0046's 4-phase
|
|
16
|
+
// plan: a proven, isolated primitive, not yet wired into engine.js's real
|
|
17
|
+
// scan loop (phase 3). See D-0046 for the full plan and why per-file
|
|
18
|
+
// (not per-detector) granularity is the eventual wiring target.
|
|
19
|
+
|
|
20
|
+
import { Worker } from 'node:worker_threads';
|
|
21
|
+
import * as path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const DEFAULT_WORKER_SCRIPT = path.join(HERE, 'analyzer-worker.js');
|
|
26
|
+
|
|
27
|
+
// PRD FR-202: "the configured deadline plus a documented grace period."
|
|
28
|
+
// The grace period exists so a worker that finishes essentially on time
|
|
29
|
+
// (message already in flight) isn't killed by scheduler jitter.
|
|
30
|
+
export const DEFAULT_GRACE_MS = 500;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Run `exportName` from `modulePath`, called with `args`, inside a worker
|
|
34
|
+
* thread, with a real preemptive deadline.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} spec
|
|
37
|
+
* @param {string} spec.modulePath - absolute path to the module to import inside the worker.
|
|
38
|
+
* @param {string} spec.exportName - the named export to call.
|
|
39
|
+
* @param {Array} [spec.args] - plain, structured-clone-safe arguments.
|
|
40
|
+
* @param {object} [opts]
|
|
41
|
+
* @param {number} opts.timeoutMs - required. The configured deadline.
|
|
42
|
+
* @param {number} [opts.graceMs] - documented grace period beyond timeoutMs before terminating. Default DEFAULT_GRACE_MS.
|
|
43
|
+
* @param {string} [opts.workerScript] - override for testing.
|
|
44
|
+
* @returns {Promise<{ok:true, result:*} | {ok:false, timedOut?:true, error?:string}>}
|
|
45
|
+
*/
|
|
46
|
+
export function runWithDeadline(spec, opts = {}) {
|
|
47
|
+
const { modulePath, exportName, args = [] } = spec || {};
|
|
48
|
+
const timeoutMs = opts.timeoutMs;
|
|
49
|
+
const graceMs = opts.graceMs ?? DEFAULT_GRACE_MS;
|
|
50
|
+
const workerScript = opts.workerScript || DEFAULT_WORKER_SCRIPT;
|
|
51
|
+
|
|
52
|
+
if (!modulePath || !exportName) {
|
|
53
|
+
return Promise.resolve({ ok: false, error: 'runWithDeadline requires spec.modulePath and spec.exportName' });
|
|
54
|
+
}
|
|
55
|
+
if (!(Number.isFinite(timeoutMs) && timeoutMs > 0)) {
|
|
56
|
+
return Promise.resolve({ ok: false, error: 'runWithDeadline requires opts.timeoutMs > 0' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
let settled = false;
|
|
61
|
+
const worker = new Worker(workerScript, { workerData: { modulePath, exportName, args } });
|
|
62
|
+
|
|
63
|
+
const finish = (value) => {
|
|
64
|
+
if (settled) return;
|
|
65
|
+
settled = true;
|
|
66
|
+
clearTimeout(killTimer);
|
|
67
|
+
worker.terminate().catch(() => {});
|
|
68
|
+
resolve(value);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// The one preemptive mechanism: a real OS-level Worker.terminate(),
|
|
72
|
+
// triggered by a real timer, not a cooperative clock-check inside the
|
|
73
|
+
// work itself. This is what makes this different from the existing
|
|
74
|
+
// _perFileTimeoutMs / deadlineMs mechanisms.
|
|
75
|
+
const killTimer = setTimeout(() => {
|
|
76
|
+
finish({ ok: false, timedOut: true, timeoutMs, graceMs });
|
|
77
|
+
}, timeoutMs + graceMs);
|
|
78
|
+
killTimer.unref?.();
|
|
79
|
+
|
|
80
|
+
worker.once('message', (msg) => {
|
|
81
|
+
if (msg && msg.ok) finish({ ok: true, result: msg.result });
|
|
82
|
+
else finish({ ok: false, error: (msg && msg.error) || 'unknown worker error' });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
worker.once('error', (err) => {
|
|
86
|
+
finish({ ok: false, error: String((err && err.message) || err) });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
worker.once('exit', (code) => {
|
|
90
|
+
finish({ ok: false, error: `worker exited with code ${code} before reporting a result` });
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Worker-thread entry point for analyzer-supervisor.js (assurance-hardening
|
|
2
|
+
// PRD FR-202, D-0046 phase 1).
|
|
3
|
+
//
|
|
4
|
+
// Generic by design: workerData names a module and an export to call with a
|
|
5
|
+
// list of plain arguments, so this file has no knowledge of what analyzer it
|
|
6
|
+
// is running. The supervisor is the only caller; this file is not meant to
|
|
7
|
+
// be imported directly.
|
|
8
|
+
|
|
9
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
10
|
+
|
|
11
|
+
async function run() {
|
|
12
|
+
try {
|
|
13
|
+
const { modulePath, exportName, args } = workerData;
|
|
14
|
+
const mod = await import(modulePath);
|
|
15
|
+
const fn = mod[exportName];
|
|
16
|
+
if (typeof fn !== 'function') {
|
|
17
|
+
throw new Error(`export "${exportName}" is not a function in ${modulePath}`);
|
|
18
|
+
}
|
|
19
|
+
const result = await fn(...(args || []));
|
|
20
|
+
parentPort.postMessage({ ok: true, result });
|
|
21
|
+
} catch (e) {
|
|
22
|
+
parentPort.postMessage({ ok: false, error: String((e && e.message) || e) });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
run();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Annotator runner (assurance-hardening PRD, Milestone 0/1, FR-106).
|
|
2
|
+
//
|
|
3
|
+
// Extracted from engine.js's local `_runAnnotator` closure so the Promise-
|
|
4
|
+
// aware error-capture contract can be fault-injection tested directly,
|
|
5
|
+
// without needing a real annotator to actually throw (this codebase's
|
|
6
|
+
// annotators are deliberately built never to — see posture/CLAUDE.md's "no
|
|
7
|
+
// throwing" convention — which is good for production but means there was
|
|
8
|
+
// no natural way to prove the capture path works end-to-end before this
|
|
9
|
+
// module existed). engine.js's `_runAnnotator` is now a thin wrapper that
|
|
10
|
+
// closes over its local `_annotatorErrors` array and delegates here; this
|
|
11
|
+
// module has no engine.js-specific state, so it takes the errors array as a
|
|
12
|
+
// parameter instead of a closure.
|
|
13
|
+
//
|
|
14
|
+
// Contract: a rejection or thrown error from `fn()` — sync or async — is
|
|
15
|
+
// captured as a structured entry, never left to become an unhandled
|
|
16
|
+
// rejection or to race ahead of the caller. The caller MUST `await` this
|
|
17
|
+
// function for the second half of that contract (capture-before-return) to
|
|
18
|
+
// hold; see engine.js's call sites, all of which now do.
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {Array<{phase:string, err:string}>} annotatorErrors - mutated in place (push only)
|
|
22
|
+
* @param {string} phase - name recorded on a captured error
|
|
23
|
+
* @param {() => any} fn - sync or async callback
|
|
24
|
+
* @returns {Promise<any>} fn()'s resolved value, or undefined if it threw/rejected
|
|
25
|
+
*/
|
|
26
|
+
export async function runAnnotatorAsync(annotatorErrors, phase, fn) {
|
|
27
|
+
try {
|
|
28
|
+
return await fn();
|
|
29
|
+
} catch (e) {
|
|
30
|
+
annotatorErrors.push({ phase, err: String((e && e.message) || e) });
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Assurance modes (assurance-hardening PRD FR-204).
|
|
2
|
+
//
|
|
3
|
+
// "Add assurance modes: advisory, standard, and strict | Strict mode fails
|
|
4
|
+
// when a required analyzer fails, times out, is unavailable, or is
|
|
5
|
+
// silently skipped." Section 12.1 names the CLI surface:
|
|
6
|
+
// `--assurance advisory|standard|strict`.
|
|
7
|
+
//
|
|
8
|
+
// Built directly on FR-203's coverage ledger (pipeline/coverage-ledger.js)
|
|
9
|
+
// and FR-206's scan-health.js -- this module adds no new signal collection
|
|
10
|
+
// of its own, only a POLICY over signals that already exist. That is
|
|
11
|
+
// deliberate: assurance modes decide how strict to be about incomplete
|
|
12
|
+
// analysis; they must never be the thing that DEFINES what "incomplete"
|
|
13
|
+
// means, or the two concepts would drift out of sync.
|
|
14
|
+
//
|
|
15
|
+
// THREE MODES:
|
|
16
|
+
// advisory — never fails the gate over scan health, regardless of what
|
|
17
|
+
// scanHealth reports. Purely informational (surfaced in scanHealth /
|
|
18
|
+
// toShipVerdict / `ci`'s stderr, per FR-206's fix -- this mode does not
|
|
19
|
+
// suppress that, it just does not ADD a build-failing consequence).
|
|
20
|
+
// standard (the DEFAULT, matching this codebase's behavior before and
|
|
21
|
+
// after FR-204) — same as advisory for gate purposes: an incomplete
|
|
22
|
+
// scan is surfaced, never silently hidden, but does not itself fail a
|
|
23
|
+
// build independent of the ordinary --fail-on severity threshold. The
|
|
24
|
+
// distinction from advisory is one of INTENT/reporting emphasis, not
|
|
25
|
+
// mechanism -- see the module-level note below on why this codebase
|
|
26
|
+
// does not invent a mechanical difference the PRD text does not ask
|
|
27
|
+
// for.
|
|
28
|
+
// strict — an incomplete scan (any analyzer failed, timed out, or was
|
|
29
|
+
// skipped by policy -- the ledger's own three non-"completed" outcomes)
|
|
30
|
+
// is a HARD FAILURE, independent of and IN ADDITION TO --fail-on,
|
|
31
|
+
// mirroring cmdCi's own existing precedent for its --policy gate
|
|
32
|
+
// ("Policy runs ALONGSIDE the --fail-on threshold; either gate can
|
|
33
|
+
// fail the build").
|
|
34
|
+
//
|
|
35
|
+
// WHY ADVISORY AND STANDARD ARE MECHANICALLY IDENTICAL HERE: the PRD's own
|
|
36
|
+
// one-line acceptance criterion only specifies STRICT mode's behavior in
|
|
37
|
+
// full ("fails when..."); it does not name a distinct mechanical
|
|
38
|
+
// consequence for standard beyond "the default, not strict." Inventing an
|
|
39
|
+
// intermediate failure condition neither named in the PRD nor requested
|
|
40
|
+
// would be exactly the kind of unrequested scope-widening this codebase's
|
|
41
|
+
// own conventions warn against. If a future requirement needs standard
|
|
42
|
+
// mode to behave differently from advisory, that is a deliberate, separate
|
|
43
|
+
// decision -- not something to guess at here.
|
|
44
|
+
|
|
45
|
+
import { isProvenanceHealthy } from '../posture/provenance/schema.js';
|
|
46
|
+
|
|
47
|
+
export const ASSURANCE_MODES = Object.freeze(['advisory', 'standard', 'strict']);
|
|
48
|
+
export const DEFAULT_ASSURANCE_MODE = 'standard';
|
|
49
|
+
|
|
50
|
+
function _isValidMode(mode) {
|
|
51
|
+
return ASSURANCE_MODES.includes(mode);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} mode - one of ASSURANCE_MODES; invalid/missing degrades to the default.
|
|
56
|
+
* @param {object|null} scanHealth - the engine's computed scan.scanHealth (FR-206).
|
|
57
|
+
* @returns {{ok: boolean, mode: string, reason: string|null, conditions: string[]}}
|
|
58
|
+
* ok:false only ever happens in strict mode; advisory/standard always ok:true
|
|
59
|
+
* (they report, they do not gate).
|
|
60
|
+
*/
|
|
61
|
+
export function evaluateAssuranceMode(mode, scanHealth, findings = []) {
|
|
62
|
+
const effectiveMode = _isValidMode(mode) ? mode : DEFAULT_ASSURANCE_MODE;
|
|
63
|
+
const conditions = Array.isArray(scanHealth?.conditions) ? scanHealth.conditions : [];
|
|
64
|
+
|
|
65
|
+
if (effectiveMode !== 'strict') {
|
|
66
|
+
return { ok: true, mode: effectiveMode, reason: null, conditions };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Strict: any of the ledger's three non-"completed" analyzer outcomes,
|
|
70
|
+
// OR an annotator error / deep-mode failure (scanHealth's OTHER,
|
|
71
|
+
// non-analyzer conditions) -- "a required analyzer... is unavailable, or
|
|
72
|
+
// is silently skipped" covers more than just coverage-ledger.js's own
|
|
73
|
+
// per-detector accounting; a scan whose ANY known-good signal degraded
|
|
74
|
+
// is not "complete" under strict's own definition.
|
|
75
|
+
const status = scanHealth?.status;
|
|
76
|
+
if (!scanHealth || status !== 'complete') {
|
|
77
|
+
const a = scanHealth?.analyzers;
|
|
78
|
+
const parts = [];
|
|
79
|
+
if (a?.failed > 0) parts.push(`${a.failed} analyzer(s) failed`);
|
|
80
|
+
if (a?.timedOut > 0) parts.push(`${a.timedOut} analyzer(s) timed out`);
|
|
81
|
+
if (a?.skippedByPolicy > 0) parts.push(`${a.skippedByPolicy} analyzer(s) silently skipped by policy`);
|
|
82
|
+
const analyzerSummary = parts.length ? ` (${parts.join(', ')})` : '';
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
mode: 'strict',
|
|
86
|
+
reason: `strict mode requires a fully complete scan; scanHealth.status is '${status ?? 'unknown'}'${analyzerSummary}`,
|
|
87
|
+
conditions,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// M2 §2.5: strict cares about overall scan completeness, which now
|
|
92
|
+
// explicitly includes PROVENANCE completeness, not just detector/analyzer
|
|
93
|
+
// completeness. A finding whose findingProvenance status is outside
|
|
94
|
+
// ['complete','uncommitted'] — including a finding with NO
|
|
95
|
+
// findingProvenance at all, e.g. --no-provenance was used — means strict
|
|
96
|
+
// cannot vouch for this scan's provenance the same way it already refuses
|
|
97
|
+
// to vouch for a scan with a failed analyzer.
|
|
98
|
+
//
|
|
99
|
+
// KNOWN INTERACTION: scan.secrets/scan.logicVulns are unconditionally
|
|
100
|
+
// stamped not_available today (M0+M1 deliberately deferred real origin
|
|
101
|
+
// resolution for those two channels — see the M2/M3/M4 design spec's
|
|
102
|
+
// §2.6). Any real secret or logic finding therefore fails strict mode
|
|
103
|
+
// until that resolution work lands. This is the literal, intended
|
|
104
|
+
// consequence of "never false certainty" applied to strict's own
|
|
105
|
+
// definition, not an oversight — a strict-mode operator with secrets
|
|
106
|
+
// findings should expect this until M3+ closes that gap.
|
|
107
|
+
//
|
|
108
|
+
// This list is INCOMPLETE without scan.supplyChain, and the omission
|
|
109
|
+
// matters more than the secrets/logic one above because it hits nearly
|
|
110
|
+
// every real project. engine.js stamps every supplyChain entry
|
|
111
|
+
// not_available too (see the loop over `supplyChain` right after the
|
|
112
|
+
// `annotateGitProvenance` calls), and that bucket covers three distinct
|
|
113
|
+
// populations, not one:
|
|
114
|
+
//
|
|
115
|
+
// - transitive `vulnerable_dep` findings: a genuine, if currently
|
|
116
|
+
// unresolved, DEFERRAL — same shape as secrets/logicVulns above. The
|
|
117
|
+
// vulnerable version was never declared in this repo's own manifests,
|
|
118
|
+
// so there is no local commit to walk yet, but one could exist to
|
|
119
|
+
// resolve in a later phase.
|
|
120
|
+
// - `unpinned_dep` / `no_lockfile` findings: a CATEGORY ERROR, not a
|
|
121
|
+
// deferral. These describe an ABSENT state (a version range with no
|
|
122
|
+
// pin, a manifest with no lockfile) — there is no "commit that
|
|
123
|
+
// introduced a missing lockfile" for any future resolver to find,
|
|
124
|
+
// because the finding is about the absence of an event, not an event
|
|
125
|
+
// itself. No amount of future engineering work makes these resolvable.
|
|
126
|
+
//
|
|
127
|
+
// Direct `vulnerable_dep` findings DO go through real origin resolution
|
|
128
|
+
// (`resolveDirectSCAOrigin`, gated on `isDirect`) and are not part of this
|
|
129
|
+
// limitation.
|
|
130
|
+
//
|
|
131
|
+
// Net effect: because `unpinned_dep`/`no_lockfile` findings are a category
|
|
132
|
+
// error rather than a deferral, `--assurance strict` will fail on nearly
|
|
133
|
+
// any real project that has a `package.json` (or equivalent manifest)
|
|
134
|
+
// today — an unpinned or unlocked dependency is common, and this check has
|
|
135
|
+
// no way to ever resolve one. This is a known, disclosed limitation of the
|
|
136
|
+
// current implementation, not a bug, and it is not something this check
|
|
137
|
+
// should route around: exempting these finding types from the strict-mode
|
|
138
|
+
// gate was considered and deliberately deferred to a future milestone
|
|
139
|
+
// rather than done here, so strict mode keeps refusing to vouch for
|
|
140
|
+
// provenance it cannot actually speak to.
|
|
141
|
+
const badProvenance = (Array.isArray(findings) ? findings : []).filter((f) => !isProvenanceHealthy(f?.findingProvenance));
|
|
142
|
+
if (badProvenance.length > 0) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
mode: 'strict',
|
|
146
|
+
reason: `strict mode requires complete finding provenance; ${badProvenance.length} finding(s) have status outside [complete, uncommitted]`,
|
|
147
|
+
conditions,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { ok: true, mode: 'strict', reason: null, conditions };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const _internals = { _isValidMode };
|