@clear-capabilities/agentic-security-scanner 0.175.0 → 0.176.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.
Files changed (101) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/bin/agentic-security.js +199 -28
  3. package/dist/2066.index.js +850 -0
  4. package/dist/2199.index.js +1 -1
  5. package/dist/2584.index.js +3332 -0
  6. package/dist/2923.index.js +13 -0
  7. package/dist/2946.index.js +81 -0
  8. package/dist/3196.index.js +27 -0
  9. package/dist/3727.index.js +292 -0
  10. package/dist/4168.index.js +509 -0
  11. package/dist/4281.index.js +993 -0
  12. package/dist/4978bec8fdf5c733dc85.json +283 -0
  13. package/dist/5099.index.js +96 -0
  14. package/dist/5616.index.js +1471 -0
  15. package/dist/5620.index.js +2370 -0
  16. package/dist/5817.index.js +714 -0
  17. package/dist/6333.index.js +98 -0
  18. package/dist/6655.index.js +3221 -0
  19. package/dist/7838.index.js +446 -974
  20. package/dist/818.index.js +4 -3
  21. package/dist/83.index.js +159 -0
  22. package/dist/8551.index.js +82 -0
  23. package/dist/8633.index.js +93 -0
  24. package/dist/8714.index.js +76 -76
  25. package/dist/8825.index.js +84 -0
  26. package/dist/932.index.js +186 -0
  27. package/dist/9560.index.js +358 -19
  28. package/dist/9800.index.js +6 -1
  29. package/dist/agentic-security.mjs +10 -10
  30. package/dist/agentic-security.mjs.sha256 +1 -1
  31. package/dist/df1e8683e7eda7b2b79c.js +29 -0
  32. package/package.json +5 -4
  33. package/src/artifact/CLAUDE.md +4 -0
  34. package/src/artifact/artifact-evidence.js +63 -0
  35. package/src/artifact/cosign.js +61 -0
  36. package/src/artifact/engine-source.js +72 -0
  37. package/src/artifact/oci-layout.js +47 -0
  38. package/src/artifact/registry.js +213 -0
  39. package/src/capabilities/tool-registry.js +2 -0
  40. package/src/lineage/CLAUDE.md +4 -0
  41. package/src/lineage/identity-token.js +11 -0
  42. package/src/lineage/observation-adapters.js +13 -0
  43. package/src/lineage/runtime-edge-states.js +139 -0
  44. package/src/lineage/runtime-explorer-view.js +82 -0
  45. package/src/lineage/runtime-observation.js +11 -0
  46. package/src/mcp/CLAUDE.md +3 -1
  47. package/src/mcp/proof-tools.js +132 -0
  48. package/src/mcp/server.js +1 -1
  49. package/src/mcp/tools.js +3 -1
  50. package/src/posture/CLAUDE.md +23 -0
  51. package/src/posture/artifact-registry.js +4 -0
  52. package/src/posture/dnr/attack-facts.js +31 -0
  53. package/src/posture/dnr/cli.js +138 -0
  54. package/src/posture/dnr/detection-ir.js +123 -0
  55. package/src/posture/dnr/exposure-window.js +134 -0
  56. package/src/posture/dnr/hunt.js +84 -0
  57. package/src/posture/dnr/log-reader.js +189 -0
  58. package/src/posture/dnr/matchers.js +90 -0
  59. package/src/posture/dnr/outputs.js +262 -0
  60. package/src/posture/dnr/payload-classes.js +120 -0
  61. package/src/posture/dnr/redact.js +25 -0
  62. package/src/posture/dnr/store.js +57 -0
  63. package/src/posture/dnr/validate.js +64 -0
  64. package/src/posture/dnr/view.js +51 -0
  65. package/src/posture/evaluation/lab/arm-ollama.js +88 -0
  66. package/src/posture/evaluation/lab/arms.js +142 -0
  67. package/src/posture/evaluation/lab/cli.js +135 -0
  68. package/src/posture/evaluation/lab/dataset.js +139 -0
  69. package/src/posture/evaluation/lab/materialize.js +44 -0
  70. package/src/posture/evaluation/lab/neutralize.js +144 -0
  71. package/src/posture/evaluation/lab/report.js +176 -0
  72. package/src/posture/evaluation/lab/run.js +225 -0
  73. package/src/posture/evaluation/lab/score.js +135 -0
  74. package/src/posture/evaluation/lab/scoreboard.js +48 -0
  75. package/src/posture/evaluation/lab/stats.js +41 -0
  76. package/src/posture/evaluation/lab/store.js +53 -0
  77. package/src/posture/fleet/cli.js +248 -63
  78. package/src/posture/fleet/discover-edges.js +135 -0
  79. package/src/posture/fleet/enterprise-graph.js +148 -0
  80. package/src/posture/fleet/fleet-graph.js +9 -4
  81. package/src/posture/fleet/fleet-manifest.js +50 -5
  82. package/src/posture/fleet/job-runner.js +153 -0
  83. package/src/posture/fleet/job-store.js +136 -0
  84. package/src/posture/fleet/org-health.js +56 -0
  85. package/src/posture/fleet/scan-cache.js +112 -0
  86. package/src/posture/fleet/scan-child.js +29 -0
  87. package/src/posture/proof-fabric/cli.js +34 -6
  88. package/src/posture/proof-fabric/container.js +57 -0
  89. package/src/posture/proof-fabric/fix-proof.js +2 -1
  90. package/src/posture/proof-fabric/generate-python.js +129 -0
  91. package/src/posture/proof-fabric/generate.js +213 -0
  92. package/src/posture/proof-fabric/reproducer.js +3 -0
  93. package/src/posture/proof-fabric/runner.js +20 -11
  94. package/src/posture/proof-fabric/siblings.js +77 -0
  95. package/src/posture/routing/policy-router.js +103 -0
  96. package/src/posture/rule-compiler/cli.js +64 -6
  97. package/src/posture/rule-compiler/corpus.js +10 -2
  98. package/src/posture/rule-compiler/mutation.js +72 -1
  99. package/src/posture/rule-compiler/promotion.js +21 -5
  100. package/src/posture/rule-compiler/proposal.js +132 -0
  101. package/src/posture/rule-compiler/store.js +16 -0
@@ -0,0 +1,84 @@
1
+ export const id = 8825;
2
+ export const ids = [8825];
3
+ export const modules = {
4
+
5
+ /***/ 58825:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ ARTIFACT_BUNDLE_SCHEMA: () => (/* binding */ ARTIFACT_BUNDLE_SCHEMA),
10
+ /* harmony export */ buildArtifactEvidenceBundle: () => (/* binding */ buildArtifactEvidenceBundle),
11
+ /* harmony export */ ensureKeyPair: () => (/* reexport safe */ _posture_evidence_bundle_js__WEBPACK_IMPORTED_MODULE_1__.ensureKeyPair),
12
+ /* harmony export */ signArtifactEvidenceBundle: () => (/* binding */ signArtifactEvidenceBundle),
13
+ /* harmony export */ verifyArtifactEvidenceBundle: () => (/* binding */ verifyArtifactEvidenceBundle)
14
+ /* harmony export */ });
15
+ /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(77598);
16
+ /* harmony import */ var _posture_evidence_bundle_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(98317);
17
+ // A signed, portable evidence bundle for one image scan (PRD 5; the sibling-bundle pattern of provenance-evidence-bundle.js).
18
+ // Ed25519, verifiable with only the public key, same key material and the same defence as its siblings: an ALLOWLIST of top-level
19
+ // keys is signed, and a bundle with any other key is refused, because a signature covers only the bytes it was computed over.
20
+ //
21
+ // What it carries is what the scan found, and nothing that is secret: secret findings are reported by layer, path and kind, never by
22
+ // value. What it proves is that the record is unmodified since signing. It does not prove the scan was complete or the image safe;
23
+ // the truncation flags and the signature state travel inside it so a reader can see how much was and was not checked.
24
+
25
+
26
+
27
+ const ARTIFACT_BUNDLE_SCHEMA = 'agentic-security/artifact-evidence@1';
28
+ const PROVES = 'This bundle\'s contents are exactly what was signed at attestation time.';
29
+ const DOES_NOT_PROVE = 'This bundle does NOT prove the image is free of vulnerabilities or secrets: read `scan.truncated` and `scan.vulnerabilityLookup` for how much was checked, and `signature` for whether the image\'s own signature was verified. It proves the RECORD is unmodified, not that the conclusions are complete.';
30
+ const MAX_ITEMS = 500;
31
+
32
+ function buildArtifactEvidenceBundle(record, { engineVersion = null, source = null, signatureResult = null } = {}) {
33
+ if (!record?.inventory?.digest) return null;
34
+ const inv = record.inventory;
35
+ const vulns = (record.vulnerabilities || []).filter((v) => v?.type === 'vulnerable_dep');
36
+ return {
37
+ schema: ARTIFACT_BUNDLE_SCHEMA,
38
+ artifact: {
39
+ digest: inv.digest, digestKind: inv.digestKind ?? null, format: inv.format ?? null, os: inv.os ?? null, architecture: inv.architecture ?? null,
40
+ labels: inv.buildLabels ?? inv.labels ?? null, source: source ? { kind: source.kind ?? null, registry: source.registry ?? null, repository: source.repository ?? null, reference: source.reference ?? null, runtime: source.runtime ?? null } : null,
41
+ },
42
+ scan: {
43
+ scannedAt: record.scannedAt ?? null,
44
+ packages: { os: inv.osPackages?.length ?? 0, app: inv.appPackages?.length ?? 0 },
45
+ vulnerabilities: vulns.slice(0, MAX_ITEMS).map((v) => ({ id: v.id ?? v.vulnId ?? null, package: v.package ?? v.name ?? null, version: v.version ?? null, severity: v.severity ?? null, cve: v.cve ?? null, kev: v.kev ?? null })),
46
+ secrets: (record.secrets?.deletedButRecoverable || []).slice(0, MAX_ITEMS).map((s) => ({ layer: s.layerIndex ?? s.addedInLayer ?? null, path: s.path ?? null, kind: s.kind ?? s.type ?? s.vuln ?? null })),
47
+ truncated: { materialization: !!inv.materializationTruncated, vulnerabilitiesListed: Math.min(vulns.length, MAX_ITEMS), vulnerabilitiesTotal: vulns.length, secretsListed: Math.min(record.secrets?.deletedButRecoverable?.length ?? 0, MAX_ITEMS), secretsTotal: record.secrets?.deletedButRecoverable?.length ?? 0 },
48
+ vulnerabilityLookup: record.vulnerabilityLookup ?? 'osv (cached or live; see scannedAt)',
49
+ },
50
+ signature: signatureResult ? { state: signatureResult.state ?? null, via: signatureResult.via ?? null, reason: signatureResult.reason ?? null } : { state: 'not-checked', via: null, reason: 'no signature verification was requested for this bundle' },
51
+ engine: { engineVersion },
52
+ proves: PROVES,
53
+ doesNotProve: DOES_NOT_PROVE,
54
+ };
55
+ }
56
+
57
+ function signArtifactEvidenceBundle(bundle, privateKeyPem) {
58
+ const sig = node_crypto__WEBPACK_IMPORTED_MODULE_0__.sign(null, Buffer.from((0,_posture_evidence_bundle_js__WEBPACK_IMPORTED_MODULE_1__/* .canonicalJson */ .dj)(bundle), 'utf8'), privateKeyPem);
59
+ return { ...bundle, attestation: { algorithm: 'ed25519', canonicalisation: ARTIFACT_BUNDLE_SCHEMA, value: sig.toString('base64') } };
60
+ }
61
+
62
+ const TOP_LEVEL = new Set(['schema', 'artifact', 'scan', 'signature', 'engine', 'proves', 'doesNotProve', 'attestation']);
63
+
64
+ function verifyArtifactEvidenceBundle(bundle, publicKeyPem) {
65
+ if (!bundle || typeof bundle !== 'object') return { ok: false, reason: 'bundle is not an object' };
66
+ if (bundle.schema !== ARTIFACT_BUNDLE_SCHEMA) return { ok: false, reason: `unrecognised schema: ${bundle.schema}` };
67
+ const unknown = Object.keys(bundle).filter((k) => !TOP_LEVEL.has(k));
68
+ if (unknown.length) return { ok: false, reason: `unrecognised top-level key(s) not covered by the signature: ${unknown.join(', ')}` };
69
+ const att = bundle.attestation;
70
+ if (!att?.value) return { ok: false, reason: 'bundle is unsigned' };
71
+ if (att.algorithm !== 'ed25519') return { ok: false, reason: `unsupported algorithm: ${att.algorithm}` };
72
+ if (!publicKeyPem) return { ok: false, reason: 'no public key supplied' };
73
+ const { attestation, ...unsigned } = bundle;
74
+ let ok = false;
75
+ try { ok = node_crypto__WEBPACK_IMPORTED_MODULE_0__.verify(null, Buffer.from((0,_posture_evidence_bundle_js__WEBPACK_IMPORTED_MODULE_1__/* .canonicalJson */ .dj)(unsigned), 'utf8'), publicKeyPem, Buffer.from(att.value, 'base64')); } catch (e) { return { ok: false, reason: `verification error: ${e.message}` }; }
76
+ return ok ? { ok: true, reason: null } : { ok: false, reason: 'signature does not match the bundle contents — it was modified after signing' };
77
+ }
78
+
79
+
80
+
81
+
82
+ /***/ })
83
+
84
+ };
@@ -0,0 +1,186 @@
1
+ export const id = 932;
2
+ export const ids = [932,2199,8551];
3
+ export const modules = {
4
+
5
+ /***/ 58551:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ __webpack_require__.r(__webpack_exports__);
9
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
10
+ /* harmony export */ hasTamperedRecords: () => (/* binding */ hasTamperedRecords),
11
+ /* harmony export */ latestRecord: () => (/* binding */ latestRecord),
12
+ /* harmony export */ listRecords: () => (/* binding */ listRecords),
13
+ /* harmony export */ saveRecord: () => (/* binding */ saveRecord)
14
+ /* harmony export */ });
15
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
16
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(76760);
17
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(31174);
18
+ /* harmony import */ var _encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(39984);
19
+ /* harmony import */ var _proof_fabric_reproducer_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(37055);
20
+ /* harmony import */ var _proof_fabric_store_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(62199);
21
+ // Persistence for the detection-and-response loop: `.agentic-security/dnr/<findingKey>/<kind>-<digest>.json`.
22
+ // Same machinery and the same guarantees as the proof store, reused rather than copied where it is exported:
23
+ // safe state-directory writes (no symlinks, no unsafe dirs), the encryption policy, retention as evidence, legal holds, and
24
+ // a recursive secret scrub before anything touches disk. A record that no longer matches its own digest is returned FLAGGED,
25
+ // never trusted and never silently dropped.
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+ const DIR = 'dnr';
34
+ const MAX_RECORD_BYTES = 1024 * 1024;
35
+ const RECORD_KINDS = Object.freeze(['detection', 'hunt', 'window', 'fix']);
36
+
37
+ const digestOf = (record) => { const { recordDigest: _omit, ...rest } = record; return (0,_proof_fabric_reproducer_js__WEBPACK_IMPORTED_MODULE_4__/* .sha256 */ .sc)((0,_proof_fabric_reproducer_js__WEBPACK_IMPORTED_MODULE_4__/* .canonicalize */ .dw)(rest)); };
38
+
39
+ function saveRecord(root, findingId, kind, record) {
40
+ if (!RECORD_KINDS.includes(kind)) return { ok: false, reason: `unknown record kind: ${kind}` };
41
+ const safe = (0,_proof_fabric_store_js__WEBPACK_IMPORTED_MODULE_5__.scrubDeep)(record);
42
+ safe.schema = `agentic-security/dnr-${kind}-record`;
43
+ safe.kind = kind;
44
+ safe.findingId = String(findingId);
45
+ safe.savedAt = record.savedAt ?? new Date().toISOString();
46
+ safe.recordDigest = digestOf(safe);
47
+ const text = JSON.stringify(safe, null, 2);
48
+ if (Buffer.byteLength(text) > MAX_RECORD_BYTES) return { ok: false, reason: 'record exceeds the size limit' };
49
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.ensureStateDir)(root)) return { ok: false, reason: 'no safe state directory for this project' };
50
+ const enc = (0,_encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__/* .maybeEncryptForWrite */ .v2)(root, DIR, text);
51
+ if (!enc.ok) return { ok: false, reason: enc.reason };
52
+ const file = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(root, DIR, (0,_proof_fabric_store_js__WEBPACK_IMPORTED_MODULE_5__.findingKey)(findingId), `${kind}-${safe.recordDigest.slice(0, 16)}.json`);
53
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.safeWriteState)(file, enc.content)) return { ok: false, reason: 'state write refused' };
54
+ return { ok: true, file, digest: safe.recordDigest, encrypted: enc.encrypted };
55
+ }
56
+
57
+ /** Newest-first records of one kind for a finding. Tampered records are flagged. */
58
+ function listRecords(root, findingId, kind) {
59
+ const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(root, DIR, (0,_proof_fabric_store_js__WEBPACK_IMPORTED_MODULE_5__.findingKey)(findingId));
60
+ let names = [];
61
+ try { names = node_fs__WEBPACK_IMPORTED_MODULE_0__.readdirSync(dir).filter((n) => n.startsWith(`${kind}-`) && n.endsWith('.json')); } catch { return []; }
62
+ const out = [];
63
+ for (const n of names) {
64
+ const file = node_path__WEBPACK_IMPORTED_MODULE_1__.join(dir, n);
65
+ try {
66
+ const rec = JSON.parse((0,_encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__.maybeDecryptForRead)(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(file, 'utf8')));
67
+ out.push({ file, record: rec, tampered: rec.recordDigest !== digestOf(rec), mtime: node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(file).mtimeMs });
68
+ } catch { out.push({ file, record: null, tampered: true, mtime: 0 }); }
69
+ }
70
+ return out.sort((a, b) => b.mtime - a.mtime);
71
+ }
72
+
73
+ /** Newest intact record of a kind, or null. A tampered newest record does NOT fall through silently: the caller can see it via listRecords. */
74
+ function latestRecord(root, findingId, kind) {
75
+ return listRecords(root, findingId, kind).find((r) => r.record && !r.tampered) ?? null;
76
+ }
77
+ const hasTamperedRecords = (root, findingId, kind) => listRecords(root, findingId, kind).some((r) => r.tampered);
78
+
79
+
80
+ /***/ }),
81
+
82
+ /***/ 62199:
83
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
84
+
85
+ __webpack_require__.r(__webpack_exports__);
86
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
87
+ /* harmony export */ findingKey: () => (/* binding */ findingKey),
88
+ /* harmony export */ latestProof: () => (/* binding */ latestProof),
89
+ /* harmony export */ listProofs: () => (/* binding */ listProofs),
90
+ /* harmony export */ saveProof: () => (/* binding */ saveProof),
91
+ /* harmony export */ scrubDeep: () => (/* binding */ scrubDeep)
92
+ /* harmony export */ });
93
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
94
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(76760);
95
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(31174);
96
+ /* harmony import */ var _encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(39984);
97
+ /* harmony import */ var _llm_validator_redact_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(65388);
98
+ /* harmony import */ var _reproducer_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(37055);
99
+ // Proof artifact store: `.agentic-security/proofs/<findingKey>/<recordDigest>.json`.
100
+ //
101
+ // Reuses the existing state machinery rather than adding another: `safeWriteState` (refuses unsafe dirs and symlinks),
102
+ // `maybeEncryptForWrite` / `maybeDecryptForRead` (the `proofs` artifact is registered `confidential`), retention
103
+ // (evidence class) and legal holds (a hold on `proofs` blocks purge). Nothing persisted contains a plaintext credential:
104
+ // records are passed through a recursive secret filter first, and the runner never returns its per-run nonce or secrets.
105
+
106
+
107
+
108
+
109
+
110
+
111
+
112
+ const PROOF_RECORD_SCHEMA = 'agentic-security/proof-record';
113
+ const PROOFS_DIR = 'proofs';
114
+ const MAX_RECORD_BYTES = 512 * 1024;
115
+
116
+ const findingKey = (id) => `${String(id).replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 80)}-${(0,_reproducer_js__WEBPACK_IMPORTED_MODULE_5__/* .sha256 */ .sc)(String(id)).slice(0, 8)}`;
117
+
118
+ // The shared redactor catches key=value shapes; bare tokens need their own pass, so persisted evidence cannot carry one.
119
+ const BARE_SECRET_RES = [
120
+ /\b(?:ghp|gho|ghu|ghs|ghr|github_pat)_[A-Za-z0-9_]{20,}\b/g, /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, /\bsk-[A-Za-z0-9_-]{20,}\b/g,
121
+ /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/g, /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
122
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, /\bSECRET-[0-9a-f]{24}-[a-z0-9_-]+\b/g,
123
+ ];
124
+ function scrubString(text) {
125
+ let t = String(text);
126
+ try { t = (0,_llm_validator_redact_js__WEBPACK_IMPORTED_MODULE_4__/* .redactSecrets */ .f)(t).text; } catch { return '[unredactable]'; }
127
+ for (const re of BARE_SECRET_RES) t = t.replace(re, '[REDACTED-SECRET]');
128
+ return t;
129
+ }
130
+
131
+ /** Deep-copy `v` with every string passed through the secret redactor. */
132
+ function scrubDeep(v) {
133
+ if (typeof v === 'string') return scrubString(v);
134
+ if (Array.isArray(v)) return v.map(scrubDeep);
135
+ if (v && typeof v === 'object') return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, scrubDeep(x)]));
136
+ return v;
137
+ }
138
+
139
+ function recordDigest(record) {
140
+ const { recordDigest: _omit, ...rest } = record;
141
+ return (0,_reproducer_js__WEBPACK_IMPORTED_MODULE_5__/* .sha256 */ .sc)((0,_reproducer_js__WEBPACK_IMPORTED_MODULE_5__/* .canonicalize */ .dw)(rest));
142
+ }
143
+
144
+ function saveProof(scanRoot, record) {
145
+ const safe = scrubDeep(record);
146
+ safe.schema = PROOF_RECORD_SCHEMA;
147
+ safe.recordDigest = recordDigest(safe);
148
+ const text = JSON.stringify(safe, null, 2);
149
+ if (Buffer.byteLength(text) > MAX_RECORD_BYTES) return { ok: false, reason: 'proof record exceeds the size limit' };
150
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.ensureStateDir)(scanRoot)) return { ok: false, reason: 'no safe state directory for this project' };
151
+ const enc = (0,_encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__/* .maybeEncryptForWrite */ .v2)(scanRoot, PROOFS_DIR, text);
152
+ if (!enc.ok) return { ok: false, reason: enc.reason };
153
+ const file = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(scanRoot, PROOFS_DIR, findingKey(record.findingId), `${safe.recordDigest}.json`);
154
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.safeWriteState)(file, enc.content)) return { ok: false, reason: 'state write refused' };
155
+ return { ok: true, file, digest: safe.recordDigest, encrypted: enc.encrypted };
156
+ }
157
+
158
+ function readRecord(file) {
159
+ const text = (0,_encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__.maybeDecryptForRead)(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(file, 'utf8'));
160
+ const rec = JSON.parse(text);
161
+ // A record whose content no longer matches its own digest was modified after it was written.
162
+ if (rec.recordDigest !== recordDigest(rec)) return { tampered: true, record: rec };
163
+ return { tampered: false, record: rec };
164
+ }
165
+
166
+ /** Newest-first records for a finding. Tampered records are returned flagged, never silently dropped or trusted. */
167
+ function listProofs(scanRoot, findingId) {
168
+ const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(scanRoot, PROOFS_DIR, findingKey(findingId));
169
+ let names = [];
170
+ try { names = node_fs__WEBPACK_IMPORTED_MODULE_0__.readdirSync(dir).filter((n) => n.endsWith('.json')); } catch { return []; }
171
+ const out = [];
172
+ for (const n of names) {
173
+ const f = node_path__WEBPACK_IMPORTED_MODULE_1__.join(dir, n);
174
+ try { const { tampered, record } = readRecord(f); out.push({ file: f, tampered, record, mtime: node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(f).mtimeMs }); } catch { out.push({ file: f, tampered: true, record: null, mtime: 0 }); }
175
+ }
176
+ return out.sort((a, b) => b.mtime - a.mtime);
177
+ }
178
+
179
+ function latestProof(scanRoot, findingId) {
180
+ return listProofs(scanRoot, findingId).find((p) => p.record) ?? null;
181
+ }
182
+
183
+
184
+ /***/ })
185
+
186
+ };