@clear-capabilities/agentic-security-scanner 0.142.0 → 0.144.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 (95) hide show
  1. package/CHANGELOG.md +399 -0
  2. package/bin/agentic-security.js +530 -54
  3. package/dist/1.index.js +223 -0
  4. package/dist/113.index.js +108 -17
  5. package/dist/144.index.js +163 -0
  6. package/dist/178.index.js +1 -1
  7. package/dist/238.index.js +3 -2
  8. package/dist/265.index.js +191 -0
  9. package/dist/384.index.js +1 -1
  10. package/dist/435.index.js +165 -52
  11. package/dist/526.index.js +108 -17
  12. package/dist/552.index.js +97 -0
  13. package/dist/637.index.js +1 -1
  14. package/dist/730.index.js +311 -0
  15. package/dist/736.index.js +301 -0
  16. package/dist/824.index.js +7 -0
  17. package/dist/905.index.js +88 -22
  18. package/dist/920.index.js +491 -0
  19. package/dist/970.index.js +109 -0
  20. package/dist/agentic-security.mjs +13 -13
  21. package/dist/agentic-security.mjs.sha256 +1 -1
  22. package/dist/calibration-seed.json +2 -0
  23. package/package.json +19 -11
  24. package/src/dataflow/index.js +18 -0
  25. package/src/dataflow/privacy-catalog.js +290 -0
  26. package/src/dataflow/privacy-deep-walker.js +515 -0
  27. package/src/dataflow/privacy-governance.js +126 -0
  28. package/src/dataflow/privacy-inventory.js +154 -0
  29. package/src/dataflow/privacy-sink-policy.js +125 -0
  30. package/src/dataflow/privacy-taint.js +115 -54
  31. package/src/dataflow/privacy-taxonomy.js +233 -0
  32. package/src/discovery/disprove.js +7 -3
  33. package/src/discovery/hunter.js +9 -5
  34. package/src/discovery/index.js +2 -2
  35. package/src/discovery/llm-invoke.js +69 -13
  36. package/src/egress/audit.js +147 -0
  37. package/src/egress/policy.js +313 -0
  38. package/src/egress/redact.js +180 -0
  39. package/src/engine.js +575 -288
  40. package/src/fix/apply-fix-service.js +403 -0
  41. package/src/fix/approver-registry.js +157 -0
  42. package/src/llm-validator/index.js +86 -9
  43. package/src/llm-validator/model-status.js +66 -0
  44. package/src/mcp/tools.js +157 -50
  45. package/src/pipeline/analyzer-supervisor.js +93 -0
  46. package/src/pipeline/analyzer-worker.js +26 -0
  47. package/src/pipeline/annotator-runner.js +33 -0
  48. package/src/pipeline/assurance-mode.js +91 -0
  49. package/src/pipeline/cascade-worker-pool.js +172 -0
  50. package/src/pipeline/cascade-worker.js +43 -0
  51. package/src/pipeline/coverage-ledger.js +0 -0
  52. package/src/pipeline/detector-runner.js +51 -0
  53. package/src/pipeline/enrichment-completion.js +58 -0
  54. package/src/pipeline/evidence-provenance.js +91 -0
  55. package/src/pipeline/finding-schema.js +101 -0
  56. package/src/pipeline/legacy-compat.js +101 -0
  57. package/src/pipeline/producer-collector.js +48 -0
  58. package/src/pipeline/producer-registry.js +112 -0
  59. package/src/pipeline/scan-health.js +144 -0
  60. package/src/posture/CLAUDE.md +2 -0
  61. package/src/posture/accuracy-scorecard.js +96 -1
  62. package/src/posture/adversary-agent.js +15 -3
  63. package/src/posture/artifact-registry.js +217 -0
  64. package/src/posture/auditor-walkthrough.js +70 -8
  65. package/src/posture/calibration-feedback.js +201 -0
  66. package/src/posture/calibration-seed.json +2 -0
  67. package/src/posture/calibration.js +25 -0
  68. package/src/posture/compliance-evidence-signing.js +131 -0
  69. package/src/posture/compliance-policy.js +314 -17
  70. package/src/posture/custom-rules.js +36 -0
  71. package/src/posture/deterministic.js +8 -1
  72. package/src/posture/encryption-provider.js +205 -0
  73. package/src/posture/evidence-grade-wording.js +71 -0
  74. package/src/posture/fix-history.js +113 -19
  75. package/src/posture/fix-honesty-gate.js +47 -6
  76. package/src/posture/fix-verify.js +56 -7
  77. package/src/posture/fleet.js +0 -0
  78. package/src/posture/flow-narration.js +7 -2
  79. package/src/posture/legal-hold.js +140 -0
  80. package/src/posture/llm-redteam.js +10 -1
  81. package/src/posture/material-change.js +90 -0
  82. package/src/posture/policy-bundle.js +274 -0
  83. package/src/posture/privacy-framework.js +33 -6
  84. package/src/posture/production-feedback.js +179 -0
  85. package/src/posture/retention-policy.js +132 -0
  86. package/src/posture/risk-dollars.js +216 -26
  87. package/src/posture/scan-checkpoint.js +176 -31
  88. package/src/posture/state-dir.js +36 -1
  89. package/src/posture/state-lifecycle-report.js +77 -0
  90. package/src/posture/suppressions.js +59 -3
  91. package/src/privacy/ir-adapter.js +380 -0
  92. package/src/report/index.js +83 -18
  93. package/src/report/oscal.js +635 -0
  94. package/src/sast/cpp.js +3 -14
  95. package/src/sca/llm-function-extract.js +6 -0
package/dist/526.index.js CHANGED
@@ -8,9 +8,10 @@ export const modules = {
8
8
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
9
  /* harmony export */ I3: () => (/* binding */ recordFixAttempt),
10
10
  /* harmony export */ fixDurationReport: () => (/* binding */ fixDurationReport),
11
- /* harmony export */ renderFixDurationSummary: () => (/* binding */ renderFixDurationSummary)
11
+ /* harmony export */ renderFixDurationSummary: () => (/* binding */ renderFixDurationSummary),
12
+ /* harmony export */ sU: () => (/* binding */ loadFixAttempts)
12
13
  /* harmony export */ });
13
- /* unused harmony exports FIX_STAGES, loadFixAttempts, bucketOf, summarizeFixDurations, _internals, summarizeFixAxes, renderFixAxes */
14
+ /* unused harmony exports FIX_STAGES, bucketOf, summarizeFixDurations, _internals, summarizeFixAxes, renderFixAxes */
14
15
  /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
15
16
  /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6760);
16
17
  /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(1174);
@@ -310,8 +311,8 @@ var external_node_child_process_ = __webpack_require__(1421);
310
311
  var external_node_fs_ = __webpack_require__(3024);
311
312
  // EXTERNAL MODULE: external "node:path"
312
313
  var external_node_path_ = __webpack_require__(6760);
313
- // EXTERNAL MODULE: ./src/engine.js + 200 modules
314
- var engine = __webpack_require__(3474);
314
+ // EXTERNAL MODULE: ./src/engine.js + 211 modules
315
+ var engine = __webpack_require__(7087);
315
316
  ;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
316
317
  // Deterministic honesty gates on fix / finding output (#7).
317
318
  //
@@ -456,23 +457,64 @@ function computeFixTier(signals) {
456
457
  }
457
458
 
458
459
  /**
459
- * Compose the three gates for a single fix's output.
460
+ * FR-308: cross-check a fix-completeness TIER against MECHANICAL evidence,
461
+ * when any is available. `signals` (computeFixTier's input) is agent-
462
+ * self-reported — this module cannot compute sinkSignatureChanged /
463
+ * allCallersRouted / testDiscriminates itself (see the header above: "the
464
+ * gate can only run against claims the AGENT self-reports... nothing here
465
+ * is server-computable"). `pocLeg` is different: fix-verify.js's PoC leg is
466
+ * a REAL execution result (posture/CLAUDE.md's execution-proof tiers), not
467
+ * a claim. When it is available and shows the proof-of-concept STILL
468
+ * demonstrates the vulnerability against the patch, a self-reported FULL
469
+ * tier is not merely internally inconsistent — it is REFUTED by fact. This
470
+ * is the literal "a mitigation or workaround cannot be represented as a
471
+ * full fix" acceptance criterion, now backed by mechanical evidence where
472
+ * it exists rather than by self-report consistency alone.
460
473
  *
461
- * ok = residual-honesty ok AND evidence-citation ok, further constrained by the
462
- * tier/residual consistency invariant:
474
+ * A `pocLeg` of `not-requested` or `inconclusive` carries no mechanical
475
+ * signal either way and is a no-op here — this check can only ever ADD a
476
+ * violation on real contrary evidence, never manufacture one from absence.
477
+ *
478
+ * @param {string} tier
479
+ * @param {{status: string, reason?: string}|null} pocLeg
480
+ * @returns {{ ok: boolean, violations: string[] }}
481
+ */
482
+ function checkMechanicalTierEvidence(tier, pocLeg) {
483
+ if (!pocLeg || typeof pocLeg !== 'object') return { ok: true, violations: [] };
484
+ if (tier === 'FULL' && pocLeg.status === 'still-exploitable') {
485
+ return {
486
+ ok: false,
487
+ violations: [`tier 'FULL' is refuted by mechanical evidence: the proof-of-concept still demonstrates the vulnerability against the patch${pocLeg.reason ? ` (${pocLeg.reason})` : ''}`],
488
+ };
489
+ }
490
+ return { ok: true, violations: [] };
491
+ }
492
+
493
+ /**
494
+ * Compose the four gates for a single fix's output.
495
+ *
496
+ * ok = residual-honesty ok AND evidence-citation ok AND mechanical-tier-
497
+ * evidence ok, further constrained by the tier/residual consistency
498
+ * invariant:
463
499
  * - a FULL tier must NOT carry a residual (a full fix has nothing left);
464
500
  * - a non-FULL tier MUST document a residual (say what's still open).
465
501
  *
466
502
  * @param {{ residual?: string, verdict?: string, evidence?: any, signals?: object }} input
503
+ * @param {{ pocLeg?: object|null }} [mechanical] - FR-308: optional real
504
+ * execution evidence (fix-verify.js's pocLeg) to cross-check the
505
+ * self-reported tier against. Omitted entirely by any caller that has no
506
+ * PoC leg to offer — this parameter never REQUIRES mechanical evidence,
507
+ * it only USES it when present.
467
508
  * @returns {{ ok: boolean, tier: string, violations: string[] }}
468
509
  */
469
- function gateFixOutput({ residual, verdict, evidence, signals } = {}) {
510
+ function gateFixOutput({ residual, verdict, evidence, signals } = {}, { pocLeg = null } = {}) {
470
511
  const tier = computeFixTier(signals);
471
512
  const residualCheck = checkResidualHonesty(residual);
472
513
  const evidenceCheck = requireCitedEvidence(verdict, evidence);
514
+ const mechanicalCheck = checkMechanicalTierEvidence(tier, pocLeg);
473
515
 
474
- const violations = [...residualCheck.violations, ...evidenceCheck.violations];
475
- let ok = residualCheck.ok && evidenceCheck.ok;
516
+ const violations = [...residualCheck.violations, ...evidenceCheck.violations, ...mechanicalCheck.violations];
517
+ let ok = residualCheck.ok && evidenceCheck.ok && mechanicalCheck.ok;
476
518
 
477
519
  const residualEmpty = typeof residual !== 'string' || residual.trim() === '';
478
520
  if (tier === 'FULL' && !residualEmpty) {
@@ -830,12 +872,6 @@ async function verifyFix({
830
872
  // and on the result so a caller cannot mistake it for a verified patch.
831
873
  const _testedPrePatch = !tests.skipped && _candidateDiffersFromDisk(scanRoot, files);
832
874
  const testsOk = tests.skipped ? true : tests.passed === true;
833
- let honesty = null;
834
- if (fixMeta && typeof fixMeta === 'object') {
835
- try { honesty = gateFixOutput(fixMeta); } catch { honesty = null; }
836
- }
837
- _lap('honesty');
838
-
839
875
  // R5 — the PoC leg. Re-run the finding's proof-of-concept against the
840
876
  // CANDIDATE patch inside R1's sandbox. A patch that still lets the PoC
841
877
  // demonstrate the predicted effect has not fixed anything, however green the
@@ -847,6 +883,11 @@ async function verifyFix({
847
883
  // to run, or a sandbox that could not start, is recorded as `inconclusive`
848
884
  // and left out of the verdict entirely. Treating "could not prove it" as
849
885
  // "fixed" is exactly the false confidence this leg exists to prevent.
886
+ //
887
+ // Computed BEFORE the honesty gate (FR-308): a still-exploitable PoC is
888
+ // MECHANICAL evidence, not a self-report, and gateFixOutput cross-checks a
889
+ // self-reported FULL tier against it below — the ordering matters, not
890
+ // just the value.
850
891
  let pocLeg = { status: 'not-requested', reason: null, tier: null };
851
892
  if (poc?.code) {
852
893
  try {
@@ -865,7 +906,54 @@ async function verifyFix({
865
906
  _lap('poc');
866
907
  const pocOk = pocLeg.status !== 'still-exploitable';
867
908
 
909
+ // FR-308: "a mitigation or workaround cannot be represented as a full
910
+ // fix" — gateFixOutput's tier/residual check is a pure self-consistency
911
+ // check (fixMeta.signals is agent-self-reported; see fix-honesty-gate.js's
912
+ // header for why nothing there is server-computable). pocLeg IS
913
+ // server-computable — a real execution result, not a claim — so it is
914
+ // passed through as the one MECHANICAL cross-check available: a
915
+ // self-reported FULL tier is refuted, not just internally inconsistent,
916
+ // when the PoC still demonstrates the vulnerability against the patch.
917
+ // D-0024: `fixMeta` is a shared envelope — FR-307/FR-1002's `approval`
918
+ // key lives alongside FR-308's completeness self-report (`residual`/
919
+ // `verdict`/`evidence`/`signals`). Gating on mere object-truthiness meant
920
+ // a caller supplying ONLY `approval` (a real, common shape once the CLI's
921
+ // --approved-by flag and the MCP schema fix made that reachable) got
922
+ // silently gated on an UNRELATED FR-308 self-consistency check it never
923
+ // engaged with — computeFixTier(undefined) defaults to MITIGATION, which
924
+ // then demands a `residual` nobody was ever asked to supply, blocking an
925
+ // otherwise-genuine, approved fix for a reason that has nothing to do
926
+ // with completeness honesty. Scope the gate to fixMeta shapes that
927
+ // actually make a completeness-adjacent claim.
928
+ const hasHonestyClaim = fixMeta && typeof fixMeta === 'object' &&
929
+ (fixMeta.residual !== undefined || fixMeta.verdict !== undefined ||
930
+ fixMeta.evidence !== undefined || fixMeta.signals !== undefined);
931
+ let honesty = null;
932
+ if (hasHonestyClaim) {
933
+ try { honesty = gateFixOutput(fixMeta, { pocLeg }); } catch { honesty = null; }
934
+ }
935
+ _lap('honesty');
936
+
868
937
  const ok = rescan.ok && (lint.ok || lint.skipped) && testsOk && pocOk && (honesty ? honesty.ok : true);
938
+
939
+ // FR-305 (assurance-hardening PRD): `ok` alone conflates "every leg
940
+ // genuinely ran and passed" with "passed, but a required leg was skipped
941
+ // or unavailable" — `lint.ok`/`testsOk` are both true in the skipped case
942
+ // by design (this codebase does not fail-closed just because a repo has
943
+ // no linter or no detected test runner), so a caller checking only `ok`
944
+ // cannot tell the difference. `lint.skipped` is only ever true when a
945
+ // linter WAS configured but its binary could not be run (missing config
946
+ // entirely returns `runner: 'none'` with no `skipped` field at all — that
947
+ // is a genuine N/A, nothing was required, not a degradation). `verifiedFull`
948
+ // is the honest label: true only when nothing that WAS required was
949
+ // skipped. A caller must never present `ok: true, verifiedFull: false` as
950
+ // "fully verified" — `degradedLegs` names exactly what was skipped so a
951
+ // report can say so plainly instead of a bare pass.
952
+ const degradedLegs = [];
953
+ if (lint.skipped) degradedLegs.push(`lint: ${lint.runner} not installed`);
954
+ if (tests.skipped) degradedLegs.push(`tests: skipped (${tests.reason})`);
955
+ const verifiedFull = ok && degradedLegs.length === 0;
956
+
869
957
  const durations = { ...stages, totalMs: Date.now() - t0 };
870
958
  const summary = [
871
959
  `re-scan: ${rescan.ok ? 'PASS' : 'FAIL — ' + rescan.reason}`,
@@ -887,6 +975,8 @@ async function verifyFix({
887
975
  : pocLeg.status === 'still-exploitable' ? `poc: FAIL — the proof-of-concept still demonstrates the vulnerability against the patch`
888
976
  : pocLeg.status === 'no-longer-proven' ? 'poc: PASS (ran against the patch and no longer demonstrates the vulnerability)'
889
977
  : `poc: inconclusive — not counted either way (${pocLeg.reason || 'no detail reported'})`,
978
+ // FR-305: never let a degraded pass read the same as a full one.
979
+ ok && !verifiedFull ? `NOTE: PASSED, but NOT fully verified — ${degradedLegs.join('; ')}` : null,
890
980
  ].filter(Boolean).join('\n');
891
981
  // Persist the attempt so the distribution can be reported from real runs.
892
982
  // `testsRan` is the load-bearing field: it is what keeps "verified with no
@@ -899,6 +989,7 @@ async function verifyFix({
899
989
  at: new Date().toISOString(),
900
990
  stableId: originalFindingStableId || null,
901
991
  ok,
992
+ verifiedFull,
902
993
  testsRan: !tests.skipped,
903
994
  testsPassed: tests.skipped ? null : tests.passed === true,
904
995
  testedPrePatch: _testedPrePatch,
@@ -911,7 +1002,7 @@ async function verifyFix({
911
1002
  });
912
1003
  }
913
1004
 
914
- return { ok, rescan, lint, tests, testedPrePatch: _testedPrePatch, honesty, poc: pocLeg, durations, summary };
1005
+ return { ok, verifiedFull, degradedLegs, rescan, lint, tests, testedPrePatch: _testedPrePatch, honesty, poc: pocLeg, durations, summary };
915
1006
  }
916
1007
 
917
1008
 
@@ -0,0 +1,97 @@
1
+ export const id = 552;
2
+ export const ids = [552];
3
+ export const modules = {
4
+
5
+ /***/ 7552:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ buildDeletionReport: () => (/* binding */ buildDeletionReport),
10
+ /* harmony export */ buildExportReport: () => (/* binding */ buildExportReport),
11
+ /* harmony export */ writeDeletionReport: () => (/* binding */ writeDeletionReport),
12
+ /* harmony export */ writeExportReport: () => (/* binding */ writeExportReport)
13
+ /* harmony export */ });
14
+ /* unused harmony exports DELETION_REPORT_FILE, EXPORT_REPORT_FILE */
15
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1174);
16
+ // FR-706 (assurance-hardening PRD): manifest-based export and deletion
17
+ // reports — "Operators can prove what was exported, deleted, retained, or
18
+ // failed."
19
+ //
20
+ // Two report kinds, one shared per-item shape ({name, status, ...}), each
21
+ // written by its one real caller in bin/agentic-security.js:
22
+ // - `cmdReset` builds a deletion report from the SAME target/preserved
23
+ // data it already computes for its console output (see FR-702/FR-703),
24
+ // reshaped into a durable, structured artifact instead of only ever
25
+ // printed and lost. Written on BOTH a dry run (items carry status
26
+ // 'planned') and a real run (status 'deleted'/'failed'), so an operator
27
+ // can prove either "what would happen" or "what did happen" depending
28
+ // on which they asked for — `dryRun` on the report says which.
29
+ // - `cmdExport` (new) builds an export report after copying every
30
+ // currently-present registered artifact to an operator-chosen
31
+ // destination, naming what was copied (with a hash) and what failed.
32
+ //
33
+ // Each report is a SINGLE OVERWRITTEN file per kind — the same "last
34
+ // action" precedent as last-scan.json, not an ever-growing log. An
35
+ // operator who needs a persistent audit trail across many runs should
36
+ // retain these externally (commit them, forward to their own SIEM); this
37
+ // module's job is only to make the LAST action's outcome durable and
38
+ // provable, not to be the audit log itself.
39
+
40
+
41
+
42
+ const DELETION_REPORT_FILE = 'deletion-report.json';
43
+ const EXPORT_REPORT_FILE = 'export-report.json';
44
+
45
+ function buildDeletionReport({ mode, dryRun, root, items, preserved }) {
46
+ const list = Array.isArray(items) ? items : [];
47
+ const preservedList = Array.isArray(preserved) ? preserved : [];
48
+ return {
49
+ schema: 'agentic-security/deletion-report@1',
50
+ generatedAt: new Date().toISOString(),
51
+ mode: mode || 'reset',
52
+ dryRun: !!dryRun,
53
+ root: root || null,
54
+ items: list,
55
+ preserved: preservedList,
56
+ summary: {
57
+ planned: list.filter(i => i.status === 'planned').length,
58
+ deleted: list.filter(i => i.status === 'deleted').length,
59
+ failed: list.filter(i => i.status === 'failed').length,
60
+ preserved: preservedList.length,
61
+ },
62
+ };
63
+ }
64
+
65
+ function writeDeletionReport(scanRoot, report) {
66
+ try {
67
+ const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_0__/* .statePath */ .BQ)(scanRoot, DELETION_REPORT_FILE);
68
+ return (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_0__/* .safeWriteState */ .Ep)(fp, JSON.stringify(report, null, 2) + '\n') ? fp : null;
69
+ } catch { return null; }
70
+ }
71
+
72
+ function buildExportReport({ root, outDir, items }) {
73
+ const list = Array.isArray(items) ? items : [];
74
+ return {
75
+ schema: 'agentic-security/export-report@1',
76
+ generatedAt: new Date().toISOString(),
77
+ root: root || null,
78
+ outDir: outDir || null,
79
+ items: list,
80
+ summary: {
81
+ exported: list.filter(i => i.status === 'exported').length,
82
+ failed: list.filter(i => i.status === 'failed').length,
83
+ },
84
+ };
85
+ }
86
+
87
+ function writeExportReport(scanRoot, report) {
88
+ try {
89
+ const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_0__/* .statePath */ .BQ)(scanRoot, EXPORT_REPORT_FILE);
90
+ return (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_0__/* .safeWriteState */ .Ep)(fp, JSON.stringify(report, null, 2) + '\n') ? fp : null;
91
+ } catch { return null; }
92
+ }
93
+
94
+
95
+ /***/ })
96
+
97
+ };
package/dist/637.index.js CHANGED
@@ -11,7 +11,7 @@ export const modules = {
11
11
  /* harmony export */ renderPrDeltaText: () => (/* binding */ renderPrDeltaText)
12
12
  /* harmony export */ });
13
13
  /* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
14
- /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3474);
14
+ /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(7087);
15
15
  // Shadowscan / security-DELTA on PR (v0.72).
16
16
  //
17
17
  // Most SAST PR-comment integrations show absolute counts — "12 findings
@@ -0,0 +1,311 @@
1
+ export const id = 730;
2
+ export const ids = [730,144];
3
+ export const modules = {
4
+
5
+ /***/ 5144:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ addLegalHold: () => (/* binding */ addLegalHold),
10
+ /* harmony export */ isUnderHold: () => (/* binding */ isUnderHold),
11
+ /* harmony export */ listLegalHolds: () => (/* binding */ listLegalHolds),
12
+ /* harmony export */ loadLegalHolds: () => (/* binding */ loadLegalHolds),
13
+ /* harmony export */ removeLegalHold: () => (/* binding */ removeLegalHold)
14
+ /* harmony export */ });
15
+ /* unused harmony export LEGAL_HOLD_FILE */
16
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
17
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(1174);
18
+ /* harmony import */ var _artifact_registry_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(471);
19
+ // FR-707 (assurance-hardening PRD): "Support legal hold and policy-
20
+ // authorized retention exceptions | Legal hold is identity-bound, reasoned,
21
+ // time-bounded where applicable, and auditable."
22
+ //
23
+ // A third instance of the recurring {owner, reason, expires_at} exception
24
+ // shape this codebase already uses twice — `posture/suppressions.js`'s
25
+ // pro-tier exception (scoped to a FINDING) and `posture/compliance-policy.js`'s
26
+ // structured `not-applicable` (scoped to a COMPLIANCE CONTROL). Per D-0025,
27
+ // these are deliberately distinct mechanisms serving different subjects, not
28
+ // one shared module — this file is the third subject: a STATE ARTIFACT.
29
+ //
30
+ // Field naming matches the existing two schemas' snake_case convention
31
+ // (`owner`, `reason`, `expires_at`) rather than inventing a fourth style.
32
+ //
33
+ // identity-bound -> `owner` (required, who placed the hold and is
34
+ // accountable for lifting it)
35
+ // reasoned -> `reason` (required — "we might need this later" is
36
+ // not a reason; same discipline suppressions.js
37
+ // already enforces for its own exceptions)
38
+ // time-bounded
39
+ // where applicable -> `expires_at` is OPTIONAL: a null/absent value is
40
+ // an INDEFINITE hold, which the acceptance
41
+ // criterion's own "where applicable" phrase
42
+ // explicitly allows (a genuine legal matter may have
43
+ // no known end date) — an ISO date value behaves
44
+ // exactly like FR-506/FR-1004's own expiring
45
+ // exceptions: once past, the hold is no longer
46
+ // active and the artifact is exposed to its normal
47
+ // retention TTL again.
48
+ // auditable -> persisted as a single JSON array under
49
+ // `.agentic-security/legal-holds.json` (itself
50
+ // registered as operator-config — an operator/legal
51
+ // team's own input, never scanner-written from scan
52
+ // results), readable via `listLegalHolds`.
53
+ //
54
+ // Consulted from TWO places, not one: `retention-policy.js#findExpiredArtifacts`
55
+ // (defense in depth for any caller reaching it directly) AND `cmdReset`
56
+ // itself for its PLAIN (non-`--expired`) path, which deletes every
57
+ // registered 'generated' artifact unconditionally and would otherwise blow
58
+ // through a hold that only gated TTL expiry.
59
+
60
+
61
+
62
+
63
+
64
+ const LEGAL_HOLD_FILE = 'legal-holds.json';
65
+
66
+ function _loadRaw(scanRoot) {
67
+ let fp;
68
+ try { fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__/* .statePath */ .BQ)(scanRoot, LEGAL_HOLD_FILE); } catch { return []; }
69
+ let raw;
70
+ try { raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8'); } catch { return []; }
71
+ try {
72
+ const doc = JSON.parse(raw);
73
+ return Array.isArray(doc) ? doc : [];
74
+ } catch { return []; }
75
+ }
76
+
77
+ /**
78
+ * All legal holds ever recorded for this project, malformed entries
79
+ * dropped rather than throwing. Never filters by expiry — see
80
+ * `isUnderHold`/`listLegalHolds` for that.
81
+ */
82
+ function loadLegalHolds(scanRoot) {
83
+ return _loadRaw(scanRoot).filter(h => h && typeof h === 'object' && typeof h.artifact === 'string' && typeof h.owner === 'string' && typeof h.reason === 'string');
84
+ }
85
+
86
+ /**
87
+ * Is `artifactName` currently protected by an active (non-expired) hold?
88
+ * Returns the matching hold record, or null. Multiple holds on the same
89
+ * artifact are permitted (e.g. two independent legal matters); the first
90
+ * still-active one found is returned.
91
+ */
92
+ function isUnderHold(artifactName, holds, now = Date.now()) {
93
+ for (const h of holds) {
94
+ if (h.artifact !== artifactName) continue;
95
+ if (!h.expires_at) return h; // indefinite hold — always active
96
+ const t = Date.parse(h.expires_at);
97
+ if (!Number.isFinite(t) || t >= now) return h;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ /**
103
+ * Active (non-expired) holds only, unless `includeExpired`. For
104
+ * `legal-hold list` / auditing.
105
+ */
106
+ function listLegalHolds(scanRoot, { includeExpired = false, now = Date.now() } = {}) {
107
+ const holds = loadLegalHolds(scanRoot);
108
+ if (includeExpired) return holds;
109
+ return holds.filter(h => {
110
+ if (!h.expires_at) return true;
111
+ const t = Date.parse(h.expires_at);
112
+ return !Number.isFinite(t) || t >= now;
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Add a legal hold. Validates identity-bound + reasoned up front (both
118
+ * required, non-empty) and that `expires_at`, if given, is a parseable
119
+ * date in the future — an already-expired hold would be a hold that
120
+ * protects nothing, which is never a legitimate request. `artifact` must
121
+ * name a real registered artifact (artifact-registry.js) — a hold on an
122
+ * unrecognised name can never protect anything and almost always means a
123
+ * typo. Returns `{ok:true, hold}` or `{ok:false, reason}`; never throws.
124
+ */
125
+ function addLegalHold(scanRoot, { artifact, owner, reason, expires_at } = {}) {
126
+ if (!artifact || typeof artifact !== 'string') return { ok: false, reason: '--artifact is required' };
127
+ if (!(0,_artifact_registry_js__WEBPACK_IMPORTED_MODULE_2__/* .isRegisteredArtifact */ .Jl)(artifact)) return { ok: false, reason: `"${artifact}" is not a registered state artifact` };
128
+ if (!owner || typeof owner !== 'string') return { ok: false, reason: '--owner is required (identity-bound)' };
129
+ if (!reason || typeof reason !== 'string') return { ok: false, reason: '--reason is required (reasoned)' };
130
+ if (expires_at) {
131
+ const t = Date.parse(expires_at);
132
+ if (!Number.isFinite(t)) return { ok: false, reason: 'expires_at must be a parseable date' };
133
+ if (t < Date.now()) return { ok: false, reason: 'expires_at is in the past — a hold that already expired protects nothing' };
134
+ }
135
+ const hold = { artifact, owner, reason, expires_at: expires_at || null, created_at: new Date().toISOString() };
136
+ const holds = _loadRaw(scanRoot);
137
+ holds.push(hold);
138
+ const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__/* .statePath */ .BQ)(scanRoot, LEGAL_HOLD_FILE);
139
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__/* .safeWriteState */ .Ep)(fp, JSON.stringify(holds, null, 2) + '\n')) {
140
+ return { ok: false, reason: 'state writes are disabled (--no-state) or this is not a safe state directory' };
141
+ }
142
+ return { ok: true, hold };
143
+ }
144
+
145
+ /**
146
+ * Remove every hold on `artifact` (lifting a hold, not letting it expire).
147
+ * Returns the number removed. A no-op (0) if none existed — never an error.
148
+ */
149
+ function removeLegalHold(scanRoot, artifact) {
150
+ const holds = _loadRaw(scanRoot);
151
+ const remaining = holds.filter(h => !(h && h.artifact === artifact));
152
+ const removedCount = holds.length - remaining.length;
153
+ if (removedCount > 0) {
154
+ const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__/* .statePath */ .BQ)(scanRoot, LEGAL_HOLD_FILE);
155
+ (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__/* .safeWriteState */ .Ep)(fp, JSON.stringify(remaining, null, 2) + '\n');
156
+ }
157
+ return removedCount;
158
+ }
159
+
160
+
161
+ /***/ }),
162
+
163
+ /***/ 6730:
164
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
165
+
166
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
167
+ /* harmony export */ findExpiredArtifacts: () => (/* binding */ findExpiredArtifacts)
168
+ /* harmony export */ });
169
+ /* unused harmony exports RETENTION_DEFAULTS, loadRetentionPolicy, effectiveTtlDays */
170
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
171
+ /* harmony import */ var _util_yaml_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2340);
172
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(1174);
173
+ /* harmony import */ var _artifact_registry_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(471);
174
+ /* harmony import */ var _legal_hold_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(5144);
175
+ // Retention policy: default and maximum TTL by artifact class (assurance-
176
+ // hardening PRD FR-702).
177
+ //
178
+ // "Enforce default and maximum TTL by artifact class | Expired caches,
179
+ // scans, evidence, tickets, and backups are purged or archived according
180
+ // to policy."
181
+ //
182
+ // TWO NUMBERS PER CLASS, ON PURPOSE
183
+ // --------------------------------------------------------------------------
184
+ // `defaultDays` is what applies when nobody configures anything — a
185
+ // reasonable, disclosed starting point, not a regulatory citation (an
186
+ // operator with a real compliance regime should configure their own via
187
+ // the policy file below). `maxDays` is a CEILING an operator's own
188
+ // configuration cannot exceed: the literal "default AND maximum" wording
189
+ // asks for both a floor-free default and a hard cap, not just a knob. An
190
+ // operator who wants indefinite retention for `evidence` for their own
191
+ // audit reasons should say so in their own retention program — this
192
+ // module will not silently allow a TTL past its built-in ceiling for a
193
+ // class, because "TTL" than can be configured to "never" is not a TTL.
194
+ //
195
+ // PURGE, NOT ARCHIVE
196
+ // --------------------------------------------------------------------------
197
+ // The acceptance criterion allows either. This implements PURGE (delete)
198
+ // only — the simpler, safer, and more directly verifiable of the two.
199
+ // "Archive" implies a defined export format and destination, which is
200
+ // FR-706's own separate scope (manifest-based export); an operator who
201
+ // wants to archive before purging already has the tool for it once FR-706
202
+ // exists, or can back up `.agentic-security/` by their own means before
203
+ // running the enforcement command.
204
+ //
205
+ // WHICH ARTIFACTS THIS APPLIES TO
206
+ // --------------------------------------------------------------------------
207
+ // Only 'generated' artifacts carrying a `retentionClass` in
208
+ // artifact-registry.js — deliberately a SUBSET of all generated artifacts
209
+ // (see that module's own header for which ones were left classless and
210
+ // why). An 'operator-config' artifact is NEVER touched by this module,
211
+ // regardless of age, matching FR-703's own "reset preserves operator-
212
+ // authored configuration" precedent.
213
+
214
+
215
+
216
+
217
+
218
+
219
+
220
+ const POLICY_FILE = 'retention-policy.yml';
221
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
222
+
223
+ // Engineering defaults, not a regulatory claim — see the header above.
224
+ const RETENTION_DEFAULTS = {
225
+ cache: { defaultDays: 7, maxDays: 30 },
226
+ scan: { defaultDays: 90, maxDays: 365 },
227
+ evidence: { defaultDays: 365, maxDays: 1095 },
228
+ ticket: { defaultDays: 180, maxDays: 730 },
229
+ backup: { defaultDays: 30, maxDays: 180 },
230
+ };
231
+
232
+ /**
233
+ * Load an operator's `.agentic-security/retention-policy.yml`. Never
234
+ * throws — a missing or malformed file degrades to "no overrides," the
235
+ * same no-op-until-configured convention this repo uses for every other
236
+ * policy surface. Shape:
237
+ * cache: { defaultDays: 3 }
238
+ * evidence: { defaultDays: 730 }
239
+ * A class not mentioned, or a file not present at all, uses
240
+ * RETENTION_DEFAULTS unmodified.
241
+ */
242
+ function loadRetentionPolicy(scanRoot) {
243
+ if (!scanRoot) return null;
244
+ let fp;
245
+ try { fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .statePath */ .BQ)(scanRoot, POLICY_FILE); } catch { return null; }
246
+ let raw;
247
+ try { raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8'); } catch { return null; }
248
+ try {
249
+ const doc = _util_yaml_js__WEBPACK_IMPORTED_MODULE_1__/* .load */ .Hh(raw);
250
+ if (!doc || typeof doc !== 'object') return null;
251
+ return doc;
252
+ } catch { return null; }
253
+ }
254
+
255
+ /**
256
+ * The TTL (in days) actually in effect for a class, after applying any
257
+ * operator override and clamping it to the class's own maxDays — an
258
+ * override CANNOT raise retention past the ceiling, only lower it (or
259
+ * leave it at the default). An unrecognised class (should not happen —
260
+ * only artifact-registry.js's own 5 named classes are ever passed in)
261
+ * degrades to null, meaning "not subject to a TTL."
262
+ */
263
+ function effectiveTtlDays(retentionClass, policy) {
264
+ const bounds = RETENTION_DEFAULTS[retentionClass];
265
+ if (!bounds) return null;
266
+ const override = policy?.[retentionClass]?.defaultDays;
267
+ if (typeof override === 'number' && Number.isFinite(override) && override >= 0) {
268
+ return Math.min(override, bounds.maxDays);
269
+ }
270
+ return bounds.defaultDays;
271
+ }
272
+
273
+ /**
274
+ * Which registered, retention-classed artifacts under `scanRoot`'s state
275
+ * dir are currently past their effective TTL. Never throws — a missing
276
+ * state dir or an artifact that does not exist on disk is simply absent
277
+ * from the result, not an error.
278
+ *
279
+ * @returns {Array<{name: string, isDir: boolean, retentionClass: string,
280
+ * ageDays: number, ttlDays: number}>}
281
+ */
282
+ function findExpiredArtifacts(scanRoot, { now = Date.now() } = {}) {
283
+ const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .stateDir */ .Pn)(scanRoot);
284
+ let dirExists = true;
285
+ try { node_fs__WEBPACK_IMPORTED_MODULE_0__.accessSync(dir); } catch { dirExists = false; }
286
+ if (!dirExists) return [];
287
+ const policy = loadRetentionPolicy(scanRoot);
288
+ // FR-707: an artifact under an active legal hold is NEVER reported as
289
+ // expired, regardless of TTL — checked here (not just in cmdReset) so
290
+ // any future caller of this function inherits the same guarantee.
291
+ const holds = (0,_legal_hold_js__WEBPACK_IMPORTED_MODULE_4__.loadLegalHolds)(scanRoot);
292
+ const expired = [];
293
+ for (const artifact of (0,_artifact_registry_js__WEBPACK_IMPORTED_MODULE_3__/* .listArtifactsWithRetentionClass */ .U_)()) {
294
+ const ttlDays = effectiveTtlDays(artifact.retentionClass, policy);
295
+ if (ttlDays === null) continue;
296
+ if ((0,_legal_hold_js__WEBPACK_IMPORTED_MODULE_4__.isUnderHold)(artifact.name, holds, now)) continue;
297
+ const p = `${dir}/${artifact.name}`;
298
+ let st;
299
+ try { st = node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(p); } catch { continue; } // not present — nothing to expire
300
+ const ageDays = (now - st.mtimeMs) / MS_PER_DAY;
301
+ if (ageDays > ttlDays) {
302
+ expired.push({ name: artifact.name, isDir: st.isDirectory(), retentionClass: artifact.retentionClass, ageDays, ttlDays });
303
+ }
304
+ }
305
+ return expired;
306
+ }
307
+
308
+
309
+ /***/ })
310
+
311
+ };