@clear-capabilities/agentic-security-scanner 0.172.0 → 0.173.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 +29 -0
- package/bin/agentic-security.js +37 -0
- package/dist/119.index.js +197 -0
- package/dist/128.index.js +129 -0
- package/dist/1293.index.js +5689 -0
- package/dist/1336.index.js +101 -101
- package/dist/1351.index.js +133 -0
- package/dist/1387.index.js +271 -0
- package/dist/1437.index.js +23 -0
- package/dist/1445.index.js +110 -0
- package/dist/1505.index.js +151 -0
- package/dist/1557.index.js +293 -0
- package/dist/1621.index.js +311 -0
- package/dist/1751.index.js +160 -0
- package/dist/1765.index.js +727 -0
- package/dist/1861.index.js +690 -0
- package/dist/1898.index.js +116 -0
- package/dist/1906.index.js +433 -0
- package/dist/1920.index.js +1 -291
- package/dist/208.index.js +1631 -0
- package/dist/2199.index.js +0 -239
- package/dist/2223.index.js +159 -0
- package/dist/2530.index.js +152 -0
- package/dist/2565.index.js +116 -0
- package/dist/2574.index.js +127 -0
- package/dist/2620.index.js +345 -0
- package/dist/2773.index.js +98 -0
- package/dist/2775.index.js +32 -0
- package/dist/2966.index.js +171 -0
- package/dist/2972.index.js +87 -0
- package/dist/2991.index.js +131 -0
- package/dist/3063.index.js +833 -0
- package/dist/3075.index.js +227 -0
- package/dist/3121.index.js +297 -0
- package/dist/3314.index.js +263 -0
- package/dist/3480.index.js +1822 -0
- package/dist/3481.index.js +24 -0
- package/dist/3506.index.js +268 -0
- package/dist/3533.index.js +591 -0
- package/dist/3653.index.js +450 -0
- package/dist/3822.index.js +131 -0
- package/dist/3868.index.js +292 -0
- package/dist/3917.index.js +152 -0
- package/dist/3918.index.js +192 -0
- package/dist/4090.index.js +288 -0
- package/dist/4121.index.js +1190 -0
- package/dist/4142.index.js +0 -239
- package/dist/4336.index.js +436 -0
- package/dist/4387.index.js +532 -0
- package/dist/4413.index.js +302 -0
- package/dist/4476.index.js +4510 -0
- package/dist/4491.index.js +133 -0
- package/dist/4538.index.js +185 -0
- package/dist/4684.index.js +319 -0
- package/dist/4740.index.js +165 -0
- package/dist/4780.index.js +209 -0
- package/dist/4802.index.js +225 -0
- package/dist/4812.index.js +379 -0
- package/dist/4835.index.js +126 -0
- package/dist/494.index.js +226 -0
- package/dist/4991.index.js +549 -0
- package/dist/5199.index.js +305 -0
- package/dist/5216.index.js +109 -0
- package/dist/5439.index.js +301 -0
- package/dist/5443.index.js +97 -0
- package/dist/5496.index.js +377 -0
- package/dist/550.index.js +2 -1
- package/dist/5801.index.js +588 -0
- package/dist/5858.index.js +223 -0
- package/dist/5933.index.js +143 -0
- package/dist/601.index.js +23 -0
- package/dist/6173.index.js +273 -0
- package/dist/6291.index.js +1871 -0
- package/dist/6312.index.js +666 -0
- package/dist/634.index.js +16182 -0
- package/dist/644.index.js +87 -0
- package/dist/6508.index.js +283 -0
- package/dist/6560.index.js +645 -0
- package/dist/6576.index.js +422 -0
- package/dist/6579.index.js +271 -0
- package/dist/6745.index.js +305 -0
- package/dist/6818.index.js +21642 -0
- package/dist/6972.index.js +167 -0
- package/dist/6995.index.js +163 -0
- package/dist/7249.index.js +191 -0
- package/dist/7372.index.js +1197 -0
- package/dist/7405.index.js +23 -6
- package/dist/7546.index.js +541 -0
- package/dist/76.index.js +448 -0
- package/dist/7879.index.js +163 -0
- package/dist/8025.index.js +130 -0
- package/dist/8054.index.js +248 -0
- package/dist/8097.index.js +201 -0
- package/dist/8126.index.js +191 -0
- package/dist/8178.index.js +78 -0
- package/dist/818.index.js +212 -0
- package/dist/8382.index.js +93 -0
- package/dist/8408.index.js +261 -0
- package/dist/8540.index.js +174 -0
- package/dist/8635.index.js +802 -0
- package/dist/8688.index.js +410 -0
- package/dist/8699.index.js +779 -0
- package/dist/8891.index.js +457 -0
- package/dist/8916.index.js +885 -0
- package/dist/9146.index.js +170 -0
- package/dist/9168.index.js +841 -0
- package/dist/9292.index.js +369 -0
- package/dist/9382.index.js +338 -48
- package/dist/9422.index.js +866 -0
- package/dist/9440.index.js +453 -0
- package/dist/9479.index.js +152 -0
- package/dist/9481.index.js +220 -0
- package/dist/9539.index.js +100 -0
- package/dist/9547.index.js +103 -0
- package/dist/9560.index.js +41 -61
- package/dist/9692.index.js +90 -0
- package/dist/9800.index.js +213 -1
- package/dist/9899.index.js +138 -0
- package/dist/9981.index.js +206 -0
- package/dist/999.index.js +117 -0
- package/dist/agentic-security.mjs +28 -28
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +31 -6
- package/src/context-intelligence/CLAUDE.md +4 -0
- package/src/context-intelligence/adapters/haskell-hie.js +28 -1
- package/src/context-intelligence/candidates.js +2 -1
- package/src/context-intelligence/canonical.js +25 -0
- package/src/context-intelligence/capabilities.js +2 -1
- package/src/context-intelligence/cli.js +16 -10
- package/src/context-intelligence/config.js +4 -1
- package/src/context-intelligence/controls.js +108 -10
- package/src/context-intelligence/counterexample.js +200 -0
- package/src/context-intelligence/cross-route.js +111 -0
- package/src/context-intelligence/entrypoints.js +2 -2
- package/src/context-intelligence/evaluation/index.js +11 -4
- package/src/context-intelligence/evaluation/scripted-reader.js +12 -2
- package/src/context-intelligence/findings.js +166 -0
- package/src/context-intelligence/invariants.js +197 -0
- package/src/context-intelligence/queue.js +6 -1
- package/src/context-intelligence/reachability.js +10 -4
- package/src/context-intelligence/review.js +129 -16
- package/src/dataflow/access-paths.js +11 -0
- package/src/dataflow/analysis-limits.js +66 -0
- package/src/dataflow/engine.js +272 -20
- package/src/dataflow/service-edges.js +174 -0
- package/src/dataflow/summaries.js +7 -1
- package/src/discovery/judge.js +2 -1
- package/src/engine.js +82 -14
- package/src/fix/apply-fix-service.js +55 -8
- package/src/fix/batch-journal.js +84 -0
- package/src/fix/exploit-replay.js +51 -0
- package/src/fix/footprint.js +65 -0
- package/src/fix/patch-policy.js +49 -0
- package/src/fix/secret-persistence.js +39 -0
- package/src/fix/test-commands.js +84 -0
- package/src/fix/verification-adapters.js +97 -0
- package/src/fix/verification-record.js +243 -0
- package/src/fix/verify-patch.js +89 -0
- package/src/ir/CLAUDE.md +5 -0
- package/src/ir/callgraph.js +23 -1
- package/src/ir/module-resolver.js +315 -0
- package/src/ir/parser-js.js +21 -2
- package/src/ir/semantic-graph.js +245 -0
- package/src/lineage/build-attested-correlation.js +63 -0
- package/src/lineage/deployment-identity.js +76 -0
- package/src/mcp/tools.js +13 -34
- package/src/pipeline/canonical-output.js +64 -0
- package/src/pipeline/detector-runner.js +7 -0
- package/src/pipeline/scan-health.js +14 -0
- package/src/pipeline/supervised-analyzers.js +125 -0
- package/src/pipeline/supervised-child.js +17 -0
- package/src/pipeline/telemetry.js +79 -0
- package/src/posture/accuracy-scorecard.js +98 -9
- package/src/posture/artifact-registry.js +5 -0
- package/src/posture/assurance/evidence-registry.js +215 -0
- package/src/posture/assurance/release-package-verify.js +135 -0
- package/src/posture/assurance/release-package.js +118 -0
- package/src/posture/assurance/release-verdict.js +94 -0
- package/src/posture/assurance/waiver.js +200 -0
- package/src/posture/compliance-truth.js +100 -0
- package/src/posture/evaluation/scan-child.js +3 -1
- package/src/posture/evaluation/why-missed.js +3 -1
- package/src/posture/exploitability-evidence.js +141 -0
- package/src/posture/fix-verify.js +4 -2
- package/src/posture/fleet/exposure-ranking.js +115 -0
- package/src/posture/fleet/service-chain-graph.js +150 -0
- package/src/posture/graph-reconciliation.js +154 -0
- package/src/posture/provenance/coverage.js +103 -0
- package/src/posture/provenance/git-evidence.js +84 -0
- package/src/posture/provenance/origin-resolver.js +7 -1
- package/src/posture/provenance/predicate-replay.js +14 -4
- package/src/posture/runtime-correlation.js +61 -15
- package/src/posture/runtime-explanation.js +116 -0
- package/src/posture/runtime-trace-ingest.js +194 -0
- package/src/posture/scan-checkpoint.js +38 -10
- package/src/posture/waf-ingest.js +7 -2
- package/src/report/index.js +4 -0
- package/src/report/oscal.js +38 -0
- package/src/report/perspectives.js +150 -0
- package/src/sast/_comment-strip.js +8 -1
- package/src/sast/ruby.js +26 -0
- package/src/util/string-memo.js +38 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
export const id = 1557;
|
|
2
|
+
export const ids = [1557];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 51557:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ analyzeTranscript: () => (/* binding */ analyzeTranscript),
|
|
10
|
+
/* harmony export */ formatCacheReport: () => (/* binding */ formatCacheReport),
|
|
11
|
+
/* harmony export */ renderCacheStatusLine: () => (/* binding */ renderCacheStatusLine)
|
|
12
|
+
/* harmony export */ });
|
|
13
|
+
/* unused harmony export _internal */
|
|
14
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
|
|
15
|
+
/* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(48161);
|
|
16
|
+
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
|
|
17
|
+
// Prompt-cache economics — turn Claude Code's own transcript usage into a
|
|
18
|
+
// dollarized report: how much prompt caching saved, how much was wasted on
|
|
19
|
+
// avoidable cache misses, and what invalidated the cache.
|
|
20
|
+
//
|
|
21
|
+
// Source of truth: the Claude Code transcript at
|
|
22
|
+
// ~/.claude/projects/<enc>/<session>.jsonl
|
|
23
|
+
// where <enc> is CLAUDE_PROJECT_DIR with `/` and `.` replaced by `-`. Each
|
|
24
|
+
// assistant turn carries `message.usage` with input/output/cache_read/
|
|
25
|
+
// cache_creation token counts (and a 5m/1h write split). We price those against
|
|
26
|
+
// per-model rates to compute real economics — no estimates, no network.
|
|
27
|
+
//
|
|
28
|
+
// Pure compute on parsed records; only `locateTranscript`/`parseTranscriptUsage`
|
|
29
|
+
// touch the filesystem. ESM (scanner tree). A trimmed CJS twin lives at
|
|
30
|
+
// hooks/lib/transcript.js for the CJS hooks; test/cache-economics.test.js asserts
|
|
31
|
+
// the two agree.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
// Cents-scale money formatter (fmtUsd in risk-dollars.js targets five-figure
|
|
37
|
+
// breach costs and won't round sub-dollar values).
|
|
38
|
+
function money(n) {
|
|
39
|
+
const v = Number(n) || 0;
|
|
40
|
+
return Math.abs(v) >= 1 ? `$${v.toFixed(2)}` : `$${v.toFixed(4)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Per-1M-token rates (input / output). Mirror hooks/model-cost-advisor.js MODELS.
|
|
44
|
+
const MODEL_RATES = {
|
|
45
|
+
fable: { label: 'Fable 5', in: 10, out: 50 },
|
|
46
|
+
opus: { label: 'Opus 4.8', in: 5, out: 25 },
|
|
47
|
+
sonnet5: { label: 'Sonnet 5', in: 3, out: 15 },
|
|
48
|
+
sonnet: { label: 'Sonnet 4.6', in: 3, out: 15 },
|
|
49
|
+
haiku: { label: 'Haiku 4.5', in: 1, out: 5 },
|
|
50
|
+
};
|
|
51
|
+
const CACHE_READ_MULT = 0.1; // cache read ≈ 0.1× input
|
|
52
|
+
const CACHE_WRITE_MULT = 1.25; // 5-minute cache write ≈ 1.25× input
|
|
53
|
+
const CACHE_WRITE_1H_MULT = 2.0; // 1-hour cache write ≈ 2× input
|
|
54
|
+
const TTL_MS = 5 * 60 * 1000;
|
|
55
|
+
|
|
56
|
+
// Map any model string to a rate family. Returns null for unpriceable models
|
|
57
|
+
// (e.g. "<synthetic>" sidechain/compaction turns) so they're skipped.
|
|
58
|
+
function rateFor(model) {
|
|
59
|
+
if (typeof model !== 'string') return null;
|
|
60
|
+
const s = model.toLowerCase();
|
|
61
|
+
if (s.includes('fable') || s.includes('mythos')) return MODEL_RATES.fable;
|
|
62
|
+
if (s.includes('haiku')) return MODEL_RATES.haiku;
|
|
63
|
+
if (s.includes('sonnet')) return (s.includes('sonnet-5') || s.includes('sonnet 5')) ? MODEL_RATES.sonnet5 : MODEL_RATES.sonnet;
|
|
64
|
+
if (s.includes('opus')) return MODEL_RATES.opus;
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Transcript discovery + parse ─────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
function encodeProjectDir(dir) {
|
|
71
|
+
return String(dir).replace(/[/.]/g, '-');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Locate the session transcript. Prefer an explicit (hook-provided) path; else
|
|
75
|
+
// derive the project's transcript dir and take the most-recently-modified jsonl.
|
|
76
|
+
function locateTranscript({ transcriptPath, projectDir } = {}) {
|
|
77
|
+
try {
|
|
78
|
+
if (transcriptPath && node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(transcriptPath)) return transcriptPath;
|
|
79
|
+
} catch { /* fall through */ }
|
|
80
|
+
try {
|
|
81
|
+
const dir = node_path__WEBPACK_IMPORTED_MODULE_2__.join(node_os__WEBPACK_IMPORTED_MODULE_1__.homedir(), '.claude', 'projects', encodeProjectDir(projectDir || process.cwd()));
|
|
82
|
+
if (!node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(dir)) return null;
|
|
83
|
+
const files = node_fs__WEBPACK_IMPORTED_MODULE_0__.readdirSync(dir)
|
|
84
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
85
|
+
.map(f => ({ f: node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, f), m: node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, f)).mtimeMs }))
|
|
86
|
+
.sort((a, b) => b.m - a.m);
|
|
87
|
+
return files.length ? files[0].f : null;
|
|
88
|
+
} catch { return null; }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Parse a transcript jsonl into per-assistant-turn usage records. Skips lines
|
|
92
|
+
// that aren't priceable assistant turns.
|
|
93
|
+
function parseTranscriptUsage(jsonlPath) {
|
|
94
|
+
let raw;
|
|
95
|
+
try { raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(jsonlPath, 'utf8'); } catch { return []; }
|
|
96
|
+
const records = [];
|
|
97
|
+
for (const line of raw.split('\n')) {
|
|
98
|
+
const t = line.trim();
|
|
99
|
+
if (!t) continue;
|
|
100
|
+
let o;
|
|
101
|
+
try { o = JSON.parse(t); } catch { continue; }
|
|
102
|
+
if (o.type !== 'assistant') continue;
|
|
103
|
+
const msg = o.message;
|
|
104
|
+
const u = msg && msg.usage;
|
|
105
|
+
if (!u || !msg.model || !rateFor(msg.model)) continue;
|
|
106
|
+
const cc = u.cache_creation || {};
|
|
107
|
+
records.push({
|
|
108
|
+
model: msg.model,
|
|
109
|
+
input: u.input_tokens || 0,
|
|
110
|
+
output: u.output_tokens || 0,
|
|
111
|
+
cacheRead: u.cache_read_input_tokens || 0,
|
|
112
|
+
cacheCreate: u.cache_creation_input_tokens || 0,
|
|
113
|
+
cacheCreate5m: cc.ephemeral_5m_input_tokens || 0,
|
|
114
|
+
cacheCreate1h: cc.ephemeral_1h_input_tokens || 0,
|
|
115
|
+
ts: o.timestamp ? Date.parse(o.timestamp) : null,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return records;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Pure economics ───────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
function writeCostUsd(r, inRate) {
|
|
124
|
+
const m5 = r.cacheCreate5m || 0, m1 = r.cacheCreate1h || 0;
|
|
125
|
+
if (m5 + m1 > 0) return (m5 * CACHE_WRITE_MULT + m1 * CACHE_WRITE_1H_MULT) * inRate;
|
|
126
|
+
return (r.cacheCreate || 0) * CACHE_WRITE_MULT * inRate; // breakdown absent
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Aggregate economics over parsed records.
|
|
130
|
+
function computeCacheEconomics(records) {
|
|
131
|
+
let turns = 0, inTok = 0, outTok = 0, cacheRead = 0, cacheCreate = 0;
|
|
132
|
+
let actualUsd = 0, uncachedUsd = 0, writePremiumUsd = 0;
|
|
133
|
+
const perModel = {};
|
|
134
|
+
|
|
135
|
+
for (const r of records) {
|
|
136
|
+
const rate = rateFor(r.model);
|
|
137
|
+
if (!rate) continue;
|
|
138
|
+
turns++;
|
|
139
|
+
const inRate = rate.in / 1e6, outRate = rate.out / 1e6;
|
|
140
|
+
|
|
141
|
+
const readCost = r.cacheRead * inRate * CACHE_READ_MULT;
|
|
142
|
+
const writeCost = writeCostUsd(r, inRate);
|
|
143
|
+
const inCost = r.input * inRate;
|
|
144
|
+
const outCost = r.output * outRate;
|
|
145
|
+
const turnActual = readCost + writeCost + inCost + outCost;
|
|
146
|
+
// What this turn would have cost with NO caching: every input-side token full price.
|
|
147
|
+
const turnUncached = (r.cacheRead + r.cacheCreate + r.input) * inRate + outCost;
|
|
148
|
+
|
|
149
|
+
actualUsd += turnActual;
|
|
150
|
+
uncachedUsd += turnUncached;
|
|
151
|
+
writePremiumUsd += writeCost - (r.cacheCreate * inRate); // the >1× premium paid to cache
|
|
152
|
+
|
|
153
|
+
inTok += r.input; outTok += r.output; cacheRead += r.cacheRead; cacheCreate += r.cacheCreate;
|
|
154
|
+
|
|
155
|
+
const key = rate.label;
|
|
156
|
+
const pm = perModel[key] || (perModel[key] = { turns: 0, actualUsd: 0, cacheRead: 0, inputSide: 0 });
|
|
157
|
+
pm.turns++; pm.actualUsd += turnActual; pm.cacheRead += r.cacheRead;
|
|
158
|
+
pm.inputSide += r.cacheRead + r.cacheCreate + r.input;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const inputSide = cacheRead + cacheCreate + inTok;
|
|
162
|
+
return {
|
|
163
|
+
turns,
|
|
164
|
+
tokens: { input: inTok, output: outTok, cacheRead, cacheCreate },
|
|
165
|
+
actualUsd,
|
|
166
|
+
uncachedUsd,
|
|
167
|
+
savedUsd: uncachedUsd - actualUsd, // net $ caching saved (can dip negative early)
|
|
168
|
+
writePremiumUsd, // $ invested establishing caches
|
|
169
|
+
cacheHitRatio: inputSide ? cacheRead / inputSide : 0,
|
|
170
|
+
costPerTurnUsd: turns ? actualUsd / turns : 0,
|
|
171
|
+
perModel,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Attribute cache drops: a turn that re-ingests a large prefix cold after a warm
|
|
176
|
+
// prior turn. Cause = model-switch | cache-expired | prefix-change.
|
|
177
|
+
function detectInvalidators(records) {
|
|
178
|
+
const leaks = [];
|
|
179
|
+
const MIN_WARM = 2000;
|
|
180
|
+
for (let i = 1; i < records.length; i++) {
|
|
181
|
+
const prev = records[i - 1], cur = records[i];
|
|
182
|
+
const prevWarm = prev.cacheRead + prev.input + prev.cacheCreate;
|
|
183
|
+
if (prevWarm < MIN_WARM) continue;
|
|
184
|
+
const curFresh = cur.input + cur.cacheCreate;
|
|
185
|
+
const coldish = cur.cacheRead < prevWarm * 0.25 && curFresh > prevWarm * 0.5;
|
|
186
|
+
if (!coldish) continue;
|
|
187
|
+
|
|
188
|
+
let cause;
|
|
189
|
+
if (cur.model !== prev.model) cause = 'model-switch';
|
|
190
|
+
else if (cur.ts && prev.ts && (cur.ts - prev.ts) > TTL_MS) cause = 'cache-expired';
|
|
191
|
+
else cause = 'prefix-change';
|
|
192
|
+
|
|
193
|
+
const rate = rateFor(cur.model);
|
|
194
|
+
const inRate = rate ? rate.in / 1e6 : 0;
|
|
195
|
+
// Extra paid vs. having kept the prefix as a cheap cache read.
|
|
196
|
+
const wastedUsd = prevWarm * inRate * (1 - CACHE_READ_MULT);
|
|
197
|
+
leaks.push({ turn: i, cause, wastedUsd, model: cur.model });
|
|
198
|
+
}
|
|
199
|
+
return leaks;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Convenience: locate → parse → compute → detect. Returns { ok:false } when no
|
|
203
|
+
// transcript is available.
|
|
204
|
+
function analyzeTranscript(opts = {}) {
|
|
205
|
+
const transcript = locateTranscript(opts);
|
|
206
|
+
if (!transcript) return { ok: false, reason: 'no-transcript' };
|
|
207
|
+
const records = parseTranscriptUsage(transcript);
|
|
208
|
+
if (!records.length) return { ok: false, reason: 'no-priceable-turns', transcript };
|
|
209
|
+
return {
|
|
210
|
+
ok: true,
|
|
211
|
+
transcript,
|
|
212
|
+
metrics: computeCacheEconomics(records),
|
|
213
|
+
leaks: detectInvalidators(records),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Report formatting ────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
const CAUSE_LABEL = {
|
|
220
|
+
'model-switch': 'model switch (cache is model-scoped)',
|
|
221
|
+
'cache-expired': 'cache expired (gap > 5-min TTL)',
|
|
222
|
+
'prefix-change': 'prefix changed (system prompt / tools / context edit)',
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// F6 — one-line HUD for a Claude Code statusLine command (mirrors
|
|
226
|
+
// watch-mode.js renderStatusLine). Takes the metrics from computeCacheEconomics.
|
|
227
|
+
function renderCacheStatusLine(metrics) {
|
|
228
|
+
if (!metrics || !metrics.turns) return 'agentic-security: no session cost yet';
|
|
229
|
+
const hit = Math.round(metrics.cacheHitRatio * 100);
|
|
230
|
+
return `agentic-security: ${money(metrics.actualUsd)} · ${hit}% cached · ${money(metrics.costPerTurnUsd)}/turn`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function formatCacheReport(result) {
|
|
234
|
+
if (!result.ok) {
|
|
235
|
+
return result.reason === 'no-transcript'
|
|
236
|
+
? 'agentic-security: no Claude Code transcript found for this project yet.'
|
|
237
|
+
: 'agentic-security: transcript has no priceable model turns yet.';
|
|
238
|
+
}
|
|
239
|
+
const m = result.metrics;
|
|
240
|
+
const lines = [];
|
|
241
|
+
lines.push('');
|
|
242
|
+
lines.push(' Prompt-cache economics — this session');
|
|
243
|
+
lines.push(` ${result.turns ?? m.turns} model turns\n`);
|
|
244
|
+
lines.push(` cache hit ratio ${(m.cacheHitRatio * 100).toFixed(1)}% (input-side tokens served from cache)`);
|
|
245
|
+
lines.push(` spent ${money(m.actualUsd)} (~${money(m.costPerTurnUsd)}/turn)`);
|
|
246
|
+
lines.push(` ▶ saved by caching ${money(m.savedUsd)} vs. ${money(m.uncachedUsd)} with no cache`);
|
|
247
|
+
lines.push(` invested in caches ${money(m.writePremiumUsd)} (write premium over base input)`);
|
|
248
|
+
lines.push('');
|
|
249
|
+
lines.push(' tokens: '
|
|
250
|
+
+ `${m.tokens.cacheRead.toLocaleString()} cached-read · `
|
|
251
|
+
+ `${m.tokens.cacheCreate.toLocaleString()} cache-write · `
|
|
252
|
+
+ `${m.tokens.input.toLocaleString()} fresh-in · `
|
|
253
|
+
+ `${m.tokens.output.toLocaleString()} out`);
|
|
254
|
+
|
|
255
|
+
const models = Object.keys(m.perModel);
|
|
256
|
+
if (models.length > 1) {
|
|
257
|
+
lines.push('\n by model:');
|
|
258
|
+
for (const k of models.sort()) {
|
|
259
|
+
const pm = m.perModel[k];
|
|
260
|
+
const hr = pm.inputSide ? (pm.cacheRead / pm.inputSide * 100).toFixed(0) : '0';
|
|
261
|
+
lines.push(` ${k.padEnd(12)} ${pm.turns} turns · ${money(pm.actualUsd)} · ${hr}% cached`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (result.leaks && result.leaks.length) {
|
|
266
|
+
const wasted = result.leaks.reduce((s, l) => s + l.wastedUsd, 0);
|
|
267
|
+
lines.push(`\n ⚠ cache leaks (${result.leaks.length}, ~${money(wasted)} wasted re-ingesting context):`);
|
|
268
|
+
const byCause = {};
|
|
269
|
+
for (const l of result.leaks) {
|
|
270
|
+
(byCause[l.cause] || (byCause[l.cause] = { n: 0, usd: 0 })).n++;
|
|
271
|
+
byCause[l.cause].usd += l.wastedUsd;
|
|
272
|
+
}
|
|
273
|
+
for (const c of Object.keys(byCause).sort()) {
|
|
274
|
+
lines.push(` · ${byCause[c].n}× ${CAUSE_LABEL[c] || c} — ~${money(byCause[c].usd)}`);
|
|
275
|
+
}
|
|
276
|
+
lines.push(' Keep one model + a stable system prompt within a working window to avoid these.');
|
|
277
|
+
} else {
|
|
278
|
+
lines.push('\n ✓ no cache leaks detected — your context stayed warm.');
|
|
279
|
+
}
|
|
280
|
+
lines.push('');
|
|
281
|
+
return lines.join('\n');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Test surface (underscore export is exempt from the dead-module gate).
|
|
285
|
+
const _internal = {
|
|
286
|
+
MODEL_RATES, CACHE_READ_MULT, CACHE_WRITE_MULT, CACHE_WRITE_1H_MULT,
|
|
287
|
+
rateFor, locateTranscript, parseTranscriptUsage, computeCacheEconomics, detectInvalidators,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
/***/ })
|
|
292
|
+
|
|
293
|
+
};
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
export const id = 1621;
|
|
2
|
+
export const ids = [1621,6995];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 66995:
|
|
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__(73024);
|
|
17
|
+
/* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(69259);
|
|
18
|
+
/* harmony import */ var _artifact_registry_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(81734);
|
|
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)(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)(scanRoot, LEGAL_HOLD_FILE);
|
|
139
|
+
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.safeWriteState)(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)(scanRoot, LEGAL_HOLD_FILE);
|
|
155
|
+
(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.safeWriteState)(fp, JSON.stringify(remaining, null, 2) + '\n');
|
|
156
|
+
}
|
|
157
|
+
return removedCount;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
/***/ }),
|
|
162
|
+
|
|
163
|
+
/***/ 11621:
|
|
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__(73024);
|
|
171
|
+
/* harmony import */ var _util_yaml_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(32469);
|
|
172
|
+
/* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(69259);
|
|
173
|
+
/* harmony import */ var _artifact_registry_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(81734);
|
|
174
|
+
/* harmony import */ var _legal_hold_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(66995);
|
|
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)(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
|
+
};
|