@clear-capabilities/agentic-security-scanner 0.172.1 → 0.173.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/bin/agentic-security.js +37 -0
  3. package/dist/119.index.js +197 -0
  4. package/dist/128.index.js +129 -0
  5. package/dist/1293.index.js +5689 -0
  6. package/dist/1351.index.js +133 -0
  7. package/dist/1387.index.js +271 -0
  8. package/dist/1437.index.js +23 -0
  9. package/dist/1445.index.js +110 -0
  10. package/dist/1505.index.js +151 -0
  11. package/dist/1557.index.js +293 -0
  12. package/dist/1621.index.js +311 -0
  13. package/dist/1751.index.js +160 -0
  14. package/dist/1765.index.js +727 -0
  15. package/dist/1861.index.js +690 -0
  16. package/dist/1898.index.js +116 -0
  17. package/dist/1906.index.js +433 -0
  18. package/dist/1920.index.js +1 -291
  19. package/dist/208.index.js +1631 -0
  20. package/dist/2199.index.js +0 -239
  21. package/dist/2223.index.js +159 -0
  22. package/dist/2530.index.js +152 -0
  23. package/dist/2565.index.js +116 -0
  24. package/dist/2574.index.js +127 -0
  25. package/dist/2620.index.js +345 -0
  26. package/dist/2773.index.js +98 -0
  27. package/dist/2775.index.js +32 -0
  28. package/dist/2966.index.js +171 -0
  29. package/dist/2972.index.js +87 -0
  30. package/dist/2991.index.js +131 -0
  31. package/dist/3063.index.js +833 -0
  32. package/dist/3075.index.js +227 -0
  33. package/dist/3121.index.js +297 -0
  34. package/dist/3314.index.js +263 -0
  35. package/dist/3480.index.js +1822 -0
  36. package/dist/3481.index.js +24 -0
  37. package/dist/3506.index.js +268 -0
  38. package/dist/3533.index.js +591 -0
  39. package/dist/3653.index.js +450 -0
  40. package/dist/3822.index.js +131 -0
  41. package/dist/3868.index.js +292 -0
  42. package/dist/3917.index.js +152 -0
  43. package/dist/3918.index.js +192 -0
  44. package/dist/4090.index.js +288 -0
  45. package/dist/4121.index.js +1190 -0
  46. package/dist/4142.index.js +0 -239
  47. package/dist/4336.index.js +436 -0
  48. package/dist/4387.index.js +532 -0
  49. package/dist/4413.index.js +302 -0
  50. package/dist/4476.index.js +4510 -0
  51. package/dist/4491.index.js +133 -0
  52. package/dist/4538.index.js +185 -0
  53. package/dist/4684.index.js +319 -0
  54. package/dist/4740.index.js +165 -0
  55. package/dist/4780.index.js +209 -0
  56. package/dist/4802.index.js +225 -0
  57. package/dist/4812.index.js +379 -0
  58. package/dist/4835.index.js +126 -0
  59. package/dist/494.index.js +226 -0
  60. package/dist/4991.index.js +549 -0
  61. package/dist/5199.index.js +305 -0
  62. package/dist/5216.index.js +109 -0
  63. package/dist/5439.index.js +301 -0
  64. package/dist/5443.index.js +97 -0
  65. package/dist/5496.index.js +377 -0
  66. package/dist/550.index.js +2 -1
  67. package/dist/5801.index.js +588 -0
  68. package/dist/5858.index.js +223 -0
  69. package/dist/5933.index.js +143 -0
  70. package/dist/601.index.js +23 -0
  71. package/dist/6173.index.js +273 -0
  72. package/dist/6291.index.js +1871 -0
  73. package/dist/6312.index.js +666 -0
  74. package/dist/634.index.js +16182 -0
  75. package/dist/644.index.js +87 -0
  76. package/dist/6508.index.js +283 -0
  77. package/dist/6560.index.js +645 -0
  78. package/dist/6576.index.js +422 -0
  79. package/dist/6579.index.js +271 -0
  80. package/dist/6745.index.js +305 -0
  81. package/dist/6818.index.js +21642 -0
  82. package/dist/6972.index.js +167 -0
  83. package/dist/6995.index.js +163 -0
  84. package/dist/7249.index.js +191 -0
  85. package/dist/7372.index.js +1197 -0
  86. package/dist/7546.index.js +541 -0
  87. package/dist/76.index.js +448 -0
  88. package/dist/7879.index.js +163 -0
  89. package/dist/8025.index.js +130 -0
  90. package/dist/8054.index.js +248 -0
  91. package/dist/8097.index.js +201 -0
  92. package/dist/8126.index.js +191 -0
  93. package/dist/8178.index.js +78 -0
  94. package/dist/818.index.js +212 -0
  95. package/dist/8382.index.js +93 -0
  96. package/dist/8408.index.js +261 -0
  97. package/dist/8540.index.js +174 -0
  98. package/dist/8635.index.js +802 -0
  99. package/dist/8688.index.js +410 -0
  100. package/dist/8699.index.js +779 -0
  101. package/dist/8891.index.js +457 -0
  102. package/dist/8916.index.js +885 -0
  103. package/dist/9146.index.js +170 -0
  104. package/dist/9168.index.js +841 -0
  105. package/dist/9292.index.js +369 -0
  106. package/dist/9382.index.js +135 -11
  107. package/dist/9422.index.js +866 -0
  108. package/dist/9440.index.js +453 -0
  109. package/dist/9479.index.js +152 -0
  110. package/dist/9481.index.js +220 -0
  111. package/dist/9539.index.js +100 -0
  112. package/dist/9547.index.js +103 -0
  113. package/dist/9560.index.js +41 -61
  114. package/dist/9692.index.js +90 -0
  115. package/dist/9800.index.js +213 -1
  116. package/dist/9899.index.js +138 -0
  117. package/dist/9981.index.js +206 -0
  118. package/dist/999.index.js +117 -0
  119. package/dist/agentic-security.mjs +27 -27
  120. package/dist/agentic-security.mjs.sha256 +1 -1
  121. package/package.json +28 -3
  122. package/src/context-intelligence/candidates.js +2 -1
  123. package/src/context-intelligence/capabilities.js +2 -1
  124. package/src/context-intelligence/cli.js +2 -0
  125. package/src/context-intelligence/config.js +3 -1
  126. package/src/context-intelligence/controls.js +49 -1
  127. package/src/context-intelligence/counterexample.js +200 -0
  128. package/src/context-intelligence/cross-route.js +111 -0
  129. package/src/context-intelligence/findings.js +166 -0
  130. package/src/context-intelligence/invariants.js +197 -0
  131. package/src/context-intelligence/queue.js +6 -1
  132. package/src/context-intelligence/reachability.js +2 -2
  133. package/src/context-intelligence/review.js +68 -4
  134. package/src/dataflow/access-paths.js +11 -0
  135. package/src/dataflow/analysis-limits.js +66 -0
  136. package/src/dataflow/engine.js +272 -20
  137. package/src/dataflow/service-edges.js +174 -0
  138. package/src/dataflow/summaries.js +7 -1
  139. package/src/discovery/judge.js +2 -1
  140. package/src/engine.js +82 -14
  141. package/src/fix/apply-fix-service.js +55 -8
  142. package/src/fix/batch-journal.js +84 -0
  143. package/src/fix/exploit-replay.js +51 -0
  144. package/src/fix/footprint.js +65 -0
  145. package/src/fix/patch-policy.js +49 -0
  146. package/src/fix/secret-persistence.js +39 -0
  147. package/src/fix/test-commands.js +84 -0
  148. package/src/fix/verification-adapters.js +97 -0
  149. package/src/fix/verification-record.js +243 -0
  150. package/src/fix/verify-patch.js +89 -0
  151. package/src/ir/CLAUDE.md +5 -0
  152. package/src/ir/callgraph.js +23 -1
  153. package/src/ir/module-resolver.js +315 -0
  154. package/src/ir/parser-js.js +21 -2
  155. package/src/ir/semantic-graph.js +245 -0
  156. package/src/lineage/build-attested-correlation.js +63 -0
  157. package/src/lineage/deployment-identity.js +76 -0
  158. package/src/mcp/tools.js +13 -34
  159. package/src/pipeline/canonical-output.js +64 -0
  160. package/src/pipeline/detector-runner.js +7 -0
  161. package/src/pipeline/scan-health.js +14 -0
  162. package/src/pipeline/supervised-analyzers.js +125 -0
  163. package/src/pipeline/supervised-child.js +17 -0
  164. package/src/pipeline/telemetry.js +79 -0
  165. package/src/posture/accuracy-scorecard.js +98 -9
  166. package/src/posture/artifact-registry.js +5 -0
  167. package/src/posture/assurance/evidence-registry.js +215 -0
  168. package/src/posture/assurance/release-package-verify.js +135 -0
  169. package/src/posture/assurance/release-package.js +118 -0
  170. package/src/posture/assurance/release-verdict.js +94 -0
  171. package/src/posture/assurance/waiver.js +200 -0
  172. package/src/posture/compliance-truth.js +100 -0
  173. package/src/posture/evaluation/scan-child.js +3 -1
  174. package/src/posture/evaluation/why-missed.js +3 -1
  175. package/src/posture/exploitability-evidence.js +141 -0
  176. package/src/posture/fix-verify.js +4 -2
  177. package/src/posture/fleet/exposure-ranking.js +115 -0
  178. package/src/posture/fleet/service-chain-graph.js +150 -0
  179. package/src/posture/graph-reconciliation.js +154 -0
  180. package/src/posture/provenance/coverage.js +103 -0
  181. package/src/posture/provenance/git-evidence.js +84 -0
  182. package/src/posture/provenance/origin-resolver.js +7 -1
  183. package/src/posture/provenance/predicate-replay.js +14 -4
  184. package/src/posture/runtime-correlation.js +61 -15
  185. package/src/posture/runtime-explanation.js +116 -0
  186. package/src/posture/runtime-trace-ingest.js +194 -0
  187. package/src/posture/scan-checkpoint.js +38 -10
  188. package/src/posture/waf-ingest.js +7 -2
  189. package/src/report/index.js +4 -0
  190. package/src/report/oscal.js +38 -0
  191. package/src/report/perspectives.js +150 -0
  192. package/src/sast/_comment-strip.js +8 -1
  193. package/src/sast/ruby.js +26 -0
  194. package/src/util/string-memo.js +38 -0
@@ -0,0 +1,227 @@
1
+ export const id = 3075;
2
+ export const ids = [3075];
3
+ export const modules = {
4
+
5
+ /***/ 53075:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ computeDelta: () => (/* binding */ computeDelta),
10
+ /* harmony export */ persistStatus: () => (/* binding */ persistStatus),
11
+ /* harmony export */ renderStatusLine: () => (/* binding */ renderStatusLine),
12
+ /* harmony export */ watchProject: () => (/* binding */ watchProject)
13
+ /* harmony export */ });
14
+ /* unused harmony exports readStatus, _internals */
15
+ /* harmony import */ var node_fs_promises__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(51455);
16
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(73024);
17
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
18
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(69259);
19
+ /* harmony import */ var _language_discovery_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(41928);
20
+ // Watch mode — continuous incremental scan as the developer edits.
21
+ //
22
+ // Spawns a long-running scan watcher that:
23
+ // 1. Subscribes to file-system events under the project root
24
+ // 2. When a file matching the scan glob changes, re-scans incrementally
25
+ // using dataflow/incremental-cache.js (per-file cache hits avoid the
26
+ // full IR build)
27
+ // 3. Diffs against the prior scan to compute a "risk delta" string
28
+ // (added/removed/changed criticals + highs)
29
+ // 4. Writes the delta to .agentic-security/watch-status.{md,json}
30
+ //
31
+ // The Claude Code statusline / a chat command can poll watch-status.md
32
+ // (cheap file read) to surface the delta inline without re-running anything.
33
+ //
34
+ // Implementation is deliberately node-only — no chokidar dep, uses
35
+ // fs.promises.watch (Node ≥ 20). Debounces to 350ms.
36
+ //
37
+ // Lifecycle: start() returns an AbortController-like handle. The caller
38
+ // (a /watch slash command or a long-running daemon spawn) is responsible
39
+ // for keeping the process alive; this module is the pure logic.
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+ const STATUS_MD = 'watch-status.md';
48
+ const STATUS_JSON = 'watch-status.json';
49
+ const DEBOUNCE_MS = 350;
50
+ const MAX_BURST = 50; // ignore beyond this # of rapid events
51
+
52
+ const SCAN_EXT_RE = /\.(?:[jt]sx?|mjs|cjs|py|java|kt|go|rb|php|cs|c|cc|cpp|h|hpp|rs|sol|vy|swift|dart|toml|yml|yaml|json|tf|tfvars|bicep)$/i;
53
+ const IGNORE_DIR_RE = /(?:^|\/)(?:\.git|node_modules|\.bench-cache|dist|build|\.next|coverage|\.agentic-security)(?:$|\/)/;
54
+
55
+ function _isScanable(rel) {
56
+ if (!rel) return false;
57
+ // An explicit export is evidence even though it lives in a build directory.
58
+ if ((0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isExplicitExport */ .Zh)(rel)) return true;
59
+ if (IGNORE_DIR_RE.test(rel)) return false;
60
+ if ((0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isLanguageSource */ .Sv)(rel) || (0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isLanguageManifest */ .mD)(rel)) return !(0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isLanguageExcludedPath */ .EG)(rel);
61
+ return SCAN_EXT_RE.test(rel);
62
+ }
63
+
64
+ /**
65
+ * Widen a batch of changed absolute paths to every file whose results the
66
+ * change can stale: dependents through imports, and all Haskell/Nix sources for
67
+ * a manifest, lockfile or export change. Batches with no Haskell/Nix path are
68
+ * returned untouched, so other languages never pay for a tree read.
69
+ */
70
+ async function expandChangedBatch(scanRoot, batch) {
71
+ const rels = batch.map((p) => node_path__WEBPACK_IMPORTED_MODULE_2__.relative(scanRoot, p).split(node_path__WEBPACK_IMPORTED_MODULE_2__.sep).join('/'));
72
+ if (!rels.some((r) => (0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isLanguageSource */ .Sv)(r) || (0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isLanguageManifest */ .mD)(r) || (0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isExplicitExport */ .Zh)(r))) return batch;
73
+ try {
74
+ const { readTree } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 58307));
75
+ const { fileContents, depFileContents } = await readTree(scanRoot);
76
+ const hit = (0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .impactedFiles */ .nn)(fileContents, depFileContents, rels);
77
+ const out = new Set(batch);
78
+ for (const r of hit) out.add(node_path__WEBPACK_IMPORTED_MODULE_2__.join(scanRoot, r));
79
+ return [...out];
80
+ } catch {
81
+ return batch;
82
+ }
83
+ }
84
+
85
+ function _readJsonSafe(fp) {
86
+ try { return JSON.parse(fsSync.readFileSync(fp, 'utf8')); } catch { return null; }
87
+ }
88
+
89
+ function _sevRank(s) { return ['info', 'low', 'medium', 'high', 'critical'].indexOf(s) + 1; }
90
+
91
+ /**
92
+ * Pure delta computation between two finding arrays. Same logic
93
+ * baseline-compare uses, surfaced as a single ASCII status line.
94
+ */
95
+ function computeDelta(prevFindings, currFindings) {
96
+ const key = (f) => `${f.file || ''}::${f.line || 0}::${f.family || f.parser || ''}`;
97
+ const prev = new Map();
98
+ for (const f of prevFindings || []) prev.set(key(f), f);
99
+ const cur = new Map();
100
+ for (const f of currFindings || []) cur.set(key(f), f);
101
+ const added = [], removed = [];
102
+ for (const [k, f] of cur) if (!prev.has(k)) added.push(f);
103
+ for (const [k, f] of prev) if (!cur.has(k)) removed.push(f);
104
+ const newCrit = added.filter(f => f.severity === 'critical').length;
105
+ const newHigh = added.filter(f => f.severity === 'high').length;
106
+ const fixedCrit = removed.filter(f => f.severity === 'critical').length;
107
+ const fixedHigh = removed.filter(f => f.severity === 'high').length;
108
+ return {
109
+ addedCount: added.length, removedCount: removed.length,
110
+ newCritical: newCrit, newHigh, fixedCritical: fixedCrit, fixedHigh,
111
+ added, removed,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Render a one-line status string for the Claude Code statusline.
117
+ */
118
+ function renderStatusLine(delta) {
119
+ const parts = [];
120
+ if (delta.newCritical) parts.push(`🛑 +${delta.newCritical} crit`);
121
+ if (delta.newHigh) parts.push(`⚠️ +${delta.newHigh} high`);
122
+ if (delta.fixedCritical) parts.push(`✅ -${delta.fixedCritical} crit`);
123
+ if (delta.fixedHigh) parts.push(`✅ -${delta.fixedHigh} high`);
124
+ if (!parts.length && (delta.addedCount + delta.removedCount) === 0) return 'agentic-security: clean';
125
+ if (!parts.length) return `agentic-security: +${delta.addedCount} / -${delta.removedCount}`;
126
+ return 'agentic-security: ' + parts.join(' · ');
127
+ }
128
+
129
+ /**
130
+ * Persist watch-status.{md,json}. Cheap atomic write (write tmp, rename).
131
+ */
132
+ function persistStatus(scanRoot, delta) {
133
+ const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_3__/* .stateDir */ .Pn)(scanRoot);
134
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_3__.stateWritesEnabled)()) return;
135
+ try { node_fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(dir, { recursive: true }); } catch {}
136
+ const status = {
137
+ ts: new Date().toISOString(),
138
+ line: renderStatusLine(delta),
139
+ delta: {
140
+ addedCount: delta.addedCount, removedCount: delta.removedCount,
141
+ newCritical: delta.newCritical, newHigh: delta.newHigh,
142
+ fixedCritical: delta.fixedCritical, fixedHigh: delta.fixedHigh,
143
+ },
144
+ addedTop5: (delta.added || []).slice(0, 5).map(f => ({
145
+ file: f.file, line: f.line, family: f.family, severity: f.severity, vuln: f.vuln,
146
+ })),
147
+ };
148
+ const jsonPath = node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, STATUS_JSON);
149
+ const mdPath = node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, STATUS_MD);
150
+ try { node_fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(jsonPath, JSON.stringify(status, null, 2)); } catch {}
151
+ const md = [
152
+ `# Watch status — ${status.ts.slice(11, 19)} UTC`,
153
+ '',
154
+ status.line,
155
+ '',
156
+ ];
157
+ if (status.addedTop5.length) {
158
+ md.push('## New findings');
159
+ for (const f of status.addedTop5) {
160
+ md.push(`- **[${(f.severity || '?').toUpperCase()}]** ${f.vuln || f.family || 'finding'} — \`${f.file}:${f.line}\``);
161
+ }
162
+ }
163
+ try { node_fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(mdPath, md.join('\n')); } catch {}
164
+ return status;
165
+ }
166
+
167
+ /**
168
+ * Read the latest watch-status (returns null if none).
169
+ */
170
+ function readStatus(scanRoot) {
171
+ return _readJsonSafe(statePath(scanRoot, STATUS_JSON));
172
+ }
173
+
174
+ /**
175
+ * Subscribe to FS events and call onChange(absPath, eventType) for each
176
+ * matching event. Debounces bursts. Returns a controller with stop().
177
+ *
178
+ * The actual incremental scan call lives in the caller — this module
179
+ * stays pure for testability.
180
+ */
181
+ async function watchProject(scanRoot, onChange, opts = {}) {
182
+ if (process.env.AGENTIC_SECURITY_NO_WATCH === '1') return { stop: async () => {}, _disabled: true };
183
+ const recursive = opts.recursive !== false;
184
+ const ac = new AbortController();
185
+ let timer = null;
186
+ const pending = new Set();
187
+ const flush = () => {
188
+ timer = null;
189
+ if (pending.size > MAX_BURST) { pending.clear(); return; }
190
+ const batch = Array.from(pending);
191
+ pending.clear();
192
+ const needsExpansion = batch.some((p) => {
193
+ const r = node_path__WEBPACK_IMPORTED_MODULE_2__.relative(scanRoot, p).split(node_path__WEBPACK_IMPORTED_MODULE_2__.sep).join('/');
194
+ return (0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isLanguageSource */ .Sv)(r) || (0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isLanguageManifest */ .mD)(r) || (0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isExplicitExport */ .Zh)(r);
195
+ });
196
+ if (!needsExpansion) { try { onChange(batch); } catch {} return; }
197
+ expandChangedBatch(scanRoot, batch).then((b) => { try { onChange(b); } catch {} });
198
+ };
199
+ let stopped = false;
200
+ (async () => {
201
+ try {
202
+ for await (const evt of node_fs_promises__WEBPACK_IMPORTED_MODULE_0__.watch(scanRoot, { recursive, signal: ac.signal })) {
203
+ if (stopped) break;
204
+ const rel = String(evt.filename || '').replace(/\\/g, '/');
205
+ if (!_isScanable(rel)) continue;
206
+ pending.add(node_path__WEBPACK_IMPORTED_MODULE_2__.join(scanRoot, rel));
207
+ if (timer) clearTimeout(timer);
208
+ timer = setTimeout(flush, DEBOUNCE_MS);
209
+ }
210
+ } catch (e) {
211
+ if (e && e.name !== 'AbortError') {
212
+ // Surface but don't crash — caller decides how to handle.
213
+ try { onChange([], e); } catch {}
214
+ }
215
+ }
216
+ })();
217
+ return {
218
+ stop: async () => { stopped = true; ac.abort(); if (timer) clearTimeout(timer); },
219
+ };
220
+ }
221
+
222
+ const _internals = { _isScanable, expandChangedBatch, SCAN_EXT_RE, IGNORE_DIR_RE };
223
+
224
+
225
+ /***/ })
226
+
227
+ };
@@ -0,0 +1,297 @@
1
+ export const id = 3121;
2
+ export const ids = [3121];
3
+ export const modules = {
4
+
5
+ /***/ 83121:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ __webpack_require__.r(__webpack_exports__);
9
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
10
+ /* harmony export */ evaluateDriftPolicies: () => (/* binding */ evaluateDriftPolicies),
11
+ /* harmony export */ loadDriftPolicies: () => (/* binding */ loadDriftPolicies)
12
+ /* harmony export */ });
13
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
14
+ // drift-policy.js — M4 deliverable #8, sub-project 8b, Task 2: a
15
+ // before/after drift-policy DSL evaluated against a GraphDiff (Task 1's
16
+ // own output, see graph-diff.js).
17
+ //
18
+ // Mirrors dataflow/privacy-sink-policy.js's fail-closed axis-matching
19
+ // STYLE exactly — the `_matchesEnvironment`/`_matchesDestination`
20
+ // precedent (an unset rule field is unconstrained; a set field the
21
+ // current context has no comparable value for does NOT match) — never
22
+ // its literal single-graph-state shape. That module evaluates a static
23
+ // state ("is this (class, sink) pair permitted right now"); this module
24
+ // evaluates a TRANSITION ("did this (class, sink) pair newly start
25
+ // flowing, or did this flow's own verdict regress"), which is why its
26
+ // rule shape is `{trigger, ...}`-keyed rather than `{allow: [...]}`.
27
+ //
28
+ // ── Design judgment calls made in this file (disclosed, not hidden) ──
29
+ //
30
+ // 1. `loadDriftPolicies(policyFilePath)` takes a literal file path, not
31
+ // a scanRoot — a deliberate, task-brief-directed divergence from
32
+ // `loadPrivacySinkPolicy(scanRoot)`'s own scanRoot-relative
33
+ // convention (`statePath(scanRoot, 'privacy-policy.json')`). Drift
34
+ // policies are a new artifact with no established scanRoot-relative
35
+ // home yet (Task 3's CLI wiring is the only real caller, and it can
36
+ // resolve whatever path convention it wants — e.g. a `--policy`
37
+ // flag, or a default under `.agentic-security/` — without this
38
+ // module forcing that choice). Keeping the signature a plain path
39
+ // keeps this module honest about being a pure evaluator with no
40
+ // opinion on where its config file lives.
41
+ //
42
+ // 2. The rule vocabulary generalizes `fromPolicyVerdict`/
43
+ // `toPolicyVerdict` to a second, structurally identical pair,
44
+ // `fromProtectionSummary`/`toProtectionSummary`, rather than adding a
45
+ // generic `field`/`from`/`to` triple. The task brief's own worked
46
+ // examples name `protectionSummary`'s "protected -> unknown/
47
+ // unprotected" transition as "an equivalent from/to pair for
48
+ // protectionSummary, generalizing" the policyVerdict pair — read
49
+ // literally, that is an instruction to add the SAME shape for a
50
+ // second watched field, not to invent a fully generic DSL over every
51
+ // WATCHED_FLOW_FIELDS entry. A generic `field`/`from`/`to` triple was
52
+ // considered and rejected: nothing in the brief's worked examples
53
+ // needs a THIRD field's transition, and a generic form would need its
54
+ // own validation against `WATCHED_FLOW_FIELDS` this task's tests
55
+ // don't exercise. Extending to a fully generic form remains easy
56
+ // later (both matchers already share one small helper,
57
+ // `_matchesVerdictTransition`) without a rule-shape migration for
58
+ // what's shipped today.
59
+ //
60
+ // 3. `dataClass`/`sinkCategory` are read as ADDITIONAL, optional
61
+ // constraints on a `changed_flow` rule too, not just `new_flow`
62
+ // rules — the brief's own rule shape lists them as top-level fields
63
+ // (not scoped textually to `new_flow` alone), and a changed flow's
64
+ // id is guaranteed present in `graphAfter` by construction (a
65
+ // `changed.flows` entry only exists when the SAME flow id survived
66
+ // into the after-graph — see graph-diff.js's own `computeGraphDiff`
67
+ // header), so the same `_resolveFlowContext` resolution that
68
+ // `new_flow` rules use applies unchanged. This lets an operator write
69
+ // "a PHI flow that regresses from protected" as one rule instead of
70
+ // two, and costs nothing when a rule doesn't set either field (both
71
+ // stay unconstrained, per the fail-closed style above).
72
+ //
73
+ // 4. A `changed_flow` rule that sets none of
74
+ // dataClass/sinkCategory/fromPolicyVerdict/toPolicyVerdict/
75
+ // fromProtectionSummary/toProtectionSummary matches EVERY
76
+ // `changed.flows` entry — every axis is independently unconstrained,
77
+ // so a rule with no axes set is the fully unconstrained case, exactly
78
+ // as an `allow` rule with neither `class` nor `sink` would be in
79
+ // `privacy-sink-policy.js` (it doesn't have that case in practice
80
+ // because `sink` is required there; this module has no required axis
81
+ // besides `trigger`, so the maximally-open rule is reachable and
82
+ // intentional — "alert on ANY change to a flow matching these
83
+ // constraints" is a real, useful policy shape).
84
+ //
85
+ // 5. The task brief's PRD-quoted worked examples also name "PII -> AI"
86
+ // ,"PCI -> log" (both plain `new_flow` shapes, already covered by
87
+ // dataClass/sinkCategory matching — no separate mechanism needed) and
88
+ // "new unresolved recipient" (a `governanceRefs.recipient`-shaped
89
+ // transition). The brief's own "Tests to write" section does not
90
+ // require a test for the recipient example, and it does not reduce
91
+ // cleanly to either matcher this task implements (a "new unresolved
92
+ // recipient" is a value becoming a specific sentinel, not a flow
93
+ // newly existing or an already-modeled verdict field transitioning)
94
+ // — left out rather than guessed at. Judgment call #2's rejected
95
+ // generic `field`/`from`/`to` form is the natural place to add it
96
+ // later, against a real governanceRefs.recipient fixture.
97
+ //
98
+ // 6. Fix round 1, Important 1 (see graph-diff.js's own judgment call #4):
99
+ // a `new_flow` rule must skip an added flow entry whose
100
+ // `causeClassification === 'reidentified'` — that flow isn't actually
101
+ // new, it's the SAME real-world (source, sink, dataElementIds) flow as
102
+ // a flow that existed before, just minted under a new flowId because
103
+ // the engine's own confidence/shape (evidenceGrade/transformationIds)
104
+ // changed. Firing `new_flow` on it would false-fire `--fail-on-drift`
105
+ // on a non-event, exactly what this fix round exists to close.
106
+
107
+
108
+
109
+ const POLICY_TRIGGERS = Object.freeze(['new_flow', 'changed_flow']);
110
+
111
+ function _isValidRuleShape(p) {
112
+ return !!p && typeof p === 'object' && !Array.isArray(p) && POLICY_TRIGGERS.includes(p.trigger);
113
+ }
114
+
115
+ /**
116
+ * Load an operator's drift-policy rules from a literal JSON file path
117
+ * (see judgment call #1 above — never a scanRoot). Never throws — a
118
+ * missing file (ENOENT) is "no policies configured," matching
119
+ * `loadPrivacySinkPolicy`'s own precedent; a malformed file logs a
120
+ * warning and degrades to no policies. Each entry is validated loosely:
121
+ * an entry that isn't a plain object with a recognized `trigger` is
122
+ * skipped (with a warning naming how many were dropped), never crashing
123
+ * the whole load over one bad rule.
124
+ */
125
+ function loadDriftPolicies(policyFilePath) {
126
+ const EMPTY = { policies: [] };
127
+ if (!policyFilePath) return EMPTY;
128
+
129
+ let raw = null;
130
+ try {
131
+ raw = JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(policyFilePath, 'utf8'));
132
+ } catch (e) {
133
+ if (e.code !== 'ENOENT') {
134
+ console.error(`agentic-security: bad JSON in drift policy file (${policyFilePath}) — falling back to no policies (${e.message})`);
135
+ }
136
+ return EMPTY;
137
+ }
138
+
139
+ if (!Array.isArray(raw?.policies)) {
140
+ console.error(`agentic-security: drift policy file ${policyFilePath} has no "policies" array — falling back to no policies (expected {"policies":[...]})`);
141
+ }
142
+ const rawPolicies = Array.isArray(raw?.policies) ? raw.policies : [];
143
+ const policies = rawPolicies.filter(_isValidRuleShape);
144
+ const skipped = rawPolicies.length - policies.length;
145
+ if (skipped > 0) {
146
+ console.error(`agentic-security: skipped ${skipped} malformed drift-policy rule(s) in ${policyFilePath} (each must be an object with trigger: 'new_flow' | 'changed_flow')`);
147
+ }
148
+ return { policies };
149
+ }
150
+
151
+ // Fail-closed, matching privacy-sink-policy.js's own
152
+ // _matchesEnvironment/_matchesDestination precedent exactly: an unset
153
+ // rule value is unconstrained (matches anything); a set rule value with
154
+ // no comparable current value does NOT match.
155
+ function _matchesDataClass(ruleDataClass, dataClasses) {
156
+ if (ruleDataClass == null) return true;
157
+ if (!Array.isArray(dataClasses) || !dataClasses.length) return false;
158
+ return dataClasses.includes(ruleDataClass);
159
+ }
160
+
161
+ function _matchesSinkCategory(ruleSinkCategory, sinkCategory) {
162
+ if (ruleSinkCategory == null) return true;
163
+ if (!sinkCategory) return false;
164
+ return ruleSinkCategory === sinkCategory;
165
+ }
166
+
167
+ // Matches a rule's {fromX, toX} pair against the {before, after} of the
168
+ // changes[] entry for field X (or undefined, when that field didn't
169
+ // change on this flow). Unset fromX/toX is unconstrained. A set fromX/
170
+ // toX with no changes[] entry for that field at all (the flow changed,
171
+ // but not on this axis) does NOT match — fails closed, same as an
172
+ // environment-scoped rule against a caller who supplied no environment.
173
+ function _matchesVerdictTransition(ruleFrom, ruleTo, changeEntry) {
174
+ if (ruleFrom == null && ruleTo == null) return true;
175
+ if (!changeEntry) return false;
176
+ if (ruleFrom != null && changeEntry.before !== ruleFrom) return false;
177
+ if (ruleTo != null && changeEntry.after !== ruleTo) return false;
178
+ return true;
179
+ }
180
+
181
+ // Resolves the real dataClasses/sinkCategory/sinkNode/flow for a flow id
182
+ // against graphAfter — shared by both new_flow (added.flows entries) and
183
+ // changed_flow (changed.flows entries) matching, since a changed.flows
184
+ // entry's id is, by construction, present in graphAfter too (see
185
+ // graph-diff.js's own computeGraphDiff: a changed entry only exists for
186
+ // a flow id surviving into the after-graph). Returns null when the id
187
+ // can't be resolved (should not happen for a well-formed GraphDiff
188
+ // produced by computeGraphDiff against its own graphAfter, but a rule
189
+ // must never crash the whole evaluation over a caller-supplied
190
+ // diff/graph pairing that doesn't actually match).
191
+ function _resolveFlowContext(flowId, graphAfter) {
192
+ const flow = (graphAfter?.flows ?? []).find((f) => f.id === flowId);
193
+ if (!flow) return null;
194
+
195
+ const dataClasses = [];
196
+ const seen = new Set();
197
+ for (const deId of flow.dataElementIds ?? []) {
198
+ const de = (graphAfter.dataElements ?? []).find((d) => d.id === deId);
199
+ if (!de) continue;
200
+ for (const cls of de.dataClasses ?? []) {
201
+ if (!seen.has(cls)) { seen.add(cls); dataClasses.push(cls); }
202
+ }
203
+ }
204
+ const dataElementNames = (flow.dataElementIds ?? [])
205
+ .map((id) => (graphAfter.dataElements ?? []).find((d) => d.id === id)?.name)
206
+ .filter((n) => typeof n === 'string' && n.length > 0);
207
+
208
+ const sinkNode = (graphAfter.nodes ?? []).find((n) => n.id === flow.sink) ?? null;
209
+ const sinkCategory = sinkNode?.subtype ?? sinkNode?.kind ?? null;
210
+
211
+ return { flow, dataClasses, dataElementNames, sinkNode, sinkCategory };
212
+ }
213
+
214
+ function _describeTransition(changeEntry) {
215
+ if (!changeEntry) return null;
216
+ return `${changeEntry.field} ${changeEntry.before} -> ${changeEntry.after}`;
217
+ }
218
+
219
+ function _buildViolation(rule, trigger, ctx, transitionDescriptions) {
220
+ const base = trigger === 'new_flow'
221
+ ? `New flow ${ctx.flow.id} (${ctx.dataClasses.join('/') || 'unclassified data'}) newly reaches sink ${ctx.sinkCategory ?? ctx.sinkNode?.id ?? 'unknown'}`
222
+ : `Flow ${ctx.flow.id} changed: ${transitionDescriptions.length ? transitionDescriptions.join(', ') : 'matched changed_flow rule (no policyVerdict/protectionSummary transition named by this rule)'}`;
223
+ const reason = rule.reason ? `${base} (${rule.reason})` : base;
224
+
225
+ return {
226
+ flowId: ctx.flow.id,
227
+ dataElementNames: ctx.dataElementNames,
228
+ sinkCategory: ctx.sinkCategory,
229
+ sinkNodeId: ctx.sinkNode?.id ?? ctx.flow.sink,
230
+ trigger,
231
+ rule,
232
+ reason,
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Evaluates operator-configured drift policies against a GraphDiff.
238
+ * `policies` is `{policies: [...]}` (loadDriftPolicies' own return
239
+ * shape). `graphAfter` is the AFTER graph the diff was computed against
240
+ * — needed because an `added.flows`/`changed.flows` entry carries only
241
+ * `{id, causeClassification, ...}` (see graph-diff.js's own header),
242
+ * never dataElement/sink info directly.
243
+ *
244
+ * Returns `{violations: [...]}` — never a bare boolean per flow; every
245
+ * violation names the real triggering flow (id, data element names,
246
+ * sink), the rule that fired, and a human-readable `reason` string.
247
+ */
248
+ function evaluateDriftPolicies(diff, policies, graphAfter) {
249
+ const violations = [];
250
+ const rules = Array.isArray(policies?.policies) ? policies.policies : [];
251
+ if (!rules.length) return { violations };
252
+
253
+ for (const rule of rules) {
254
+ if (rule.trigger === 'new_flow') {
255
+ for (const addedEntry of diff?.added?.flows ?? []) {
256
+ // See judgment call #6 above / graph-diff.js's own judgment call
257
+ // #4: a reidentified added flow is not actually new.
258
+ if (addedEntry.causeClassification === 'reidentified') continue;
259
+ const ctx = _resolveFlowContext(addedEntry.id, graphAfter);
260
+ if (!ctx) continue;
261
+ if (!_matchesDataClass(rule.dataClass, ctx.dataClasses)) continue;
262
+ if (!_matchesSinkCategory(rule.sinkCategory, ctx.sinkCategory)) continue;
263
+ violations.push(_buildViolation(rule, 'new_flow', ctx, []));
264
+ }
265
+ } else if (rule.trigger === 'changed_flow') {
266
+ for (const changedEntry of diff?.changed?.flows ?? []) {
267
+ const ctx = _resolveFlowContext(changedEntry.id, graphAfter);
268
+ if (!ctx) continue;
269
+ if (!_matchesDataClass(rule.dataClass, ctx.dataClasses)) continue;
270
+ if (!_matchesSinkCategory(rule.sinkCategory, ctx.sinkCategory)) continue;
271
+
272
+ const changes = Array.isArray(changedEntry.changes) ? changedEntry.changes : [];
273
+ const policyChange = changes.find((c) => c.field === 'policyVerdict');
274
+ if (!_matchesVerdictTransition(rule.fromPolicyVerdict, rule.toPolicyVerdict, policyChange)) continue;
275
+
276
+ const protectionChange = changes.find((c) => c.field === 'protectionSummary');
277
+ if (!_matchesVerdictTransition(rule.fromProtectionSummary, rule.toProtectionSummary, protectionChange)) continue;
278
+
279
+ const transitionDescriptions = [policyChange, protectionChange].map(_describeTransition).filter(Boolean);
280
+ violations.push(_buildViolation(rule, 'changed_flow', ctx, transitionDescriptions));
281
+ }
282
+ }
283
+ // An unrecognized trigger was already filtered out by
284
+ // loadDriftPolicies' own _isValidRuleShape when loaded from disk; a
285
+ // directly hand-built `policies` object (as tests do) with an
286
+ // unrecognized trigger simply matches nothing, silently — the same
287
+ // "match nothing rather than throw" discipline as every other
288
+ // unresolvable axis in this file.
289
+ }
290
+
291
+ return { violations };
292
+ }
293
+
294
+
295
+ /***/ })
296
+
297
+ };