@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,1471 @@
1
+ export const id = 5616;
2
+ export const ids = [5616,2199,932,8551];
3
+ export const modules = {
4
+
5
+ /***/ 45616:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ // ESM COMPAT FLAG
9
+ __webpack_require__.r(__webpack_exports__);
10
+
11
+ // EXPORTS
12
+ __webpack_require__.d(__webpack_exports__, {
13
+ cmdDnr: () => (/* binding */ cmdDnr)
14
+ });
15
+
16
+ // EXTERNAL MODULE: external "node:fs"
17
+ var external_node_fs_ = __webpack_require__(73024);
18
+ // EXTERNAL MODULE: external "node:path"
19
+ var external_node_path_ = __webpack_require__(76760);
20
+ // EXTERNAL MODULE: ./src/posture/proof-fabric/store.js
21
+ var store = __webpack_require__(62199);
22
+ // EXTERNAL MODULE: ./src/posture/proof-fabric/reproducer.js
23
+ var reproducer = __webpack_require__(37055);
24
+ // EXTERNAL MODULE: ./src/posture/proof-fabric/states.js
25
+ var states = __webpack_require__(12090);
26
+ // EXTERNAL MODULE: ./src/posture/dnr/payload-classes.js
27
+ var payload_classes = __webpack_require__(62561);
28
+ // EXTERNAL MODULE: ./src/posture/dnr/attack-facts.js
29
+ var attack_facts = __webpack_require__(79134);
30
+ ;// CONCATENATED MODULE: ./src/posture/dnr/detection-ir.js
31
+ // Detection intermediate representation (PRD 7, requirement 1). One IR per confirmed finding; every rendered rule, every
32
+ // hunt and every validation is derived from it, so there is exactly one place where the claim "what the exploit looks like"
33
+ // is made.
34
+ //
35
+ // Evidence grounding (requirement 3) is enforced structurally:
36
+ // * the IR is CLOSED-WORLD: `validateDetectionIR` refuses any key it does not list, and none of them can carry an attacker
37
+ // address, a domain, a file hash or a user agent;
38
+ // * every fact that came from somewhere records where (`facts[field] = {source, ref}`), and a fact with no source is not
39
+ // put in the IR at all (it is `null`, and the limitation is stated);
40
+ // * the confidence is computed from the evidence state, never asserted.
41
+
42
+
43
+
44
+
45
+
46
+ const DETECTION_IR_SCHEMA = 'agentic-security/detection-ir';
47
+ const DETECTION_IR_VERSION = 1;
48
+
49
+ const IR_KEYS = Object.freeze(['schema', 'version', 'id', 'findingId', 'cwe', 'family', 'attackBehavior', 'endpoint', 'parameter', 'payloadClass', 'affectedOperation', 'processEffect', 'networkEffect', 'dbEffect', 'confidence', 'grounding', 'evidence', 'facts', 'limitations', 'digest']);
50
+ const GROUNDING = Object.freeze(['exploit-confirmed', 'reproduced', 'static-only']);
51
+
52
+ /** "POST /api/export" -> {method, path}; anything else -> null. A finding's own route text is a fact source like any other. */
53
+ function parseRouteText(text) {
54
+ const m = /^\s*(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(\/[A-Za-z0-9/_\-.:{}~%]{0,199})\s*$/.exec(String(text ?? ''));
55
+ return m ? { method: m[1], path: m[2] } : null;
56
+ }
57
+
58
+ function groundingFor(proof) {
59
+ const state = proof?.state ?? null;
60
+ if (state && (0,states/* stateRank */.g3)(state) >= (0,states/* stateRank */.g3)('EXPLOIT_CONFIRMED')) return 'exploit-confirmed';
61
+ if (state && (0,states/* stateRank */.g3)(state) >= (0,states/* stateRank */.g3)('REPRODUCED')) return 'reproduced';
62
+ return 'static-only';
63
+ }
64
+
65
+ /**
66
+ * Build the IR for one finding. Returns {ok:true, ir} or {ok:false, reason}.
67
+ * A finding with no proof is refused unless `allowUnproven` is set, in which case the IR says so (`grounding: static-only`)
68
+ * and carries a lower confidence. A finding with no recognisable payload class is refused: there is nothing to detect.
69
+ */
70
+ function buildDetectionIR({ finding, proof = null, allowUnproven = false } = {}) {
71
+ if (!finding || typeof finding !== 'object') return { ok: false, reason: 'no finding' };
72
+ const findingId = String(finding.stableId ?? finding.id ?? '');
73
+ if (!findingId) return { ok: false, reason: 'the finding has no stable id' };
74
+ const grounding = groundingFor(proof);
75
+ if (grounding === 'static-only' && !allowUnproven) {
76
+ return { ok: false, reason: `no exploit proof for ${findingId}: prove it first (triage exploit ${findingId} --prove), or pass --allow-unproven to build a lower-confidence detection from the static finding alone` };
77
+ }
78
+ const attack = proof?.reproducer?.attack ?? null;
79
+ const attackErrs = (0,attack_facts/* validateAttackFacts */.GT)(attack);
80
+ if (attackErrs.length) return { ok: false, reason: `the reproducer's attack facts are invalid: ${attackErrs.join('; ')}` };
81
+
82
+ const facts = {};
83
+ const evidence = [];
84
+ const limitations = [];
85
+ if (finding.file) evidence.push({ kind: 'finding', ref: `${finding.file}:${finding.line ?? '?'}`, id: findingId });
86
+ if (proof?.recordDigest) evidence.push({ kind: 'proof', ref: `proof:${proof.recordDigest}`, state: proof.state ?? null, oracle: proof.oracle ?? null });
87
+ if (attack) evidence.push({ kind: 'reproducer', ref: `reproducer:${String(proof?.reproducer ? (0,reproducer/* sha256 */.sc)((0,reproducer/* canonicalize */.dw)(proof.reproducer)) : '').slice(0, 16)}` });
88
+
89
+ // payload class: the reproducer's explicit statement wins, then the finding's CWE
90
+ let payloadClass = null;
91
+ if (attack?.payloadClass) { payloadClass = attack.payloadClass; facts.payloadClass = { source: 'reproducer', ref: 'attack.payloadClass' }; }
92
+ else if ((0,payload_classes/* classForCwe */.T6)(finding.cwe)) { payloadClass = (0,payload_classes/* classForCwe */.T6)(finding.cwe); facts.payloadClass = { source: 'finding', ref: `cwe:${finding.cwe}` }; }
93
+ if (!payloadClass) return { ok: false, reason: `no payload class describes ${finding.cwe || 'this finding'}; a detection would have to guess, and a guessed detection is not generated` };
94
+ const def = payload_classes/* PAYLOAD_CLASSES */.oE[payloadClass];
95
+
96
+ // endpoint: the reproducer's facts, else the finding's own route text
97
+ let endpoint = null;
98
+ if (attack?.method && attack?.path) { endpoint = { method: attack.method, path: attack.path }; facts.endpoint = { source: 'reproducer', ref: 'attack.method+attack.path' }; }
99
+ else {
100
+ const fromFinding = parseRouteText(finding.route ?? finding.endpoint);
101
+ if (fromFinding) { endpoint = fromFinding; facts.endpoint = { source: 'finding', ref: finding.route ? 'route' : 'endpoint' }; }
102
+ }
103
+ if (!endpoint) limitations.push('no endpoint is known for this finding, so no request-level rule is generated; only the payload class is described');
104
+
105
+ let parameter = null;
106
+ if (attack?.parameter) { parameter = { name: attack.parameter, location: attack.location }; facts.parameter = { source: 'reproducer', ref: 'attack.parameter' }; }
107
+ else if (payloadClass !== 'auth-bypass') limitations.push('no vulnerable parameter is known, so the payload test is applied to every query and body value of the endpoint');
108
+
109
+ let sink = null;
110
+ if (attack?.sink) { sink = attack.sink; facts.sink = { source: 'reproducer', ref: 'attack.sink' }; }
111
+ const affectedOperation = { sink, file: finding.file ? String(finding.file) : null, line: Number.isInteger(finding.line) ? finding.line : null };
112
+
113
+ const confidence = Math.max(0.1, Math.min(0.95,
114
+ (grounding === 'exploit-confirmed' ? 0.9 : grounding === 'reproduced' ? 0.7 : 0.4) - (endpoint ? 0 : 0.2) - (parameter || payloadClass === 'auth-bypass' ? 0 : 0.1)));
115
+ if (grounding !== 'exploit-confirmed') limitations.push(grounding === 'reproduced' ? 'the exploit was reproduced but not independently confirmed' : 'the exploit was not executed; this detection rests on static analysis only');
116
+
117
+ const ir = {
118
+ schema: DETECTION_IR_SCHEMA, version: DETECTION_IR_VERSION,
119
+ id: '', findingId,
120
+ cwe: finding.cwe ? String(finding.cwe) : null,
121
+ family: finding.family ? String(finding.family) : payloadClass,
122
+ attackBehavior: def.behavior,
123
+ endpoint, parameter, payloadClass, affectedOperation,
124
+ processEffect: def.effects.process ?? 'none', networkEffect: def.effects.network ?? 'none', dbEffect: def.effects.db ?? 'none',
125
+ confidence: Number(confidence.toFixed(2)), grounding, evidence, facts, limitations, digest: '',
126
+ };
127
+ ir.digest = (0,reproducer/* sha256 */.sc)((0,reproducer/* canonicalize */.dw)({ ...ir, id: '', digest: '' }));
128
+ ir.id = `det-${ir.digest.slice(0, 12)}`;
129
+ return { ok: true, ir };
130
+ }
131
+
132
+ /** Structural validation of an IR read back from disk or supplied by a caller. Returns a list of problems. */
133
+ function validateDetectionIR(ir) {
134
+ if (!ir || typeof ir !== 'object' || Array.isArray(ir)) return ['detection IR must be an object'];
135
+ const errs = [];
136
+ for (const k of Object.keys(ir)) if (!IR_KEYS.includes(k)) errs.push(`unknown IR field: ${k}`);
137
+ if (ir.schema !== DETECTION_IR_SCHEMA || ir.version !== DETECTION_IR_VERSION) errs.push('unsupported schema or version');
138
+ if (typeof ir.findingId !== 'string' || !ir.findingId) errs.push('findingId required');
139
+ if (!payload_classes/* PAYLOAD_CLASSES */.oE[ir.payloadClass]) errs.push(`unknown payload class: ${String(ir.payloadClass)}`);
140
+ if (!GROUNDING.includes(ir.grounding)) errs.push('unknown grounding');
141
+ if (ir.endpoint != null) {
142
+ if (!attack_facts/* HTTP_METHODS */.y8.includes(ir.endpoint.method) || typeof ir.endpoint.path !== 'string') errs.push('endpoint must be {method, path}');
143
+ else if ((0,attack_facts/* validateAttackFacts */.GT)({ method: ir.endpoint.method, path: ir.endpoint.path }).length) errs.push('endpoint path is not a plain URL path');
144
+ }
145
+ if (ir.parameter != null) {
146
+ if (!attack_facts/* PARAM_LOCATIONS */.ZC.includes(ir.parameter.location) || typeof ir.parameter.name !== 'string') errs.push('parameter must be {name, location}');
147
+ else if ((0,attack_facts/* validateAttackFacts */.GT)({ parameter: ir.parameter.name, location: ir.parameter.location }).length) errs.push('parameter name is not a plain name');
148
+ }
149
+ if (typeof ir.confidence !== 'number' || ir.confidence < 0 || ir.confidence > 1) errs.push('confidence must be between 0 and 1');
150
+ if (!Array.isArray(ir.evidence) || !Array.isArray(ir.limitations)) errs.push('evidence and limitations must be lists');
151
+ if (ir.digest && ir.digest !== (0,reproducer/* sha256 */.sc)((0,reproducer/* canonicalize */.dw)({ ...ir, id: '', digest: '' }))) errs.push('digest does not match the content');
152
+ return errs;
153
+ }
154
+
155
+ ;// CONCATENATED MODULE: ./src/posture/dnr/outputs.js
156
+ // Renderers for a detection IR (PRD 7, requirement 2): Sigma-compatible logic, a generic HTTP detection, generic WAF
157
+ // suggestions, a structured log query and YARA where it is appropriate. The P1 renderers (Splunk SPL, Microsoft Sentinel KQL,
158
+ // CloudWatch Logs Insights) are text only; nothing here connects to a SIEM.
159
+ //
160
+ // Every value interpolated into a rule comes from the IR, and the IR is closed-world (detection-ir.js), so a rule can only
161
+ // contain what a proof established. Each renderer escapes for ITS target's quoting rules, because an endpoint path or a
162
+ // parameter name is hostile input (it came from a reproducer a model may have written).
163
+
164
+
165
+ const FORMATS = Object.freeze(['sigma', 'http', 'waf', 'log-query', 'yara', 'spl', 'kql', 'cloudwatch', 'elastic', 'yaral', 'datadog']);
166
+
167
+ /** One regular expression (source text, case-insensitive) covering a class's indicators. */
168
+ function classRegex(className) {
169
+ const def = payload_classes/* PAYLOAD_CLASSES */.oE[className];
170
+ if (!def) return null;
171
+ if (def.matcher === 'length') return `.{${def.minLength},}`;
172
+ if (def.matcher === 'behavior') return null;
173
+ return def.indicators.map((re) => `(?:${re.source})`).join('|');
174
+ }
175
+
176
+ const hasTemplate = (p) => /\/(?::[A-Za-z_]\w*|\{[A-Za-z_]\w*\})(?=\/|$)/.test(p);
177
+ const escRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
178
+ /** A path template as an anchored regular expression. */
179
+ function pathRegex(path) {
180
+ return `^${path.split('/').map((seg) => (/^(?::[A-Za-z_]\w*|\{[A-Za-z_]\w*\})$/.test(seg) ? '[^/]+' : escRe(seg))).join('/')}$`;
181
+ }
182
+
183
+ const levelFor = (ir) => (ir.confidence >= 0.8 ? 'high' : ir.confidence >= 0.5 ? 'medium' : 'low');
184
+ const title = (ir) => `Exploitation attempt: ${ir.payloadClass} against ${ir.endpoint ? `${ir.endpoint.method} ${ir.endpoint.path}` : 'an application endpoint'}`;
185
+ const numericRuleId = (ir) => 9000000 + (parseInt(ir.digest.slice(0, 5), 16) % 999999);
186
+ const unavailable = (reason) => ({ applicable: false, reason });
187
+ const fpNotes = (ir) => [
188
+ 'A legitimate client that sends this payload shape to the same endpoint (a security scanner, a penetration test).',
189
+ ...(ir.payloadClass === 'auth-bypass' ? ['Public endpoints that are intentionally reachable without a credential.'] : []),
190
+ ];
191
+
192
+ /** Sigma rule as YAML text. JSON string syntax is used for every scalar; it is valid YAML and cannot be broken out of. */
193
+ function sigma(ir) {
194
+ if (!ir.endpoint) return unavailable('no endpoint is known for this finding, so there is no request to describe');
195
+ const q = JSON.stringify;
196
+ const lines = [
197
+ `title: ${q(title(ir))}`,
198
+ `id: ${q(`${ir.digest.slice(0, 8)}-${ir.digest.slice(8, 12)}-${ir.digest.slice(12, 16)}-${ir.digest.slice(16, 20)}-${ir.digest.slice(20, 32)}`)}`,
199
+ 'status: experimental',
200
+ `description: ${q(`${ir.attackBehavior}. Generated from finding ${ir.findingId} (${ir.grounding}).`)}`,
201
+ 'references:', ` - ${q(`finding:${ir.findingId}`)}`,
202
+ 'author: agentic-security',
203
+ 'logsource:', ' category: webserver',
204
+ 'detection:', ' selection_request:',
205
+ ` cs-method: ${q(ir.endpoint.method)}`,
206
+ ];
207
+ if (hasTemplate(ir.endpoint.path)) lines.push(` cs-uri-stem|re: ${q(pathRegex(ir.endpoint.path))}`);
208
+ else lines.push(` cs-uri-stem: ${q(ir.endpoint.path)}`);
209
+ let condition = 'selection_request';
210
+ if (ir.payloadClass === 'auth-bypass') {
211
+ lines.push(' selection_outcome:', ' sc-status|startswith: "2"', ' filter_credential:', ' cs-authorization|exists: true');
212
+ condition = 'selection_request and selection_outcome and not filter_credential';
213
+ } else {
214
+ const field = ir.parameter?.location === 'body' ? 'request-body' : ir.parameter?.location === 'header' ? `cs(${ir.parameter.name})` : ir.parameter?.location === 'cookie' ? 'cs-cookie' : 'cs-uri-query';
215
+ lines.push(' selection_payload:', ` ${field}|re|i: ${q(classRegex(ir.payloadClass))}`);
216
+ condition = 'selection_request and selection_payload';
217
+ }
218
+ lines.push(` condition: ${condition}`, 'falsepositives:', ...fpNotes(ir).map((n) => ` - ${q(n)}`), `level: ${levelFor(ir)}`,
219
+ 'tags:', ' - attack.initial_access', ' - attack.t1190', ...(ir.cwe ? [` - ${q(String(ir.cwe).toLowerCase().replace('cwe-', 'cwe.'))}`] : []));
220
+ return { applicable: true, format: 'sigma', content: `${lines.join('\n')}\n` };
221
+ }
222
+
223
+ function http(ir) {
224
+ if (!ir.endpoint) return unavailable('no endpoint is known for this finding');
225
+ const def = payload_classes/* PAYLOAD_CLASSES */.oE[ir.payloadClass];
226
+ const detection = {
227
+ schema: 'agentic-security/http-detection', version: 1, id: ir.id, findingId: ir.findingId, level: levelFor(ir), mode: 'detect',
228
+ request: { method: ir.endpoint.method, pathTemplate: ir.endpoint.path },
229
+ parameter: ir.parameter,
230
+ payload: ir.payloadClass === 'auth-bypass' ? { class: 'auth-bypass', behavior: 'status 2xx with no credential' }
231
+ : { class: ir.payloadClass, matcher: def.matcher, minLength: def.minLength ?? null, regex: classRegex(ir.payloadClass), flags: 'i' },
232
+ limitations: ir.limitations,
233
+ };
234
+ return { applicable: true, format: 'http', content: `${JSON.stringify(detection, null, 2)}\n` };
235
+ }
236
+
237
+ /** ModSecurity-style suggestion. Always detect-only (`pass`): a generated rule must never block traffic on its own. */
238
+ function waf(ir) {
239
+ if (!ir.endpoint) return unavailable('no endpoint is known for this finding');
240
+ const id = numericRuleId(ir);
241
+ const msg = `Agentic Security ${ir.id}: ${ir.payloadClass} on ${ir.endpoint.method} ${ir.endpoint.path}`.replace(/['"\\]/g, '');
242
+ const rx = (s) => s.replace(/"/g, '\\x22');
243
+ const pathOp = hasTemplate(ir.endpoint.path) ? `"@rx ${rx(pathRegex(ir.endpoint.path))}"` : `"@streq ${rx(ir.endpoint.path)}"`;
244
+ const out = [
245
+ '# SUGGESTION ONLY. Detect-only (pass). Review, test against your own traffic, then decide whether to enforce.',
246
+ `SecRule REQUEST_METHOD "@streq ${ir.endpoint.method}" "id:${id},phase:2,pass,nolog,chain"`,
247
+ ` SecRule REQUEST_FILENAME ${pathOp} "chain"`,
248
+ ];
249
+ if (ir.payloadClass === 'auth-bypass') {
250
+ out.push(` SecRule &REQUEST_HEADERS:Authorization "@eq 0" "log,auditlog,msg:'${msg}',severity:'WARNING',tag:'cwe-${(ir.cwe || '').replace(/\D/g, '')}'"`);
251
+ } else {
252
+ const target = ir.parameter?.location === 'body' ? `ARGS_POST${ir.parameter ? `:${ir.parameter.name}` : ''}` : ir.parameter?.location === 'header' ? `REQUEST_HEADERS:${ir.parameter.name}` : ir.parameter?.location === 'cookie' ? `REQUEST_COOKIES:${ir.parameter.name}` : ir.parameter ? `ARGS_GET:${ir.parameter.name}` : 'ARGS';
253
+ out.push(` SecRule ${target} "@rx ${rx(classRegex(ir.payloadClass))}" "t:urlDecodeUni,log,auditlog,msg:'${msg}',severity:'CRITICAL',tag:'cwe-${(ir.cwe || '').replace(/\D/g, '')}'"`);
254
+ }
255
+ return { applicable: true, format: 'waf', content: `${out.join('\n')}\n`, enforcement: 'detect-only' };
256
+ }
257
+
258
+ function logQuery(ir) {
259
+ if (!ir.endpoint) return unavailable('no endpoint is known for this finding');
260
+ const where = [{ field: 'http.request.method', op: 'eq', value: ir.endpoint.method }, hasTemplate(ir.endpoint.path) ? { field: 'url.path', op: 'regex', value: pathRegex(ir.endpoint.path) } : { field: 'url.path', op: 'eq', value: ir.endpoint.path }];
261
+ if (ir.payloadClass === 'auth-bypass') where.push({ field: 'http.response.status_code', op: 'range', value: [200, 299] }, { field: 'http.request.headers.authorization', op: 'absent' });
262
+ else where.push({ field: ir.parameter?.location === 'body' ? 'http.request.body.content' : ir.parameter?.location === 'query' ? `url.query.${ir.parameter.name}` : 'url.query', op: 'regex', value: classRegex(ir.payloadClass), flags: 'i' });
263
+ return { applicable: true, format: 'log-query', content: `${JSON.stringify({ schema: 'agentic-security/structured-log-query', version: 1, id: ir.id, findingId: ir.findingId, where }, null, 2)}\n` };
264
+ }
265
+
266
+ function yara(ir) {
267
+ if (ir.payloadClass !== 'deserialization') return unavailable(`YARA matches file or memory content; the ${ir.payloadClass} class is request-shaped, so a YARA rule would be misleading`);
268
+ const ident = `as_${ir.digest.slice(0, 12)}`;
269
+ const content = [
270
+ `rule ${ident} {`, ' meta:', ` description = ${JSON.stringify(`serialized object stream (${ir.findingId})`)}`, ` finding = ${JSON.stringify(ir.findingId)}`, ' generated_by = "agentic-security"',
271
+ ' strings:', ' $java_raw = { AC ED 00 05 }', ' $java_b64 = "rO0AB"', ' $pickle_b64 = "gASV"', ' $php = /O:[0-9]{1,4}:"[A-Za-z_\\\\]/',
272
+ ' condition:', ' any of them', '}', '',
273
+ ].join('\n');
274
+ return { applicable: true, format: 'yara', content };
275
+ }
276
+
277
+ const splQ = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
278
+ function spl(ir) {
279
+ if (!ir.endpoint) return unavailable('no endpoint is known for this finding');
280
+ const pathPart = hasTemplate(ir.endpoint.path) ? `| regex uri_path=${splQ(pathRegex(ir.endpoint.path))}` : `uri_path=${splQ(ir.endpoint.path)}`;
281
+ const head = `index=* method=${ir.endpoint.method} ${hasTemplate(ir.endpoint.path) ? '' : pathPart}`.trim();
282
+ const rest = [hasTemplate(ir.endpoint.path) ? pathPart : null,
283
+ ir.payloadClass === 'auth-bypass' ? '| where status>=200 AND status<300 AND isnull(http_authorization)' : `| regex ${ir.parameter?.location === 'body' ? 'request_body' : 'uri_query'}=${splQ(`(?i)${classRegex(ir.payloadClass)}`)}`].filter(Boolean);
284
+ return { applicable: true, format: 'spl', content: `${[head, ...rest].join('\n')}\n` };
285
+ }
286
+ const kqlQ = (s) => `@"${s.replace(/"/g, '""')}"`;
287
+ function kql(ir) {
288
+ if (!ir.endpoint) return unavailable('no endpoint is known for this finding');
289
+ if (['body', 'header', 'cookie'].includes(ir.parameter?.location)) return unavailable(`the W3CIISLog table does not record request ${ir.parameter.location}s, so this rule would never see the parameter`);
290
+ const lines = ['W3CIISLog', `| where csMethod == ${JSON.stringify(ir.endpoint.method)}`,
291
+ hasTemplate(ir.endpoint.path) ? `| where csUriStem matches regex ${kqlQ(pathRegex(ir.endpoint.path))}` : `| where csUriStem == ${JSON.stringify(ir.endpoint.path)}`,
292
+ ir.payloadClass === 'auth-bypass' ? '| where scStatus between (200 .. 299) and isempty(csAuthorization)' : `| where csUriQuery matches regex ${kqlQ(`(?i)${classRegex(ir.payloadClass)}`)}`];
293
+ return { applicable: true, format: 'kql', content: `${lines.join('\n')}\n` };
294
+ }
295
+ function cloudwatch(ir) {
296
+ if (!ir.endpoint) return unavailable('no endpoint is known for this finding');
297
+ if (['body', 'header', 'cookie'].includes(ir.parameter?.location)) return unavailable(`access logs do not record request ${ir.parameter.location}s, so this query would never see the parameter`);
298
+ const re = (s) => `/${s.replace(/\//g, '\\/')}/`;
299
+ const lines = ['fields @timestamp, @message', `| filter @message like ${re(`${ir.endpoint.method} ${hasTemplate(ir.endpoint.path) ? pathRegex(ir.endpoint.path).replace(/^\^|\$$/g, '') : escRe(ir.endpoint.path)}`)}`,
300
+ ir.payloadClass === 'auth-bypass' ? '| filter @message like /" 2[0-9][0-9] /' : `| filter @message like ${re(`(?i)${classRegex(ir.payloadClass)}`)}`, '| sort @timestamp desc'];
301
+ return { applicable: true, format: 'cloudwatch', content: `${lines.join('\n')}\n` };
302
+ }
303
+
304
+ const RENDERERS = { sigma, http, waf, 'log-query': logQuery, yara, spl, kql, cloudwatch };
305
+
306
+ /** Render one format. An inapplicable format is reported as such with its reason, never as an empty rule. */
307
+ function renderDetection(ir, format) {
308
+ const fn = RENDERERS[format];
309
+ if (!fn) return { applicable: false, reason: `unknown format: ${format}` };
310
+ return { format, ...fn(ir) };
311
+ }
312
+
313
+ function renderAll(ir) {
314
+ return Object.fromEntries(FORMATS.map((f) => [f, renderDetection(ir, f)]));
315
+ }
316
+
317
+ // ── Elasticsearch / Elastic Security, Google Chronicle (YARA-L) and Datadog Cloud SIEM (P1 adapters) ──────────────────────────────────
318
+ // Text only, like every other renderer here: nothing connects to a SIEM. Each target has a different regular-expression dialect or none,
319
+ // and a rule that silently means something else is worse than no rule, so a renderer either translates exactly or reports why it cannot.
320
+
321
+ /**
322
+ * A JavaScript regular-expression source as an Elasticsearch/Lucene `regexp` pattern, or null when some construct has no Lucene
323
+ * equivalent (anchors, word boundaries, look-around, back-references). A lazy quantifier becomes greedy, which is exact for a yes/no match. Lucene matches the WHOLE term, so the caller
324
+ * wraps unanchored patterns in `.*(...).*`; case-insensitivity is the query's `case_insensitive` flag, not an inline flag.
325
+ */
326
+ function luceneRegex(src) {
327
+ const cls = { d: '0-9', w: 'a-zA-Z0-9_', s: ' \\t\\n\\r' };
328
+ const bare = { d: '[0-9]', D: '[^0-9]', w: '[a-zA-Z0-9_]', s: '[ \\t\\n\\r]', S: '[^ \\t\\n\\r]' };
329
+ const RESERVED = new Set(['&', '~', '<', '>', '@', '"', '#']);
330
+ let out = ''; let inClass = false; let prevQuant = false;
331
+ for (let i = 0; i < src.length; i++) {
332
+ const c = src[i];
333
+ if (c === '\\') {
334
+ const n = src[++i];
335
+ if (n === undefined) return null;
336
+ if (inClass) {
337
+ if (cls[n]) out += cls[n]; else if (/[dswDSWbBnrt0-9A-Za-z]/.test(n)) { if (n === 'n') out += '\\n'; else if (n === 'r') out += '\\r'; else if (n === 't') out += '\\t'; else return null; } else out += `\\${n}`;
338
+ } else if (bare[n]) out += bare[n];
339
+ else if (n === 'n') out += '\\n'; else if (n === 'r') out += '\\r'; else if (n === 't') out += '\\t';
340
+ else if (/[A-Za-z0-9]/.test(n)) return null;
341
+ else out += `\\${n}`;
342
+ prevQuant = false; continue;
343
+ }
344
+ if (inClass) { if (c === ']') inClass = false; out += RESERVED.has(c) ? `\\${c}` : c; continue; }
345
+ if (c === '[') { inClass = true; out += c; prevQuant = false; continue; }
346
+ if (c === '(') {
347
+ if (src[i + 1] === '?') { if (src[i + 2] === ':') { out += '('; i += 2; prevQuant = false; continue; } return null; }
348
+ out += '('; prevQuant = false; continue;
349
+ }
350
+ if (c === '^' || c === '$') return null;
351
+ if (c === '?' && prevQuant) { prevQuant = false; continue; } // a lazy quantifier: for a yes/no match the greedy form is equivalent, and Lucene has no lazy form
352
+ if (c === '*' || c === '+' || c === '?') { out += c; prevQuant = true; continue; }
353
+ if (c === '{') { const m = /^\{\d+(?:,\d*)?\}/.exec(src.slice(i)); if (!m) { out += '\\{'; prevQuant = false; continue; } out += m[0]; i += m[0].length - 1; prevQuant = true; continue; }
354
+ if (c === '}') { out += '\\}'; prevQuant = false; continue; }
355
+ out += RESERVED.has(c) ? `\\${c}` : c; prevQuant = false;
356
+ }
357
+ return inClass ? null : out;
358
+ }
359
+
360
+ function elastic(ir) {
361
+ if (!ir.endpoint) return unavailable('no endpoint is known for this finding');
362
+ const loc = ir.parameter?.location;
363
+ const filter = [{ term: { 'http.request.method': ir.endpoint.method } }];
364
+ if (hasTemplate(ir.endpoint.path)) {
365
+ const p = luceneRegex(pathRegex(ir.endpoint.path).replace(/^\^|\$$/g, ''));
366
+ if (p === null) return unavailable('the endpoint path template has no Lucene regular-expression form');
367
+ filter.push({ regexp: { 'url.path': { value: p } } });
368
+ } else filter.push({ term: { 'url.path': ir.endpoint.path } });
369
+ const mustNot = [];
370
+ if (ir.payloadClass === 'auth-bypass') {
371
+ filter.push({ range: { 'http.response.status_code': { gte: 200, lte: 299 } } });
372
+ mustNot.push({ exists: { field: 'http.request.headers.authorization' } });
373
+ } else {
374
+ const re = luceneRegex(classRegex(ir.payloadClass) ?? '');
375
+ if (re === null || !classRegex(ir.payloadClass)) return unavailable(`the ${ir.payloadClass} indicators use regular-expression constructs Lucene cannot express exactly (anchors, word boundaries or look-around), so a rule would not mean what the proof established`);
376
+ const field = loc === 'body' ? 'http.request.body.content' : loc === 'query' ? 'url.query' : loc === 'header' || loc === 'cookie' ? null : 'url.query';
377
+ if (!field) return unavailable(`ECS web logs do not record request ${loc}s by default, so this rule would never see the parameter`);
378
+ filter.push({ regexp: { [field]: { value: `.*(${re}).*`, case_insensitive: true } } });
379
+ }
380
+ const body = { query: { bool: { filter, ...(mustNot.length ? { must_not: mustNot } : {}) } }, _meta: { id: ir.id, findingId: ir.findingId, generatedBy: 'agentic-security', note: 'Lucene regexp matches the whole field value; the pattern is wrapped accordingly.' } };
381
+ return { applicable: true, format: 'elastic', content: `${JSON.stringify(body, null, 2)}\n` };
382
+ }
383
+
384
+ /** True when a regex source uses `^`, `$` or a word boundary outside a character class: its meaning depends on WHERE the value starts and ends. */
385
+ const edgeSensitive = (src) => /[\^$]/.test(src.replace(/\\./g, 'x').replace(/\[[^\]]*\]/g, 'x')) || /\\[bB]/.test(src);
386
+
387
+ function yaral(ir) {
388
+ if (!ir.endpoint) return unavailable('no endpoint is known for this finding');
389
+ if (['body', 'header', 'cookie'].includes(ir.parameter?.location)) return unavailable(`UDM HTTP events carry the URL, not request ${ir.parameter.location}s, so this rule would never see the parameter`);
390
+ if (ir.payloadClass === 'auth-bypass') return unavailable('UDM HTTP events do not carry request headers, so "no credential was sent" cannot be expressed');
391
+ const cr = classRegex(ir.payloadClass);
392
+ if (!cr) return unavailable(`the ${ir.payloadClass} class has no regular-expression indicators`);
393
+ // the indicators are written against a query string or a body; YARA-L sees the whole URL, so a pattern anchored to "the start/end of the value" would mean something else
394
+ if (edgeSensitive(cr)) return unavailable(`the ${ir.payloadClass} indicators are anchored to the start or end of the parameter value, and UDM only exposes the whole URL, so a rule would not mean what the proof established`);
395
+ const pathRe = hasTemplate(ir.endpoint.path) ? pathRegex(ir.endpoint.path).replace(/^\^|\$$/g, '') : escRe(ir.endpoint.path);
396
+ const urlRe = `(?i)^[^?]*${pathRe}(?:\\?.*)?$`;
397
+ const payloadRe = `(?i)${cr}`;
398
+ if (urlRe.includes('`') || payloadRe.includes('`')) return unavailable('the pattern contains a backtick, which cannot appear in a YARA-L raw regular expression');
399
+ const ident = `as_${ir.digest.slice(0, 12)}`;
400
+ const content = [
401
+ `rule ${ident} {`, ' meta:', ` description = ${JSON.stringify(title(ir))}`, ` finding = ${JSON.stringify(ir.findingId)}`, ' author = "agentic-security"', ` severity = ${JSON.stringify(levelFor(ir).toUpperCase())}`,
402
+ ' events:', ' $e.metadata.event_type = "NETWORK_HTTP"', ` $e.network.http.method = ${JSON.stringify(ir.endpoint.method)}`,
403
+ ` re.regex($e.target.url, \`${urlRe}\`)`, ` re.regex($e.target.url, \`${payloadRe}\`)`, ' condition:', ' $e', '}', '',
404
+ ].join('\n');
405
+ return { applicable: true, format: 'yaral', content };
406
+ }
407
+
408
+ function datadog(ir) {
409
+ if (!ir.endpoint) return unavailable('no endpoint is known for this finding');
410
+ if (ir.payloadClass !== 'auth-bypass') return unavailable(`Datadog log search has wildcards but no regular expressions, so the ${ir.payloadClass} indicators cannot be expressed exactly; use the log-query or sigma output`);
411
+ const wild = ir.endpoint.path.replace(/\/(?::[A-Za-z_]\w*|\{[A-Za-z_]\w*\})(?=\/|$)/g, '/*');
412
+ if (/[\s"\\]/.test(wild)) return unavailable('the endpoint path contains characters that cannot be quoted safely in a Datadog query');
413
+ const query = `@http.method:${ir.endpoint.method} @http.url_details.path:${wild} @http.status_code:[200 TO 299] -@http.request.headers.authorization:*`;
414
+ const rule = { name: title(ir), type: 'log_detection', isEnabled: false, message: `${title(ir)}. Finding ${ir.findingId}. Generated by agentic-security; review before enabling.`, tags: ['source:agentic-security'], queries: [{ query, aggregation: 'count', groupByFields: [], distinctFields: [], name: 'a' }], cases: [{ name: 'proven pattern seen', status: levelFor(ir) === 'high' ? 'high' : 'medium', condition: 'a > 0', notifications: [] }], options: { evaluationWindow: 900, keepAlive: 3600, maxSignalDuration: 86400 } };
415
+ return { applicable: true, format: 'datadog', content: `${JSON.stringify(rule, null, 2)}\n` };
416
+ }
417
+ RENDERERS.elastic = elastic; RENDERERS.yaral = yaral; RENDERERS.datadog = datadog;
418
+
419
+ ;// CONCATENATED MODULE: ./src/posture/dnr/matchers.js
420
+ // The one evaluator behind every detection output. Validation (synthetic events), hunting (historical logs) and the
421
+ // rendered rules' described behavior all call `evaluateEvent`, so a rule that passes validation is the same rule that
422
+ // searches the logs. An event is the NORMALIZED request record produced by log-reader.js or by the validator.
423
+ //
424
+ // event = { time, method, path, query: {name: [values]}, bodyParams: {name: [values]} | null, headers: {name: value} | null,
425
+ // status, authPresent: boolean | null }
426
+ //
427
+ // A missing field is `undefined`/`null`, never an empty string: "the log did not record this" and "the value was empty" are
428
+ // different facts, and only the first makes an event undecidable.
429
+
430
+
431
+ const norm = (p) => (p.length > 1 ? p.replace(/\/+$/, '') : p);
432
+
433
+ /** `/api/users/:id` and `/api/users/{id}` match any single non-empty segment in that position. */
434
+ function pathMatches(template, actual) {
435
+ if (typeof actual !== 'string' || !actual.startsWith('/')) return false;
436
+ const t = norm(template).split('/');
437
+ const a = norm(actual.split('?')[0]).split('/');
438
+ if (t.length !== a.length) return false;
439
+ return t.every((seg, i) => (/^(?::[A-Za-z_]\w*|\{[A-Za-z_]\w*\})$/.test(seg) ? a[i].length > 0 : seg === a[i]));
440
+ }
441
+
442
+ /** Fields an event must have for a detection to be able to say "no" about it. */
443
+ function requiredFields(ir) {
444
+ const need = new Set();
445
+ if (ir.endpoint) { need.add('method'); need.add('path'); }
446
+ if (ir.payloadClass === 'auth-bypass') { need.add('status'); need.add('auth'); return [...need]; }
447
+ const loc = ir.parameter?.location;
448
+ if (loc === 'body') need.add('body');
449
+ else if (loc === 'header' || loc === 'cookie') need.add('headers');
450
+ else if (loc === 'path') need.add('path');
451
+ else need.add('query');
452
+ if (!ir.endpoint) need.add('path');
453
+ return [...need];
454
+ }
455
+
456
+ function candidateValues(ir, event) {
457
+ const out = [];
458
+ const p = ir.parameter;
459
+ const take = (bag, name) => { for (const v of (bag?.[name] ?? [])) out.push([`${name}`, v]); };
460
+ if (!p) {
461
+ for (const [k, vs] of Object.entries(event.query ?? {})) for (const v of vs) out.push([`query:${k}`, v]);
462
+ for (const [k, vs] of Object.entries(event.bodyParams ?? {})) for (const v of vs) out.push([`body:${k}`, v]);
463
+ return { values: out, decidable: event.query != null };
464
+ }
465
+ if (p.location === 'query') { take(event.query, p.name); return { values: out.map(([k, v]) => [`query:${k}`, v]), decidable: event.query != null }; }
466
+ if (p.location === 'body') { take(event.bodyParams, p.name); return { values: out.map(([k, v]) => [`body:${k}`, v]), decidable: event.bodyParams != null }; }
467
+ if (p.location === 'header' || p.location === 'cookie') {
468
+ const v = event.headers?.[p.name.toLowerCase()];
469
+ return { values: v == null ? [] : [[`${p.location}:${p.name}`, v]], decidable: event.headers != null };
470
+ }
471
+ // path parameter: the segment where the template has the parameter
472
+ if (p.location === 'path' && ir.endpoint) {
473
+ const t = norm(ir.endpoint.path).split('/');
474
+ const a = norm(String(event.path).split('?')[0]).split('/');
475
+ t.forEach((seg, i) => { if (seg === `:${p.name}` || seg === `{${p.name}}`) out.push([`path:${p.name}`, a[i] ?? '']); });
476
+ }
477
+ return { values: out, decidable: typeof event.path === 'string' };
478
+ }
479
+
480
+ /**
481
+ * @returns {{match: boolean, undecidable: boolean, missing: string[], matchedOn: string|null, value: string|null}}
482
+ * `undecidable` means "this event lacks a field the detection needs", which is different from "this event does not match".
483
+ */
484
+ function evaluateEvent(ir, event) {
485
+ const res = { match: false, undecidable: false, missing: [], matchedOn: null, value: null };
486
+ if (ir.endpoint) {
487
+ if (!event.method) res.missing.push('method');
488
+ if (typeof event.path !== 'string') res.missing.push('path');
489
+ if (res.missing.length) { res.undecidable = true; return res; }
490
+ if (String(event.method).toUpperCase() !== ir.endpoint.method) return res;
491
+ if (!pathMatches(ir.endpoint.path, event.path)) return res;
492
+ } else if (typeof event.path !== 'string') { res.missing.push('path'); res.undecidable = true; return res; }
493
+
494
+ if (ir.payloadClass === 'auth-bypass') {
495
+ if (event.status == null) res.missing.push('status');
496
+ if (event.authPresent == null) res.missing.push('auth');
497
+ if (res.missing.length) { res.undecidable = true; return res; }
498
+ if (event.status >= 200 && event.status < 300 && event.authPresent === false) { res.match = true; res.matchedOn = 'unauthenticated-success'; res.value = `${event.status}`; }
499
+ return res;
500
+ }
501
+
502
+ if (!payload_classes/* PAYLOAD_CLASSES */.oE[ir.payloadClass]) { res.undecidable = true; res.missing.push('payload-class'); return res; }
503
+ const { values, decidable } = candidateValues(ir, event);
504
+ for (const [where, v] of values) {
505
+ if ((0,payload_classes/* matchesClass */.Us)(ir.payloadClass, v)) { res.match = true; res.matchedOn = where; res.value = String(v).slice(0, 200); return res; }
506
+ }
507
+ if (!decidable) { res.undecidable = true; res.missing.push(ir.parameter?.location === 'body' ? 'body' : ir.parameter?.location === 'header' || ir.parameter?.location === 'cookie' ? 'headers' : 'query'); }
508
+ return res;
509
+ }
510
+
511
+ ;// CONCATENATED MODULE: ./src/posture/dnr/validate.js
512
+ // Detection validation (PRD 7, requirement 6): before a detection is allowed to be "enabled", prove it behaves.
513
+ // positive exploit event -> the detection fires
514
+ // negative lookalike -> the detection does not fire
515
+ // Events are synthesized from the IR's own proven facts (endpoint, parameter, class) and the class's exemplar payloads, then
516
+ // pushed through the SAME evaluator the hunt uses, so a rule that validates is the rule that searches the logs. The negatives
517
+ // are the point: a rule that fires on everything passes every positive test.
518
+
519
+
520
+
521
+
522
+ const PROBE = 'probe';
523
+
524
+ function eventFor(ir, { method, path, values, extraParam = null, status = 200, authPresent = null }) {
525
+ const p = ir.parameter;
526
+ const bag = {};
527
+ const ev = { time: '2024-01-01T00:00:00.000Z', method, path, query: {}, bodyParams: {}, headers: {}, status, authPresent };
528
+ const put = (loc, name, vals) => {
529
+ if (loc === 'query') ev.query[name] = vals;
530
+ else if (loc === 'body') ev.bodyParams[name] = vals;
531
+ else if (loc === 'header' || loc === 'cookie') ev.headers[name.toLowerCase()] = vals[0];
532
+ };
533
+ void bag;
534
+ if (values) {
535
+ if (p?.location === 'path') {
536
+ const t = ir.endpoint.path.split('/').map((seg) => (seg === `:${p.name}` || seg === `{${p.name}}` ? encodeURIComponent(values[0]) : (/^(?::\w+|\{\w+\})$/.test(seg) ? 'x' : seg)));
537
+ if (path === ir.endpoint.path.replace(/(:[A-Za-z_]\w*|\{[A-Za-z_]\w*\})/g, '42')) ev.path = t.join('/');
538
+ } else if (p) put(p.location, extraParam ?? p.name, values);
539
+ else put('query', extraParam ?? PROBE, values);
540
+ }
541
+ // a channel the IR does not use is "present and empty", not "not logged": the event models a log that records everything
542
+ return ev;
543
+ }
544
+
545
+ function validateDetection(ir) {
546
+ const def = payload_classes/* PAYLOAD_CLASSES */.oE[ir.payloadClass];
547
+ const cases = [];
548
+ const render = { regexCompiles: true };
549
+ try { const rx = classRegex(ir.payloadClass); if (rx) new RegExp(rx, 'i'); } catch { render.regexCompiles = false; }
550
+ if (!ir.endpoint) return { schema: 'agentic-security/detection-validation', version: 1, detectionId: ir.id, status: 'NOT_VALIDATED', reason: 'no endpoint is known, so there is no request to synthesize', cases, render, counts: { positives: 0, negatives: 0, passed: 0, failed: 0 } };
551
+ const concretePath = ir.endpoint.path.replace(/(:[A-Za-z_]\w*|\{[A-Za-z_]\w*\})/g, '42');
552
+ const { method } = ir.endpoint;
553
+ const add = (kind, name, ev) => { const r = evaluateEvent(ir, ev); cases.push({ kind, name, expected: kind === 'positive', fired: r.match, undecidable: r.undecidable, ok: kind === 'positive' ? r.match : !r.match }); };
554
+
555
+ if (ir.payloadClass === 'auth-bypass') {
556
+ add('positive', 'success with no credential', eventFor(ir, { method, path: concretePath, status: 200, authPresent: false }));
557
+ add('negative', 'success with a credential', eventFor(ir, { method, path: concretePath, status: 200, authPresent: true }));
558
+ add('negative', 'refusal without a credential (401)', eventFor(ir, { method, path: concretePath, status: 401, authPresent: false }));
559
+ add('negative', 'refusal without a credential (403)', eventFor(ir, { method, path: concretePath, status: 403, authPresent: false }));
560
+ add('negative', 'same request on another path', eventFor(ir, { method, path: '/other/unrelated', status: 200, authPresent: false }));
561
+ } else {
562
+ for (const [i, v] of def.exemplars.positive.entries()) add('positive', `exploit payload #${i + 1}`, eventFor(ir, { method, path: concretePath, values: [v] }));
563
+ for (const [i, v] of def.exemplars.benign.entries()) add('negative', `benign value #${i + 1} on the same endpoint`, eventFor(ir, { method, path: concretePath, values: [v] }));
564
+ const exploit = [def.exemplars.positive[0]];
565
+ const otherMethod = method === 'GET' ? 'POST' : 'GET';
566
+ add('negative', `exploit payload with another method (${otherMethod})`, eventFor(ir, { method: otherMethod, path: concretePath, values: exploit }));
567
+ add('negative', 'exploit payload on another path', eventFor(ir, { method, path: '/other/unrelated', values: exploit }));
568
+ add('negative', 'same endpoint, no parameter at all', eventFor(ir, { method, path: concretePath }));
569
+ if (ir.parameter && ir.parameter.location !== 'path') add('negative', 'exploit payload in an unrelated parameter', eventFor(ir, { method, path: concretePath, values: exploit, extraParam: 'unrelated_field' }));
570
+ }
571
+ const positives = cases.filter((c) => c.kind === 'positive'); const negatives = cases.filter((c) => c.kind === 'negative');
572
+ const failed = cases.filter((c) => !c.ok);
573
+ const status = failed.length === 0 && positives.length && negatives.length && render.regexCompiles ? 'VALIDATED' : 'FAILED';
574
+ return { schema: 'agentic-security/detection-validation', version: 1, detectionId: ir.id, status, cases, render, counts: { positives: positives.length, negatives: negatives.length, passed: cases.length - failed.length, failed: failed.length }, failures: failed.map((c) => c.name) };
575
+ }
576
+
577
+ // EXTERNAL MODULE: external "node:readline"
578
+ var external_node_readline_ = __webpack_require__(80481);
579
+ ;// CONCATENATED MODULE: ./src/posture/dnr/log-reader.js
580
+ // Bounded, streaming reader for historical HTTP logs (PRD 7, requirement 4). Logs are hostile input: a file may be huge,
581
+ // binary, symlinked, line-less or crafted to blow up a parser. Everything here has a cap and a clock, and every cap that is
582
+ // reached is REPORTED (`truncated`, `budgetExhausted`, skipped-file reasons) so the hunt can answer INCONCLUSIVE rather than
583
+ // pretend it searched everything.
584
+ //
585
+ // Production logs stay local: this module reads files and calls a callback. It imports no network module, and a test pins
586
+ // that. Understood shapes: Apache/nginx common and combined access logs, JSON lines (including Envoy and generic structured
587
+ // loggers). Anything else on a line is a parse failure, counted, never guessed at.
588
+
589
+
590
+
591
+
592
+ const LIMITS = Object.freeze({ maxFiles: 500, maxDepth: 4, maxFileBytes: 256 * 1024 * 1024, maxTotalBytes: 2 * 1024 * 1024 * 1024, maxLines: 20_000_000, maxLineChars: 32 * 1024, maxParams: 200, budgetMs: 10 * 60 * 1000 });
593
+ const TEXT_EXT = /\.(?:log|txt|json|jsonl|ndjson|access|out)$|^[^.]+$/i;
594
+ const MONTHS = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 };
595
+ const CLF = /^(\S+) \S+ (\S+) \[([^\]]+)\] "([A-Z]{3,8}) (\S+)(?: HTTP\/[\d.]+)?" (\d{3}|-) (?:\d+|-)/;
596
+
597
+ const decode = (s) => { try { return decodeURIComponent(String(s).replace(/\+/g, ' ')); } catch { return String(s); } };
598
+
599
+ function parseQuery(qs, cap = LIMITS.maxParams) {
600
+ const out = {};
601
+ let n = 0;
602
+ for (const part of String(qs ?? '').split('&')) {
603
+ if (!part) continue;
604
+ if (++n > cap) break;
605
+ const i = part.indexOf('=');
606
+ const k = decode(i === -1 ? part : part.slice(0, i));
607
+ const v = i === -1 ? '' : decode(part.slice(i + 1));
608
+ if (k.length > 128) continue;
609
+ (out[k] ??= []).push(v);
610
+ }
611
+ return out;
612
+ }
613
+
614
+ function parseTime(v) {
615
+ if (v == null || v === '') return null;
616
+ if (typeof v === 'number' && Number.isFinite(v)) { const ms = v > 1e12 ? v : v * 1000; const d = new Date(ms); return Number.isNaN(d.getTime()) ? null : d.toISOString(); }
617
+ const s = String(v);
618
+ const clf = /^(\d{1,2})\/([A-Za-z]{3})\/(\d{4}):(\d{2}):(\d{2}):(\d{2}) ([+-]\d{4})$/.exec(s);
619
+ if (clf && clf[2] in MONTHS) {
620
+ const off = clf[7];
621
+ const sign = off[0] === '-' ? -1 : 1;
622
+ const offMin = sign * (Number(off.slice(1, 3)) * 60 + Number(off.slice(3, 5)));
623
+ return new Date(Date.UTC(Number(clf[3]), MONTHS[clf[2]], Number(clf[1]), Number(clf[4]), Number(clf[5]), Number(clf[6])) - offMin * 60000).toISOString();
624
+ }
625
+ if (/^\d{9,13}(?:\.\d+)?$/.test(s)) return parseTime(Number(s));
626
+ const d = new Date(s);
627
+ return Number.isNaN(d.getTime()) ? null : d.toISOString();
628
+ }
629
+
630
+ function flatten(obj, prefix = '', depth = 0, out = {}) {
631
+ for (const [k, v] of Object.entries(obj)) {
632
+ const key = prefix ? `${prefix}.${k}` : k;
633
+ if (v && typeof v === 'object' && !Array.isArray(v) && depth < 3) flatten(v, key, depth + 1, out);
634
+ else out[key.toLowerCase()] = v;
635
+ }
636
+ return out;
637
+ }
638
+ const pick = (flat, names) => { for (const n of names) if (flat[n] !== undefined && flat[n] !== null && flat[n] !== '') return flat[n]; return undefined; };
639
+
640
+ function fromRequestTarget(target) {
641
+ const t = String(target);
642
+ const i = t.indexOf('?');
643
+ const p = i === -1 ? t : t.slice(0, i);
644
+ return { path: p.startsWith('http') ? (() => { try { return new URL(p).pathname; } catch { return null; } })() : p, query: i === -1 ? {} : parseQuery(t.slice(i + 1)) };
645
+ }
646
+
647
+ /** One log line -> normalized event, or null when it is not a request record in a shape this reader understands. */
648
+ function parseLine(line) {
649
+ const text = line.trim();
650
+ if (!text) return undefined; // blank: neither parsed nor failed
651
+ if (text[0] === '{') {
652
+ let obj;
653
+ try { obj = JSON.parse(text); } catch { return null; }
654
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return null;
655
+ const f = flatten(obj);
656
+ const method = pick(f, ['method', 'request_method', 'http.request.method', 'http.method', 'req.method', 'request.method', 'httpmethod', 'verb']);
657
+ let target = pick(f, ['path', 'uri', 'url', 'request_uri', ':path', 'url.path', 'request.path', 'req.url', 'http.url', 'request.uri', 'url.original', 'requesturi']);
658
+ const requestLine = pick(f, ['request']); // "GET /x HTTP/1.1" in some structured loggers
659
+ let m = method;
660
+ if (!target && typeof requestLine === 'string') { const rl = /^([A-Z]{3,8}) (\S+)/.exec(requestLine); if (rl) { m = m ?? rl[1]; target = rl[2]; } }
661
+ if (typeof target !== 'string' && typeof m !== 'string') return null;
662
+ // `flatten` dissolves nested objects, so a structured body or query is read from the original object
663
+ const nested = (...chain) => chain.reduce((acc, k) => (acc && typeof acc === 'object' ? acc[k] : undefined), obj);
664
+ const qsField = pick(f, ['query', 'query_string', 'url.query', 'request.query', 'querystring']) ?? nested('query') ?? nested('request', 'query') ?? nested('url', 'query');
665
+ const tgt = typeof target === 'string' ? fromRequestTarget(target) : { path: undefined, query: undefined };
666
+ let query = tgt.query;
667
+ if (typeof qsField === 'string') query = { ...(query ?? {}), ...parseQuery(qsField.replace(/^\?/, '')) };
668
+ else if (qsField && typeof qsField === 'object') query = { ...(query ?? {}), ...Object.fromEntries(Object.entries(qsField).slice(0, LIMITS.maxParams).map(([k, v]) => [k, [].concat(v).map(String)])) };
669
+ const bodyRaw = pick(f, ['body', 'request_body', 'request.body', 'http.request.body.content', 'req.body', 'payload']) ?? nested('body') ?? nested('request', 'body') ?? nested('req', 'body') ?? nested('http', 'request', 'body', 'content') ?? nested('payload');
670
+ let bodyParams = null;
671
+ if (typeof bodyRaw === 'string') {
672
+ try { const j = JSON.parse(bodyRaw); bodyParams = j && typeof j === 'object' && !Array.isArray(j) ? Object.fromEntries(Object.entries(flatten(j)).slice(0, LIMITS.maxParams).map(([k, v]) => [k, [String(v)]])) : { '': [bodyRaw] }; } catch { bodyParams = bodyRaw.includes('=') ? parseQuery(bodyRaw) : { '': [bodyRaw] }; }
673
+ } else if (bodyRaw && typeof bodyRaw === 'object') bodyParams = Object.fromEntries(Object.entries(flatten(bodyRaw)).slice(0, LIMITS.maxParams).map(([k, v]) => [k, [String(v)]]));
674
+ const headers = {};
675
+ let sawHeaders = false;
676
+ for (const [k, v] of Object.entries(f)) {
677
+ const hm = /^(?:request\.)?headers?\.(.+)$/.exec(k) || /^http\.request\.headers\.(.+)$/.exec(k);
678
+ if (hm) { headers[hm[1]] = String(v); sawHeaders = true; }
679
+ }
680
+ const authHeader = headers.authorization ?? pick(f, ['authorization']);
681
+ const authFlag = pick(f, ['authenticated', 'is_authenticated', 'auth.present']);
682
+ const authPresent = authHeader != null ? true : typeof authFlag === 'boolean' ? authFlag : (sawHeaders ? false : null);
683
+ const statusRaw = pick(f, ['status', 'status_code', 'response_code', 'http.response.status_code', 'sc-status', 'response.status', 'res.statuscode']);
684
+ const status = Number.isInteger(Number(statusRaw)) && statusRaw !== undefined ? Number(statusRaw) : null;
685
+ return { time: parseTime(pick(f, ['@timestamp', 'timestamp', 'time', 'ts', 'start_time', 'datetime', 'date'])), method: typeof m === 'string' ? m.toUpperCase() : null, path: tgt.path ?? null, query: query ?? null, bodyParams, headers: sawHeaders ? headers : null, status, authPresent };
686
+ }
687
+ const c = CLF.exec(text);
688
+ if (c) {
689
+ const { path: p, query } = fromRequestTarget(c[5]);
690
+ return { time: parseTime(c[3]), method: c[4], path: p, query, bodyParams: null, headers: null, status: c[6] === '-' ? null : Number(c[6]), authPresent: null };
691
+ }
692
+ return null;
693
+ }
694
+
695
+ function collectFiles(root, limits, skipped) {
696
+ const out = [];
697
+ const walk = (p, depth) => {
698
+ let st;
699
+ try { st = external_node_fs_.lstatSync(p); } catch { skipped.push({ file: external_node_path_.basename(p), reason: 'unreadable' }); return; }
700
+ if (st.isSymbolicLink()) { skipped.push({ file: external_node_path_.basename(p), reason: 'symlink' }); return; }
701
+ if (st.isDirectory()) {
702
+ if (depth > limits.maxDepth) { skipped.push({ file: external_node_path_.basename(p), reason: 'too-deep' }); return; }
703
+ let names = [];
704
+ try { names = external_node_fs_.readdirSync(p).sort(); } catch { skipped.push({ file: external_node_path_.basename(p), reason: 'unreadable' }); return; }
705
+ for (const n of names) walk(external_node_path_.join(p, n), depth + 1);
706
+ return;
707
+ }
708
+ if (!st.isFile()) return;
709
+ if (/\.(?:gz|zip|bz2|xz|zst|tgz)$/i.test(p)) { skipped.push({ file: external_node_path_.basename(p), reason: 'compressed-unsupported' }); return; }
710
+ if (!TEXT_EXT.test(external_node_path_.basename(p))) { skipped.push({ file: external_node_path_.basename(p), reason: 'not-a-log-extension' }); return; }
711
+ if (out.length >= limits.maxFiles) { skipped.push({ file: external_node_path_.basename(p), reason: 'file-count-limit' }); return; }
712
+ out.push({ file: p, size: st.size });
713
+ };
714
+ walk(root, 0);
715
+ return out;
716
+ }
717
+
718
+ /**
719
+ * Stream every request record under `target` through `onEvent(event)`. Never throws on bad content.
720
+ * @returns stats: files, lines, parsed/failed counts, time window, per-field availability, and every limit that was hit.
721
+ */
722
+ async function readLogs(target, { onEvent, signal, limits: over = {} } = {}) {
723
+ const limits = { ...LIMITS, ...over };
724
+ const stats = { files: [], filesSkipped: [], lines: 0, parsed: 0, failed: 0, oversizeLines: 0, windowStart: null, windowEnd: null, eventsWithTime: 0, fieldCounts: { method: 0, path: 0, query: 0, body: 0, headers: 0, status: 0, auth: 0 }, truncated: [], budgetExhausted: false, aborted: false, unreadable: false };
725
+ let st;
726
+ try { st = external_node_fs_.lstatSync(target); } catch { stats.unreadable = true; return stats; }
727
+ if (st.isSymbolicLink()) { stats.unreadable = true; stats.filesSkipped.push({ file: external_node_path_.basename(target), reason: 'symlink' }); return stats; }
728
+ const base = st.isDirectory() ? target : external_node_path_.dirname(target);
729
+ const files = st.isDirectory() ? collectFiles(target, limits, stats.filesSkipped) : collectFiles(target, limits, stats.filesSkipped);
730
+ const deadline = Date.now() + limits.budgetMs;
731
+ let totalBytes = 0;
732
+ for (const { file, size } of files) {
733
+ if (signal?.aborted) { stats.aborted = true; break; }
734
+ if (Date.now() > deadline) { stats.budgetExhausted = true; stats.truncated.push('time-budget'); break; }
735
+ if (size > limits.maxFileBytes) { stats.filesSkipped.push({ file: external_node_path_.relative(base, file), reason: 'file-too-large' }); stats.truncated.push('file-too-large'); continue; }
736
+ if (totalBytes + size > limits.maxTotalBytes) { stats.filesSkipped.push({ file: external_node_path_.relative(base, file), reason: 'total-size-limit' }); stats.truncated.push('total-size-limit'); continue; }
737
+ totalBytes += size;
738
+ const fstat = { name: external_node_path_.relative(base, file) || external_node_path_.basename(file), bytes: size, lines: 0, parsed: 0, failed: 0 };
739
+ stats.files.push(fstat);
740
+ const stream = external_node_fs_.createReadStream(file, { encoding: 'utf8', highWaterMark: 1 << 20 });
741
+ const rl = external_node_readline_.createInterface({ input: stream, crlfDelay: Infinity });
742
+ try {
743
+ for await (const line of rl) {
744
+ if (signal?.aborted) { stats.aborted = true; break; }
745
+ if ((fstat.lines & 0x3ff) === 0 && Date.now() > deadline) { stats.budgetExhausted = true; stats.truncated.push('time-budget'); break; }
746
+ if (stats.lines >= limits.maxLines) { stats.truncated.push('line-limit'); break; }
747
+ stats.lines++; fstat.lines++;
748
+ if (line.length > limits.maxLineChars) { stats.oversizeLines++; stats.failed++; fstat.failed++; continue; }
749
+ const ev = parseLine(line);
750
+ if (ev === undefined) continue;
751
+ if (ev === null) { stats.failed++; fstat.failed++; continue; }
752
+ stats.parsed++; fstat.parsed++;
753
+ if (ev.method) stats.fieldCounts.method++;
754
+ if (ev.path != null) stats.fieldCounts.path++;
755
+ if (ev.query != null) stats.fieldCounts.query++;
756
+ if (ev.bodyParams != null) stats.fieldCounts.body++;
757
+ if (ev.headers != null) stats.fieldCounts.headers++;
758
+ if (ev.status != null) stats.fieldCounts.status++;
759
+ if (ev.authPresent != null) stats.fieldCounts.auth++;
760
+ if (ev.time) { stats.eventsWithTime++; if (!stats.windowStart || ev.time < stats.windowStart) stats.windowStart = ev.time; if (!stats.windowEnd || ev.time > stats.windowEnd) stats.windowEnd = ev.time; }
761
+ onEvent?.(ev);
762
+ }
763
+ } catch { stats.filesSkipped.push({ file: fstat.name, reason: 'read-error' }); } finally { rl.close(); stream.destroy(); }
764
+ if (stats.aborted || stats.budgetExhausted || stats.truncated.includes('line-limit')) break;
765
+ }
766
+ stats.truncated = [...new Set(stats.truncated)];
767
+ return stats;
768
+ }
769
+
770
+ // EXTERNAL MODULE: external "node:crypto"
771
+ var external_node_crypto_ = __webpack_require__(77598);
772
+ ;// CONCATENATED MODULE: ./src/posture/dnr/redact.js
773
+ // Redaction for anything the loop persists or prints from production logs (PRD 7, privacy requirements). A matched log value
774
+ // is evidence that an exploit-shaped request existed; it must never also be a copy of a password, a token or regulated data.
775
+ // Layered on the proof fabric's secret scrubber (credentials), with the data classes a request log is likely to carry.
776
+
777
+
778
+
779
+ const PATTERNS = [
780
+ [/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, '[REDACTED-EMAIL]'],
781
+ [/\b(?:\d[ -]?){13,19}\b/g, '[REDACTED-NUMBER]'],
782
+ [/\b\d{3}-\d{2}-\d{4}\b/g, '[REDACTED-ID]'],
783
+ [/\b(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi, '[REDACTED-CREDENTIAL]'],
784
+ [/\b(?:password|passwd|pwd|secret|token|api[_-]?key|authorization|session|sid)=[^&\s"']+/gi, (m) => `${m.split('=')[0]}=[REDACTED]`],
785
+ [/\b[A-Za-z0-9+/_-]{40,}={0,2}/g, '[REDACTED-TOKEN]'],
786
+ ];
787
+
788
+ /** Redact a string for storage or display, then bound its length. */
789
+ function redactValue(value, max = 120) {
790
+ let t = String(value ?? '');
791
+ for (const [re, rep] of PATTERNS) t = t.replace(re, rep);
792
+ t = (0,store.scrubDeep)(t);
793
+ return t.length > max ? `${t.slice(0, max)}…` : t;
794
+ }
795
+
796
+ /** Stable short fingerprint of the ORIGINAL value, so two sightings of the same payload can be recognised without storing it. */
797
+ const fingerprint = (value) => (0,external_node_crypto_.createHash)('sha256').update(String(value ?? '')).digest('hex').slice(0, 16);
798
+
799
+ ;// CONCATENATED MODULE: ./src/posture/dnr/hunt.js
800
+ // Historical hunting (PRD 7, requirement 4): search local logs for requests that have the shape of the proven exploit.
801
+ //
802
+ // Four outcomes, and the rules for reaching each are deliberately asymmetric:
803
+ // OBSERVED at least one event matches. A match is a lead, not a verdict of compromise.
804
+ // NOT_OBSERVED nothing matched AND the evidence was good enough to look: logs readable and parsed cleanly, every
805
+ // field the detection needs present on the events, the read not cut short, timestamps available.
806
+ // INCONCLUSIVE nothing matched but one of those conditions failed. This is the honest answer, never NOT_OBSERVED.
807
+ // TELEMETRY_UNAVAILABLE no readable request records at all.
808
+ //
809
+ // NOT_OBSERVED never means NOT_EXPLOITED, and the result says so in a field a consumer can test (`notExploitedClaim: false`)
810
+ // and in a sentence a person reads.
811
+
812
+
813
+
814
+
815
+
816
+ const HUNT_VERDICTS = Object.freeze(['OBSERVED', 'NOT_OBSERVED', 'INCONCLUSIVE', 'TELEMETRY_UNAVAILABLE']);
817
+ const MAX_SAMPLES = 20;
818
+ const MAX_PARSE_FAILURE_RATIO = 0.05;
819
+
820
+ const STATEMENTS = {
821
+ OBSERVED: (n) => `${n} request(s) have the shape of this exploit. A match is a lead to investigate, not proof of compromise: a scanner or a test could produce the same request.`,
822
+ NOT_OBSERVED: () => 'No matching request was found in the searched logs. This does not establish that the vulnerability was not exploited: exploitation could have used a route, encoding or time these logs do not cover.',
823
+ INCONCLUSIVE: () => 'No matching request was found, but the logs could not support a negative answer. See the caveats; do not read this as "not exploited".',
824
+ TELEMETRY_UNAVAILABLE: () => 'There were no readable request records to search, so nothing can be said about past exploitation.',
825
+ };
826
+
827
+ const FIELD_COUNT_KEY = { method: 'method', path: 'path', query: 'query', body: 'body', headers: 'headers', status: 'status', auth: 'auth' };
828
+
829
+ async function huntLogs({ ir, logsPath, since = null, until = null, signal, limits } = {}) {
830
+ const base = { schema: 'agentic-security/hunt-result', version: 1, findingId: ir.findingId, detectionId: ir.id, detectionDigest: ir.digest, notExploitedClaim: false, egress: 'none' };
831
+ const sinceMs = since ? Date.parse(since) : null;
832
+ const untilMs = until ? Date.parse(until) : null;
833
+ if ((since && Number.isNaN(sinceMs)) || (until && Number.isNaN(untilMs))) {
834
+ return { ...base, verdict: 'INCONCLUSIVE', caveats: ['the --since/--until value is not a valid date, so nothing was searched'], matches: { count: 0, samples: [] }, statement: STATEMENTS.INCONCLUSIVE() };
835
+ }
836
+ const sampleList = [];
837
+ let matchCount = 0; let firstSeen = null; let lastSeen = null;
838
+ let undecidable = 0; const undecidableFields = new Set();
839
+ let endpointEvents = 0; let outOfWindow = 0; let undated = 0;
840
+
841
+ const stats = await readLogs(logsPath, {
842
+ signal, limits,
843
+ onEvent(ev) {
844
+ if (sinceMs != null || untilMs != null) {
845
+ if (!ev.time) { undated++; return; }
846
+ const t = Date.parse(ev.time);
847
+ if ((sinceMs != null && t < sinceMs) || (untilMs != null && t > untilMs)) { outOfWindow++; return; }
848
+ }
849
+ const r = evaluateEvent(ir, ev);
850
+ if (r.undecidable) { undecidable++; r.missing.forEach((m) => undecidableFields.add(m)); return; }
851
+ if (ir.endpoint && ev.method === ir.endpoint.method) endpointEvents++;
852
+ if (!r.match) return;
853
+ matchCount++;
854
+ if (ev.time) { if (!firstSeen || ev.time < firstSeen) firstSeen = ev.time; if (!lastSeen || ev.time > lastSeen) lastSeen = ev.time; }
855
+ if (sampleList.length < MAX_SAMPLES) sampleList.push({ time: ev.time, method: ev.method, path: redactValue(ev.path, 160), matchedOn: r.matchedOn, valueFingerprint: fingerprint(r.value), valuePreview: redactValue(r.value, 80), status: ev.status });
856
+ },
857
+ });
858
+
859
+ const logs = { source: external_node_path_.basename(String(logsPath)), files: stats.files.length, filesSkipped: stats.filesSkipped, lines: stats.lines, parsed: stats.parsed, failed: stats.failed, truncated: stats.truncated };
860
+ const window = { start: stats.windowStart, end: stats.windowEnd, datedEvents: stats.eventsWithTime };
861
+ const caveats = [];
862
+ const result = (verdict) => ({ ...base, verdict, logs, window, coverage: { requestedWindow: { since, until }, outOfWindow, undated, endpointEvents, requiredFields: requiredFields(ir), fieldAvailability: Object.fromEntries(Object.entries(FIELD_COUNT_KEY).map(([k, key]) => [k, stats.parsed ? Number((stats.fieldCounts[key] / stats.parsed).toFixed(3)) : 0])) }, matches: { count: matchCount, firstSeen, lastSeen, samples: sampleList }, caveats, statement: STATEMENTS[verdict](matchCount) });
863
+
864
+ if (stats.unreadable || stats.parsed === 0) {
865
+ caveats.push(stats.unreadable ? 'the log path could not be read' : stats.lines === 0 ? 'the logs contain no lines' : 'no line was recognised as a request record (supported: common/combined access logs, JSON lines)');
866
+ return result('TELEMETRY_UNAVAILABLE');
867
+ }
868
+ if (matchCount > 0) {
869
+ if (stats.truncated.length || stats.aborted) caveats.push(`the read was cut short (${[...stats.truncated, ...(stats.aborted ? ['aborted'] : [])].join(', ')}); more matches may exist`);
870
+ return result('OBSERVED');
871
+ }
872
+ // nothing matched: decide whether a negative answer is supportable
873
+ const failRatio = stats.lines ? stats.failed / stats.lines : 0;
874
+ if (failRatio > MAX_PARSE_FAILURE_RATIO) caveats.push(`${(failRatio * 100).toFixed(1)}% of lines could not be parsed (limit ${MAX_PARSE_FAILURE_RATIO * 100}%)`);
875
+ if (stats.truncated.length || stats.aborted || stats.budgetExhausted) caveats.push(`the read was cut short (${[...stats.truncated, ...(stats.aborted ? ['aborted'] : [])].join(', ') || 'budget'})`);
876
+ if (undecidable > 0) caveats.push(`${undecidable} event(s) lacked a field this detection needs (${[...undecidableFields].join(', ')}), so they could not be judged`);
877
+ if (!stats.eventsWithTime) caveats.push('no event carries a timestamp, so the searched time window is unknown');
878
+ if (stats.filesSkipped.some((f) => f.reason !== 'not-a-log-extension')) caveats.push(`${stats.filesSkipped.filter((f) => f.reason !== 'not-a-log-extension').length} file(s) were skipped (${[...new Set(stats.filesSkipped.map((f) => f.reason))].join(', ')})`);
879
+ if (undated > 0) caveats.push(`${undated} event(s) had no timestamp and were excluded by the requested window`);
880
+ if (!caveats.length && ir.endpoint && endpointEvents === 0) caveats.push('no request to the endpoint appears in the logs at all (informational: nothing to exploit was seen either)');
881
+ const blocking = caveats.some((c) => !c.startsWith('no request to the endpoint'));
882
+ return result(blocking ? 'INCONCLUSIVE' : 'NOT_OBSERVED');
883
+ }
884
+
885
+ // EXTERNAL MODULE: external "node:child_process"
886
+ var external_node_child_process_ = __webpack_require__(31421);
887
+ ;// CONCATENATED MODULE: ./src/posture/dnr/exposure-window.js
888
+ // Vulnerability exposure window (PRD 7, requirement 5): combine WHEN the flaw entered the code, WHEN a build containing it
889
+ // first ran, WHEN the fix was committed and deployed, and WHAT telemetry exists, into the span that can actually be hunted.
890
+ //
891
+ // Every date is sourced (provenance, a deployment record, a git commit, a hunt's own telemetry window). A date that has no
892
+ // source is reported as unknown and listed under `unknowns`; the window that depends on it is then `partial`, or
893
+ // `unavailable`, and the output never fills a gap with an assumption. Part of the exposure that predates the telemetry is
894
+ // reported as `unobservableBefore`: it happened, and nothing here can say whether it was exploited.
895
+
896
+
897
+
898
+ const MAX_DEPLOYMENTS = 1000;
899
+ const SHA_RE = /^[0-9a-f]{7,64}$/i;
900
+ const ts = (v) => { if (v == null) return null; const t = Date.parse(v); return Number.isNaN(t) ? null : t; };
901
+ const iso = (t) => (t == null ? null : new Date(t).toISOString());
902
+
903
+ /** Parse a deployment-history file. Returns {deployments, problems}. Unusable entries are listed, never silently dropped. */
904
+ function loadDeployments(file, { environment = null } = {}) {
905
+ const problems = [];
906
+ let raw;
907
+ try { raw = JSON.parse(external_node_fs_.readFileSync(file, 'utf8')); } catch (e) { return { deployments: [], problems: [`could not read deployment history: ${e.code || e.message}`] }; }
908
+ const list = Array.isArray(raw) ? raw : Array.isArray(raw?.deployments) ? raw.deployments : null;
909
+ if (!list) return { deployments: [], problems: ['deployment history must be a list, or an object with a "deployments" list'] };
910
+ if (list.length > MAX_DEPLOYMENTS) problems.push(`only the first ${MAX_DEPLOYMENTS} of ${list.length} deployments were read`);
911
+ const deployments = [];
912
+ list.slice(0, MAX_DEPLOYMENTS).forEach((d, i) => {
913
+ if (!d || typeof d !== 'object') { problems.push(`deployment ${i}: not an object`); return; }
914
+ if (typeof d.commit !== 'string' || !SHA_RE.test(d.commit)) { problems.push(`deployment ${i}: commit must be a hex sha`); return; }
915
+ if (ts(d.deployedAt) == null) { problems.push(`deployment ${i}: deployedAt is not a date`); return; }
916
+ if (environment && d.environment !== environment) return;
917
+ deployments.push({ commit: d.commit.toLowerCase(), deployedAt: iso(ts(d.deployedAt)), environment: typeof d.environment === 'string' ? d.environment.slice(0, 40) : null, artifactDigest: typeof d.artifactDigest === 'string' ? d.artifactDigest.slice(0, 80) : null });
918
+ });
919
+ return { deployments, problems };
920
+ }
921
+
922
+ function git(repo, args, timeoutMs = 15000) {
923
+ const r = (0,external_node_child_process_.spawnSync)('git', args, { cwd: repo, encoding: 'utf8', timeout: timeoutMs, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1 << 20 });
924
+ return r.error || r.status === null ? { ok: false, status: null, out: '' } : { ok: true, status: r.status, out: r.stdout.trim() };
925
+ }
926
+ /** true / false / null (null: git could not answer, e.g. an unknown commit or a shallow clone). */
927
+ function isAncestor(repo, ancestor, descendant) {
928
+ const r = git(repo, ['merge-base', '--is-ancestor', ancestor, descendant]);
929
+ if (!r.ok) return null;
930
+ return r.status === 0 ? true : r.status === 1 ? false : null;
931
+ }
932
+ function commitDate(repo, sha) {
933
+ const r = git(repo, ['show', '-s', '--format=%cI', sha]);
934
+ return r.ok && r.status === 0 && r.out ? iso(ts(r.out)) : null;
935
+ }
936
+
937
+ /** True only when the NEWEST recorded deployment is one that contains the flaw: a statement about the record, not about today. */
938
+ function stillExposed(deployments, vulnerable) {
939
+ if (!deployments.length || !vulnerable.length) return false;
940
+ const newest = [...deployments].sort((a, b) => Date.parse(b.deployedAt) - Date.parse(a.deployedAt))[0];
941
+ return vulnerable.some((d) => d.commit === newest.commit && d.deployedAt === newest.deployedAt);
942
+ }
943
+
944
+ function computeExposureWindow({ finding, repo, deployments: rawDeployments = [], deploymentProblems = [], fixCommit = null, telemetry = null, deps = {} } = {}) {
945
+ // callers may pass records that did not come through loadDeployments; normalise dates so every output is comparable
946
+ const deployments = rawDeployments.filter((d) => d && ts(d.deployedAt) != null).map((d) => ({ ...d, deployedAt: iso(ts(d.deployedAt)) }));
947
+ const anc = deps.isAncestor ?? ((a, d) => isAncestor(repo, a, d));
948
+ const cdate = deps.commitDate ?? ((sha) => commitDate(repo, sha));
949
+ const unknowns = [];
950
+ const origin = finding?.findingProvenance?.findingOrigin ?? null;
951
+ const introCommit = origin?.commit ?? null;
952
+ const introducedAt = iso(ts(origin?.authorDate ?? origin?.committerDate));
953
+ if (!introCommit) unknowns.push('introduction commit (the finding has no resolved origin)');
954
+ else if (!introducedAt) unknowns.push('introduction date');
955
+
956
+ const fixAt = fixCommit ? cdate(fixCommit) : null;
957
+ if (!fixCommit) unknowns.push('fix commit (not supplied)');
958
+ else if (!fixAt) unknowns.push('fix commit date (the commit could not be read)');
959
+
960
+ const unresolved = [];
961
+ const vulnerable = []; const fixed = [];
962
+ if (!deployments.length) unknowns.push('deployment history (none supplied)');
963
+ for (const d of deployments) {
964
+ if (!introCommit) break;
965
+ const hasBug = anc(introCommit, d.commit);
966
+ if (hasBug === null) { unresolved.push({ commit: d.commit, deployedAt: d.deployedAt, reason: 'ancestry could not be determined' }); continue; }
967
+ const hasFix = fixCommit ? anc(fixCommit, d.commit) : false;
968
+ if (fixCommit && hasFix === null) { unresolved.push({ commit: d.commit, deployedAt: d.deployedAt, reason: 'fix ancestry could not be determined' }); continue; }
969
+ if (hasBug && !hasFix) vulnerable.push(d);
970
+ if (hasFix) fixed.push(d);
971
+ }
972
+ if (unresolved.length) unknowns.push(`${unresolved.length} deployment(s) whose contents could not be resolved against the introduction/fix commits`);
973
+ const byTime = (a, b) => Date.parse(a.deployedAt) - Date.parse(b.deployedAt);
974
+ const firstVulnerable = [...vulnerable].sort(byTime)[0] ?? null;
975
+ const fixDeployedAt = [...fixed].sort(byTime)[0] ?? null;
976
+ if (introCommit && deployments.length && !firstVulnerable && !unresolved.length) unknowns.push('no recorded deployment contains the vulnerable code');
977
+ if (fixCommit && deployments.length && !fixDeployedAt && !unresolved.length) unknowns.push('the fix has not been deployed according to the deployment history');
978
+
979
+ const exposureStart = ts(firstVulnerable?.deployedAt);
980
+ const exposureEnd = ts(fixDeployedAt?.deployedAt);
981
+ const telStart = ts(telemetry?.start); const telEnd = ts(telemetry?.end);
982
+ if (telStart == null || telEnd == null) unknowns.push('telemetry window (no dated telemetry)');
983
+
984
+ let observable = null; let unobservableBefore = null;
985
+ if (exposureStart != null) {
986
+ const end = exposureEnd ?? telEnd;
987
+ if (telStart != null && telEnd != null && end != null) {
988
+ const s = Math.max(exposureStart, telStart); const e = Math.min(end, telEnd);
989
+ if (s <= e) observable = { start: iso(s), end: iso(e), openEnded: exposureEnd == null };
990
+ if (exposureStart < telStart) unobservableBefore = { start: iso(exposureStart), end: iso(Math.min(telStart, end ?? telStart)) };
991
+ }
992
+ }
993
+ const status = exposureStart == null ? 'unavailable' : (unknowns.length || !observable) ? 'partial' : 'complete';
994
+ return {
995
+ schema: 'agentic-security/exposure-window', version: 1, findingId: finding?.stableId ?? finding?.id ?? null, status,
996
+ vulnerabilityIntroduced: introducedAt ? { at: introducedAt, commit: introCommit, source: 'finding-provenance' } : null,
997
+ firstVulnerableDeployment: firstVulnerable ? { at: firstVulnerable.deployedAt, commit: firstVulnerable.commit, environment: firstVulnerable.environment, artifactDigest: firstVulnerable.artifactDigest } : null,
998
+ fixCommitted: fixAt ? { at: fixAt, commit: fixCommit } : null,
999
+ fixDeployed: fixDeployedAt ? { at: fixDeployedAt.deployedAt, commit: fixDeployedAt.commit, environment: fixDeployedAt.environment, artifactDigest: fixDeployedAt.artifactDigest } : null,
1000
+ stillExposed: stillExposed(deployments, vulnerable),
1001
+ telemetryWindow: telStart != null && telEnd != null ? { start: iso(telStart), end: iso(telEnd) } : null,
1002
+ observableWindow: observable, unobservableBefore, unresolvedDeployments: unresolved, deploymentProblems, unknowns,
1003
+ statement: 'The observable window is the part of the exposure that telemetry covers. Exposure outside it is unobserved, which is not the same as unexploited.',
1004
+ };
1005
+ }
1006
+
1007
+ function formatExposureWindow(w) {
1008
+ const day = (x) => (x ? x.at.slice(0, 10) : 'unknown');
1009
+ const lines = [
1010
+ `Vulnerability introduced: ${day(w.vulnerabilityIntroduced)}`,
1011
+ `First vulnerable deployment: ${day(w.firstVulnerableDeployment)}`,
1012
+ `Fix committed: ${day(w.fixCommitted)}`,
1013
+ `Fix deployed: ${w.fixDeployed ? day(w.fixDeployed) : w.stillExposed ? 'not deployed (still exposed)' : 'unknown'}`,
1014
+ `Telemetry available: ${w.telemetryWindow ? `${w.telemetryWindow.start.slice(0, 10)} to ${w.telemetryWindow.end.slice(0, 10)}` : 'unknown'}`,
1015
+ '',
1016
+ `Observable exposure window: ${w.observableWindow ? `${w.observableWindow.start.slice(0, 10)} to ${w.observableWindow.openEnded ? 'present (fix not deployed)' : w.observableWindow.end.slice(0, 10)}` : 'cannot be determined'}`,
1017
+ ];
1018
+ if (w.unobservableBefore) lines.push(`Exposed but not observable: ${w.unobservableBefore.start.slice(0, 10)} to ${w.unobservableBefore.end.slice(0, 10)} (before telemetry began)`);
1019
+ if (w.unknowns.length) lines.push('', `Status: ${w.status}. Unknown: ${w.unknowns.join('; ')}`);
1020
+ return lines.join('\n');
1021
+ }
1022
+
1023
+ // EXTERNAL MODULE: ./src/posture/dnr/store.js
1024
+ var dnr_store = __webpack_require__(58551);
1025
+ // EXTERNAL MODULE: ./src/posture/state-dir.js
1026
+ var state_dir = __webpack_require__(31174);
1027
+ ;// CONCATENATED MODULE: ./src/posture/dnr/view.js
1028
+ // The unified finding view (PRD 7, requirement 8): FIND, PROVE, HUNT, FIX, VERIFY, DETECT in one place, assembled only from
1029
+ // records that exist. A step with no record is shown as missing, not inferred from its neighbours; a tampered record is
1030
+ // shown as such and not used.
1031
+
1032
+
1033
+
1034
+
1035
+
1036
+
1037
+ function loadFindingFromLastScan(root, id) {
1038
+ try {
1039
+ const last = JSON.parse(external_node_fs_.readFileSync((0,state_dir.statePath)(root, 'last-scan.json'), 'utf8'));
1040
+ return (last.findings || []).find((f) => f.id === id || f.stableId === id) ?? null;
1041
+ } catch { return null; }
1042
+ }
1043
+
1044
+ const days = (w) => (w?.start && w?.end ? Math.max(1, Math.round((Date.parse(w.end) - Date.parse(w.start)) / 86400000)) : null);
1045
+
1046
+ function buildLifecycleView(root, findingId) {
1047
+ const finding = loadFindingFromLastScan(root, findingId);
1048
+ const proofs = (0,store.listProofs)(root, findingId);
1049
+ const proofTampered = proofs.some((p) => p.tampered);
1050
+ const proof = proofs.find((p) => p.record && !p.tampered)?.record ?? null;
1051
+ // An earlier hunt that OBSERVED matches must not be hidden by a later hunt over different logs that did not.
1052
+ const hunts = (0,dnr_store.listRecords)(root, findingId, 'hunt').filter((r) => r.record && !r.tampered).map((r) => r.record);
1053
+ const hunt = hunts.find((h) => h.verdict === 'OBSERVED') ?? hunts[0] ?? null;
1054
+ const otherVerdicts = hunt ? [...new Set(hunts.filter((h) => h !== hunt).map((h) => h.verdict))] : [];
1055
+ const fix = (0,dnr_store.latestRecord)(root, findingId, 'fix')?.record ?? null;
1056
+ const det = (0,dnr_store.latestRecord)(root, findingId, 'detection')?.record ?? null;
1057
+ const tampered = [proofTampered ? 'proof' : null, ...['hunt', 'fix', 'detection'].map((k) => ((0,dnr_store.listRecords)(root, findingId, k).some((r) => r.tampered) ? k : null))].filter(Boolean);
1058
+
1059
+ const confirmed = proof && (0,states/* stateRank */.g3)(proof.state) >= (0,states/* stateRank */.g3)('EXPLOIT_CONFIRMED');
1060
+ const steps = {
1061
+ FIND: finding ? { status: 'done', text: `${confirmed ? 'Confirmed vulnerability' : 'Candidate finding'}: ${finding.vuln ?? finding.id}${finding.file ? ` (${finding.file}${finding.line ? `:${finding.line}` : ''})` : ''}` } : { status: 'missing', text: 'Finding not found in the last scan' },
1062
+ PROVE: proof ? { status: confirmed ? 'done' : 'partial', text: confirmed ? `Exploit reproduced ${proof.attempts?.successes ?? 0}/${proof.attempts?.run ?? 0}, independent replay ${proof.independentReplay?.status ?? 'not run'}` : `Not confirmed (${proof.state})` } : { status: 'missing', text: 'No exploit proof' },
1063
+ HUNT: hunt ? { status: hunt.verdict === 'OBSERVED' || hunt.verdict === 'NOT_OBSERVED' ? 'done' : 'partial', text: `${days(hunt.window) ?? 'unknown'} day(s) of telemetry searched, ${hunt.verdict === 'OBSERVED' ? `${hunt.matches?.count ?? 0} matching event(s) observed` : hunt.verdict === 'NOT_OBSERVED' ? 'no matching events (not the same as not exploited)' : `${hunt.verdict.toLowerCase().replace('_', ' ')}`}${otherVerdicts.length ? `; other hunt(s): ${otherVerdicts.join(', ')}` : ''}` } : { status: 'missing', text: 'No historical hunt run' },
1064
+ FIX: fix ? { status: fix.verified ? 'done' : 'partial', text: fix.verified ? `${fix.completeness} verified remediation` : `Fix not verified: ${fix.reason ?? fix.incompleteStep ?? 'incomplete'}` } : { status: 'missing', text: 'No verified remediation recorded' },
1065
+ VERIFY: fix ? (fix.legs?.postPatch && fix.legs.postPatch.exploitSucceeded === 0 && fix.legs.postPatch.completed === fix.legs.postPatch.attempts ? { status: 'done', text: 'Original exploit blocked' } : { status: 'partial', text: 'Original exploit not shown to be blocked' }) : { status: 'missing', text: 'No post-fix replay recorded' },
1066
+ DETECT: det ? (det.validation?.status === 'VALIDATED' ? { status: 'done', text: `Recurrence rule enabled (${det.ir?.id})` } : { status: 'partial', text: `Detection ${det.ir?.id} generated but not validated (${det.validation?.status ?? 'no validation'}); not enabled` }) : { status: 'missing', text: 'No detection generated' },
1067
+ };
1068
+ return { findingId, steps, tampered, links: { detection: det ? { id: det.ir?.id, recordDigest: det.recordDigest, proofRecordDigest: det.proof?.recordDigest ?? null } : null, proofRecordDigest: proof?.recordDigest ?? null } };
1069
+ }
1070
+
1071
+ function formatLifecycleView(v) {
1072
+ const mark = { done: 'x', partial: '~', missing: ' ' };
1073
+ const lines = [`Finding ${v.findingId}`, ''];
1074
+ for (const [name, s] of Object.entries(v.steps)) lines.push(`[${mark[s.status]}] ${name.padEnd(7)} ${s.text}`);
1075
+ if (v.tampered.length) lines.push('', `WARNING: ${v.tampered.join(', ')} record(s) failed their integrity check and were not used.`);
1076
+ lines.push('', '[x] done [~] partial or unconfirmed [ ] missing');
1077
+ return lines.join('\n');
1078
+ }
1079
+
1080
+ ;// CONCATENATED MODULE: ./src/posture/dnr/cli.js
1081
+ // CLI surface for the detection-and-response loop: `dnr generate|validate|hunt|window|show|status`.
1082
+ // Non-interactive by construction (no command reads stdin), bounded (log reads have size, line and time budgets), and local:
1083
+ // nothing here contacts a network or a model, so production logs never leave the machine.
1084
+ //
1085
+ // Exit codes: 0 completed and clean, 4 usage or precondition, 1 a stored/produced result failed its own check (validation),
1086
+ // and for `hunt` a distinct code per verdict so automation cannot read an unanswered question as a clean one:
1087
+ // 0 NOT_OBSERVED 10 OBSERVED 11 INCONCLUSIVE 12 TELEMETRY_UNAVAILABLE
1088
+
1089
+
1090
+
1091
+
1092
+
1093
+
1094
+
1095
+
1096
+
1097
+
1098
+
1099
+ const HUNT_EXIT = Object.freeze({ NOT_OBSERVED: 0, OBSERVED: 10, INCONCLUSIVE: 11, TELEMETRY_UNAVAILABLE: 12 });
1100
+ const EXT = { sigma: 'yml', http: 'json', waf: 'conf', 'log-query': 'json', yara: 'yar', spl: 'spl', kql: 'kql', cloudwatch: 'txt', elastic: 'json', yaral: 'yaral', datadog: 'json' };
1101
+
1102
+ const usage = () => { console.error('usage: dnr generate <finding-id> [--format sigma|http|waf|log-query|yara|spl|kql|cloudwatch|elastic|yaral|datadog|all] [--out dir] [--allow-unproven]\n dnr validate <finding-id>\n dnr hunt <finding-id> --logs <file-or-dir> [--since ISO] [--until ISO]\n dnr window <finding-id> --deployments <file> [--fix-commit sha] [--environment name]\n dnr show <finding-id>\n dnr status <finding-id>'); return 4; };
1103
+
1104
+ function ensureDetection(root, id, flags) {
1105
+ const existing = (0,dnr_store.latestRecord)(root, id, 'detection');
1106
+ if (existing && !flags.regenerate) return { ok: true, saved: existing.record };
1107
+ const finding = loadFindingFromLastScan(root, id);
1108
+ if (!finding) return { ok: false, reason: `Finding ${id} not found in the last scan.` };
1109
+ const proofRec = (0,store.latestProof)(root, id);
1110
+ const built = buildDetectionIR({ finding, proof: proofRec?.record ?? null, allowUnproven: !!flags['allow-unproven'] });
1111
+ if (!built.ok) return { ok: false, reason: built.reason };
1112
+ const validation = validateDetection(built.ir);
1113
+ const record = { ir: built.ir, validation: { status: validation.status, counts: validation.counts, failures: validation.failures ?? [], reason: validation.reason ?? null }, enabled: validation.status === 'VALIDATED', proof: proofRec?.record ? { recordDigest: proofRec.record.recordDigest ?? null, state: proofRec.record.state ?? null } : null };
1114
+ const saved = (0,dnr_store.saveRecord)(root, id, 'detection', record);
1115
+ if (!saved.ok) return { ok: false, reason: `could not store the detection: ${saved.reason}` };
1116
+ return { ok: true, saved: { ...record, recordDigest: saved.digest }, fresh: true };
1117
+ }
1118
+
1119
+ function writeOut(dir, id, ir, formats) {
1120
+ // the id names a file: it must be the digest-derived form, never text that arrived from a record on disk
1121
+ if (!/^det-[0-9a-f]{12}$/.test(String(ir.id))) { console.error('refusing to write: the detection id is not in its derived form'); return []; }
1122
+ external_node_fs_.mkdirSync(dir, { recursive: true });
1123
+ const written = [];
1124
+ for (const f of formats) {
1125
+ const r = renderDetection(ir, f);
1126
+ if (!r.applicable) continue;
1127
+ const file = external_node_path_.join(dir, `${ir.id}.${f === 'log-query' ? 'query' : f}.${EXT[f]}`);
1128
+ try { if (external_node_fs_.lstatSync(file).isSymbolicLink()) { console.error(`refusing to write through a symlink: ${file}`); continue; } } catch { /* does not exist */ }
1129
+ external_node_fs_.writeFileSync(file, r.content);
1130
+ written.push(file);
1131
+ }
1132
+ return written;
1133
+ }
1134
+
1135
+ async function cmdDnr(args, root) {
1136
+ const sub = args._[1];
1137
+ const id = String(args._[2] ?? '');
1138
+ const flags = args.flags || {};
1139
+ if (!sub || !id) return usage();
1140
+
1141
+ if (sub === 'generate') {
1142
+ const r = ensureDetection(root, id, { ...flags, regenerate: true });
1143
+ if (!r.ok) { console.error(r.reason); return 4; }
1144
+ const { ir, validation } = r.saved;
1145
+ const want = flags.format ? String(flags.format) : null;
1146
+ if (want && want !== 'all' && !FORMATS.includes(want)) { console.error(`unknown format "${want}"; choose one of ${FORMATS.join(', ')}, or all`); return 4; }
1147
+ if (flags.json) { console.log(JSON.stringify({ ir, validation, outputs: want && want !== 'all' ? { [want]: renderDetection(ir, want) } : renderAll(ir) }, null, 2)); return validation.status === 'VALIDATED' ? 0 : 1; }
1148
+ console.log(`Detection ${ir.id} for ${ir.findingId}: ${ir.payloadClass} (${ir.grounding}, confidence ${ir.confidence})`);
1149
+ console.log(`Validation: ${validation.status} (${validation.counts.passed}/${validation.counts.positives + validation.counts.negatives} synthetic cases)${validation.status === 'VALIDATED' ? '; recurrence rule enabled' : '; NOT enabled'}`);
1150
+ for (const l of ir.limitations) console.log(` limitation: ${l}`);
1151
+ if (flags.out) { const w = writeOut(external_node_path_.resolve(String(flags.out)), id, ir, want && want !== 'all' ? [want] : FORMATS); console.log(`Wrote ${w.length} file(s) to ${external_node_path_.resolve(String(flags.out))}`); }
1152
+ else if (want && want !== 'all') { const o = renderDetection(ir, want); console.log(''); console.log(o.applicable ? o.content : `(${want} not applicable: ${o.reason})`); }
1153
+ else console.log(`Formats: ${FORMATS.filter((f) => renderDetection(ir, f).applicable).join(', ')} (use --format <name> to print one, --out <dir> to write them)`);
1154
+ return validation.status === 'VALIDATED' ? 0 : 1;
1155
+ }
1156
+
1157
+ if (sub === 'validate') {
1158
+ const rec = (0,dnr_store.latestRecord)(root, id, 'detection');
1159
+ if (!rec) { console.error(`No detection for ${id}. Run: dnr generate ${id}`); return 4; }
1160
+ const errs = validateDetectionIR(rec.record.ir);
1161
+ if (errs.length) { console.error(`The stored detection is invalid: ${errs.join('; ')}`); return 1; }
1162
+ const v = validateDetection(rec.record.ir);
1163
+ if (flags.json) console.log(JSON.stringify(v, null, 2));
1164
+ else { console.log(`Validation: ${v.status} (${v.counts.passed}/${v.cases.length} cases)`); for (const c of v.cases.filter((x) => !x.ok)) console.log(` FAILED: ${c.kind} "${c.name}" ${c.kind === 'positive' ? 'did not fire' : 'fired'}`); if (v.reason) console.log(` ${v.reason}`); }
1165
+ return v.status === 'VALIDATED' ? 0 : 1;
1166
+ }
1167
+
1168
+ if (sub === 'hunt') {
1169
+ if (!flags.logs) { console.error('dnr hunt needs --logs <file-or-directory>. Nothing was searched.'); return 4; }
1170
+ const det = ensureDetection(root, id, flags);
1171
+ if (!det.ok) { console.error(det.reason); return 4; }
1172
+ const ac = new AbortController();
1173
+ const onSig = () => ac.abort();
1174
+ process.once('SIGINT', onSig);
1175
+ let result;
1176
+ try { result = await huntLogs({ ir: det.saved.ir, logsPath: external_node_path_.resolve(String(flags.logs)), since: flags.since ?? null, until: flags.until ?? null, signal: ac.signal }); } finally { process.off('SIGINT', onSig); }
1177
+ const saved = (0,dnr_store.saveRecord)(root, id, 'hunt', result);
1178
+ if (flags.json) console.log(JSON.stringify({ ...result, stored: saved.ok ? saved.file : null }, null, 2));
1179
+ else {
1180
+ console.log(`Hunt: ${result.verdict}`);
1181
+ console.log(`Logs: ${result.logs.files} file(s), ${result.logs.parsed}/${result.logs.lines} lines parsed${result.window.start ? `, ${result.window.start.slice(0, 10)} to ${result.window.end.slice(0, 10)}` : ', no timestamps'}`);
1182
+ if (result.matches.count) console.log(`Matches: ${result.matches.count}${result.matches.firstSeen ? ` (first ${result.matches.firstSeen}, last ${result.matches.lastSeen})` : ''}`);
1183
+ for (const s of result.matches.samples.slice(0, 5)) console.log(` ${s.time ?? 'undated'} ${s.method} ${s.path} [${s.matchedOn}] ${s.valuePreview}`);
1184
+ for (const c of result.caveats) console.log(`Caveat: ${c}`);
1185
+ console.log(result.statement);
1186
+ if (!saved.ok) console.log(`(result not stored: ${saved.reason})`);
1187
+ }
1188
+ return HUNT_EXIT[result.verdict] ?? 11;
1189
+ }
1190
+
1191
+ if (sub === 'window') {
1192
+ if (!flags.deployments) { console.error('dnr window needs --deployments <file> (a JSON list of {commit, deployedAt, environment?}). Nothing was computed.'); return 4; }
1193
+ const finding = loadFindingFromLastScan(root, id);
1194
+ if (!finding) { console.error(`Finding ${id} not found in the last scan.`); return 4; }
1195
+ const { deployments, problems } = loadDeployments(external_node_path_.resolve(String(flags.deployments)), { environment: flags.environment ?? null });
1196
+ const hunt = (0,dnr_store.latestRecord)(root, id, 'hunt')?.record ?? null;
1197
+ const telemetry = flags['telemetry-start'] && flags['telemetry-end'] ? { start: flags['telemetry-start'], end: flags['telemetry-end'] } : hunt?.window ?? null;
1198
+ const w = computeExposureWindow({ finding, repo: root, deployments, deploymentProblems: problems, fixCommit: flags['fix-commit'] ?? null, telemetry });
1199
+ (0,dnr_store.saveRecord)(root, id, 'window', w);
1200
+ if (flags.json) console.log(JSON.stringify(w, null, 2)); else console.log(`${formatExposureWindow(w)}\n\n${w.statement}`);
1201
+ return w.status === 'complete' ? 0 : 1;
1202
+ }
1203
+
1204
+ if (sub === 'show') {
1205
+ const rec = (0,dnr_store.latestRecord)(root, id, 'detection');
1206
+ if (!rec) { console.error(`No detection for ${id}.`); return 4; }
1207
+ if ((0,dnr_store.listRecords)(root, id, 'detection').some((r) => r.tampered)) console.error('WARNING: a stored detection record failed its integrity check and was not used.');
1208
+ console.log(JSON.stringify(flags.json ? rec.record : { id: rec.record.ir.id, payloadClass: rec.record.ir.payloadClass, grounding: rec.record.ir.grounding, endpoint: rec.record.ir.endpoint, enabled: rec.record.enabled, validation: rec.record.validation.status, proof: rec.record.proof }, null, 2));
1209
+ return 0;
1210
+ }
1211
+
1212
+ if (sub === 'status') {
1213
+ const v = buildLifecycleView(root, id);
1214
+ if (flags.json) console.log(JSON.stringify(v, null, 2)); else console.log(formatLifecycleView(v));
1215
+ return 0;
1216
+ }
1217
+ return usage();
1218
+ }
1219
+
1220
+
1221
+ /***/ }),
1222
+
1223
+ /***/ 58551:
1224
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
1225
+
1226
+ __webpack_require__.r(__webpack_exports__);
1227
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1228
+ /* harmony export */ hasTamperedRecords: () => (/* binding */ hasTamperedRecords),
1229
+ /* harmony export */ latestRecord: () => (/* binding */ latestRecord),
1230
+ /* harmony export */ listRecords: () => (/* binding */ listRecords),
1231
+ /* harmony export */ saveRecord: () => (/* binding */ saveRecord)
1232
+ /* harmony export */ });
1233
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
1234
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(76760);
1235
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(31174);
1236
+ /* harmony import */ var _encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(39984);
1237
+ /* harmony import */ var _proof_fabric_reproducer_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(37055);
1238
+ /* harmony import */ var _proof_fabric_store_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(62199);
1239
+ // Persistence for the detection-and-response loop: `.agentic-security/dnr/<findingKey>/<kind>-<digest>.json`.
1240
+ // Same machinery and the same guarantees as the proof store, reused rather than copied where it is exported:
1241
+ // safe state-directory writes (no symlinks, no unsafe dirs), the encryption policy, retention as evidence, legal holds, and
1242
+ // a recursive secret scrub before anything touches disk. A record that no longer matches its own digest is returned FLAGGED,
1243
+ // never trusted and never silently dropped.
1244
+
1245
+
1246
+
1247
+
1248
+
1249
+
1250
+
1251
+ const DIR = 'dnr';
1252
+ const MAX_RECORD_BYTES = 1024 * 1024;
1253
+ const RECORD_KINDS = Object.freeze(['detection', 'hunt', 'window', 'fix']);
1254
+
1255
+ 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)); };
1256
+
1257
+ function saveRecord(root, findingId, kind, record) {
1258
+ if (!RECORD_KINDS.includes(kind)) return { ok: false, reason: `unknown record kind: ${kind}` };
1259
+ const safe = (0,_proof_fabric_store_js__WEBPACK_IMPORTED_MODULE_5__.scrubDeep)(record);
1260
+ safe.schema = `agentic-security/dnr-${kind}-record`;
1261
+ safe.kind = kind;
1262
+ safe.findingId = String(findingId);
1263
+ safe.savedAt = record.savedAt ?? new Date().toISOString();
1264
+ safe.recordDigest = digestOf(safe);
1265
+ const text = JSON.stringify(safe, null, 2);
1266
+ if (Buffer.byteLength(text) > MAX_RECORD_BYTES) return { ok: false, reason: 'record exceeds the size limit' };
1267
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.ensureStateDir)(root)) return { ok: false, reason: 'no safe state directory for this project' };
1268
+ const enc = (0,_encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__/* .maybeEncryptForWrite */ .v2)(root, DIR, text);
1269
+ if (!enc.ok) return { ok: false, reason: enc.reason };
1270
+ 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`);
1271
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.safeWriteState)(file, enc.content)) return { ok: false, reason: 'state write refused' };
1272
+ return { ok: true, file, digest: safe.recordDigest, encrypted: enc.encrypted };
1273
+ }
1274
+
1275
+ /** Newest-first records of one kind for a finding. Tampered records are flagged. */
1276
+ function listRecords(root, findingId, kind) {
1277
+ const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(root, DIR, (0,_proof_fabric_store_js__WEBPACK_IMPORTED_MODULE_5__.findingKey)(findingId));
1278
+ let names = [];
1279
+ try { names = node_fs__WEBPACK_IMPORTED_MODULE_0__.readdirSync(dir).filter((n) => n.startsWith(`${kind}-`) && n.endsWith('.json')); } catch { return []; }
1280
+ const out = [];
1281
+ for (const n of names) {
1282
+ const file = node_path__WEBPACK_IMPORTED_MODULE_1__.join(dir, n);
1283
+ try {
1284
+ const rec = JSON.parse((0,_encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__.maybeDecryptForRead)(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(file, 'utf8')));
1285
+ out.push({ file, record: rec, tampered: rec.recordDigest !== digestOf(rec), mtime: node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(file).mtimeMs });
1286
+ } catch { out.push({ file, record: null, tampered: true, mtime: 0 }); }
1287
+ }
1288
+ return out.sort((a, b) => b.mtime - a.mtime);
1289
+ }
1290
+
1291
+ /** Newest intact record of a kind, or null. A tampered newest record does NOT fall through silently: the caller can see it via listRecords. */
1292
+ function latestRecord(root, findingId, kind) {
1293
+ return listRecords(root, findingId, kind).find((r) => r.record && !r.tampered) ?? null;
1294
+ }
1295
+ const hasTamperedRecords = (root, findingId, kind) => listRecords(root, findingId, kind).some((r) => r.tampered);
1296
+
1297
+
1298
+ /***/ }),
1299
+
1300
+ /***/ 12090:
1301
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
1302
+
1303
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1304
+ /* harmony export */ KF: () => (/* binding */ toProofTier),
1305
+ /* harmony export */ g3: () => (/* binding */ stateRank),
1306
+ /* harmony export */ jn: () => (/* binding */ fromProofTier),
1307
+ /* harmony export */ np: () => (/* binding */ advance)
1308
+ /* harmony export */ });
1309
+ /* unused harmony exports EVIDENCE_STATES, COMPLETENESS */
1310
+ // Evidence state ladder for the Universal Exploit Proof Fabric.
1311
+ //
1312
+ // Evidence state is SEPARATE from severity and confidence. It extends, never replaces, the vocabulary that already
1313
+ // exists: `proof-tier.js` (execution-proven / proof-failed / taint-proven / unproven) and the fix-verify legs
1314
+ // (`verifiedFull`, `degradedLegs`). `toProofTier` / `fromProofTier` are the explicit bridges.
1315
+ //
1316
+ // Transitions are guarded: a state can only be reached from the state directly before it (or by dropping back), so no
1317
+ // caller can jump a finding to EXPLOIT_CONFIRMED without passing REPRODUCED, or to FIX_VERIFIED without
1318
+ // FIX_REPLAY_BLOCKED. A degraded or incomplete run NEVER advances a state.
1319
+ const EVIDENCE_STATES = Object.freeze([
1320
+ 'CANDIDATE',
1321
+ 'STATICALLY_CORROBORATED',
1322
+ 'REPRODUCER_GENERATED',
1323
+ 'REPRODUCED',
1324
+ 'EXPLOIT_CONFIRMED',
1325
+ 'FIX_REPLAY_BLOCKED',
1326
+ 'FIX_VERIFIED',
1327
+ ]);
1328
+
1329
+ const stateRank = (s) => EVIDENCE_STATES.indexOf(s);
1330
+
1331
+ const REQUIRES_PREDECESSOR = Object.freeze(Object.fromEntries(EVIDENCE_STATES.map((s, i) => [s, i === 0 ? null : EVIDENCE_STATES[i - 1]])));
1332
+
1333
+ /**
1334
+ * Advance `from` to `to`. Returns { ok, state, reason }. Moving up requires `to` to be exactly the next rung; moving
1335
+ * down or staying is always allowed (evidence can be lost, never invented). Unknown states are rejected.
1336
+ */
1337
+ function advance(from, to) {
1338
+ if (!EVIDENCE_STATES.includes(from)) return { ok: false, state: 'CANDIDATE', reason: `unknown current state: ${String(from)}` };
1339
+ if (!EVIDENCE_STATES.includes(to)) return { ok: false, state: from, reason: `unknown target state: ${String(to)}` };
1340
+ if (stateRank(to) <= stateRank(from)) return { ok: true, state: to, reason: null };
1341
+ if (REQUIRES_PREDECESSOR[to] !== from) return { ok: false, state: from, reason: `cannot reach ${to} from ${from}: ${REQUIRES_PREDECESSOR[to]} is required first` };
1342
+ return { ok: true, state: to, reason: null };
1343
+ }
1344
+
1345
+ // Bridges to the older vocabulary so existing consumers keep working.
1346
+ function toProofTier(state) {
1347
+ const r = stateRank(state);
1348
+ if (r >= stateRank('EXPLOIT_CONFIRMED')) return 'execution-proven';
1349
+ if (r >= stateRank('STATICALLY_CORROBORATED')) return 'taint-proven';
1350
+ return 'unproven';
1351
+ }
1352
+
1353
+ function fromProofTier(tier) {
1354
+ if (tier === 'taint-proven') return 'STATICALLY_CORROBORATED';
1355
+ // execution-proven under the older single-run marker path is one run, not the 3/3 + independent replay the fabric
1356
+ // requires, so it maps to REPRODUCED and never to EXPLOIT_CONFIRMED.
1357
+ if (tier === 'execution-proven') return 'REPRODUCED';
1358
+ return 'CANDIDATE';
1359
+ }
1360
+
1361
+ // Completeness vocabulary shared with fix-verify: FULL only when nothing was skipped.
1362
+ const COMPLETENESS = Object.freeze(['FULL', 'DEGRADED', 'INCOMPLETE']);
1363
+
1364
+
1365
+ /***/ }),
1366
+
1367
+ /***/ 62199:
1368
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
1369
+
1370
+ __webpack_require__.r(__webpack_exports__);
1371
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1372
+ /* harmony export */ findingKey: () => (/* binding */ findingKey),
1373
+ /* harmony export */ latestProof: () => (/* binding */ latestProof),
1374
+ /* harmony export */ listProofs: () => (/* binding */ listProofs),
1375
+ /* harmony export */ saveProof: () => (/* binding */ saveProof),
1376
+ /* harmony export */ scrubDeep: () => (/* binding */ scrubDeep)
1377
+ /* harmony export */ });
1378
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
1379
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(76760);
1380
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(31174);
1381
+ /* harmony import */ var _encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(39984);
1382
+ /* harmony import */ var _llm_validator_redact_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(65388);
1383
+ /* harmony import */ var _reproducer_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(37055);
1384
+ // Proof artifact store: `.agentic-security/proofs/<findingKey>/<recordDigest>.json`.
1385
+ //
1386
+ // Reuses the existing state machinery rather than adding another: `safeWriteState` (refuses unsafe dirs and symlinks),
1387
+ // `maybeEncryptForWrite` / `maybeDecryptForRead` (the `proofs` artifact is registered `confidential`), retention
1388
+ // (evidence class) and legal holds (a hold on `proofs` blocks purge). Nothing persisted contains a plaintext credential:
1389
+ // records are passed through a recursive secret filter first, and the runner never returns its per-run nonce or secrets.
1390
+
1391
+
1392
+
1393
+
1394
+
1395
+
1396
+
1397
+ const PROOF_RECORD_SCHEMA = 'agentic-security/proof-record';
1398
+ const PROOFS_DIR = 'proofs';
1399
+ const MAX_RECORD_BYTES = 512 * 1024;
1400
+
1401
+ 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)}`;
1402
+
1403
+ // The shared redactor catches key=value shapes; bare tokens need their own pass, so persisted evidence cannot carry one.
1404
+ const BARE_SECRET_RES = [
1405
+ /\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,
1406
+ /\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,
1407
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, /\bSECRET-[0-9a-f]{24}-[a-z0-9_-]+\b/g,
1408
+ ];
1409
+ function scrubString(text) {
1410
+ let t = String(text);
1411
+ try { t = (0,_llm_validator_redact_js__WEBPACK_IMPORTED_MODULE_4__/* .redactSecrets */ .f)(t).text; } catch { return '[unredactable]'; }
1412
+ for (const re of BARE_SECRET_RES) t = t.replace(re, '[REDACTED-SECRET]');
1413
+ return t;
1414
+ }
1415
+
1416
+ /** Deep-copy `v` with every string passed through the secret redactor. */
1417
+ function scrubDeep(v) {
1418
+ if (typeof v === 'string') return scrubString(v);
1419
+ if (Array.isArray(v)) return v.map(scrubDeep);
1420
+ if (v && typeof v === 'object') return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, scrubDeep(x)]));
1421
+ return v;
1422
+ }
1423
+
1424
+ function recordDigest(record) {
1425
+ const { recordDigest: _omit, ...rest } = record;
1426
+ return (0,_reproducer_js__WEBPACK_IMPORTED_MODULE_5__/* .sha256 */ .sc)((0,_reproducer_js__WEBPACK_IMPORTED_MODULE_5__/* .canonicalize */ .dw)(rest));
1427
+ }
1428
+
1429
+ function saveProof(scanRoot, record) {
1430
+ const safe = scrubDeep(record);
1431
+ safe.schema = PROOF_RECORD_SCHEMA;
1432
+ safe.recordDigest = recordDigest(safe);
1433
+ const text = JSON.stringify(safe, null, 2);
1434
+ if (Buffer.byteLength(text) > MAX_RECORD_BYTES) return { ok: false, reason: 'proof record exceeds the size limit' };
1435
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.ensureStateDir)(scanRoot)) return { ok: false, reason: 'no safe state directory for this project' };
1436
+ const enc = (0,_encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__/* .maybeEncryptForWrite */ .v2)(scanRoot, PROOFS_DIR, text);
1437
+ if (!enc.ok) return { ok: false, reason: enc.reason };
1438
+ const file = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(scanRoot, PROOFS_DIR, findingKey(record.findingId), `${safe.recordDigest}.json`);
1439
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.safeWriteState)(file, enc.content)) return { ok: false, reason: 'state write refused' };
1440
+ return { ok: true, file, digest: safe.recordDigest, encrypted: enc.encrypted };
1441
+ }
1442
+
1443
+ function readRecord(file) {
1444
+ const text = (0,_encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__.maybeDecryptForRead)(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(file, 'utf8'));
1445
+ const rec = JSON.parse(text);
1446
+ // A record whose content no longer matches its own digest was modified after it was written.
1447
+ if (rec.recordDigest !== recordDigest(rec)) return { tampered: true, record: rec };
1448
+ return { tampered: false, record: rec };
1449
+ }
1450
+
1451
+ /** Newest-first records for a finding. Tampered records are returned flagged, never silently dropped or trusted. */
1452
+ function listProofs(scanRoot, findingId) {
1453
+ const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(scanRoot, PROOFS_DIR, findingKey(findingId));
1454
+ let names = [];
1455
+ try { names = node_fs__WEBPACK_IMPORTED_MODULE_0__.readdirSync(dir).filter((n) => n.endsWith('.json')); } catch { return []; }
1456
+ const out = [];
1457
+ for (const n of names) {
1458
+ const f = node_path__WEBPACK_IMPORTED_MODULE_1__.join(dir, n);
1459
+ 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 }); }
1460
+ }
1461
+ return out.sort((a, b) => b.mtime - a.mtime);
1462
+ }
1463
+
1464
+ function latestProof(scanRoot, findingId) {
1465
+ return listProofs(scanRoot, findingId).find((p) => p.record) ?? null;
1466
+ }
1467
+
1468
+
1469
+ /***/ })
1470
+
1471
+ };