@clear-capabilities/agentic-security-scanner 0.172.1 → 0.174.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 (201) hide show
  1. package/CHANGELOG.md +26 -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 +31 -6
  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 +282 -24
  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/language/haskell-findings.js +2 -0
  157. package/src/language/haskell-guards.js +1 -1
  158. package/src/language/haskell-ir.js +3 -1
  159. package/src/language/haskell-models.js +94 -0
  160. package/src/language/haskell-security-rules.js +20 -0
  161. package/src/language/nixos-hardening.js +72 -0
  162. package/src/lineage/build-attested-correlation.js +63 -0
  163. package/src/lineage/deployment-identity.js +76 -0
  164. package/src/mcp/tools.js +13 -34
  165. package/src/pipeline/canonical-output.js +64 -0
  166. package/src/pipeline/detector-runner.js +7 -0
  167. package/src/pipeline/scan-health.js +14 -0
  168. package/src/pipeline/supervised-analyzers.js +125 -0
  169. package/src/pipeline/supervised-child.js +17 -0
  170. package/src/pipeline/telemetry.js +79 -0
  171. package/src/posture/accuracy-scorecard.js +98 -9
  172. package/src/posture/artifact-registry.js +5 -0
  173. package/src/posture/assurance/evidence-registry.js +215 -0
  174. package/src/posture/assurance/release-package-verify.js +135 -0
  175. package/src/posture/assurance/release-package.js +118 -0
  176. package/src/posture/assurance/release-verdict.js +94 -0
  177. package/src/posture/assurance/waiver.js +200 -0
  178. package/src/posture/compliance-truth.js +100 -0
  179. package/src/posture/evaluation/scan-child.js +3 -1
  180. package/src/posture/evaluation/why-missed.js +3 -1
  181. package/src/posture/exploitability-evidence.js +141 -0
  182. package/src/posture/fix-verify.js +4 -2
  183. package/src/posture/fleet/exposure-ranking.js +115 -0
  184. package/src/posture/fleet/service-chain-graph.js +150 -0
  185. package/src/posture/graph-reconciliation.js +154 -0
  186. package/src/posture/provenance/coverage.js +103 -0
  187. package/src/posture/provenance/git-evidence.js +84 -0
  188. package/src/posture/provenance/origin-resolver.js +7 -1
  189. package/src/posture/provenance/predicate-replay.js +14 -4
  190. package/src/posture/runtime-correlation.js +61 -15
  191. package/src/posture/runtime-explanation.js +116 -0
  192. package/src/posture/runtime-trace-ingest.js +194 -0
  193. package/src/posture/scan-checkpoint.js +38 -10
  194. package/src/posture/waf-ingest.js +7 -2
  195. package/src/report/index.js +4 -0
  196. package/src/report/oscal.js +38 -0
  197. package/src/report/perspectives.js +150 -0
  198. package/src/sast/_comment-strip.js +8 -1
  199. package/src/sast/ruby.js +26 -0
  200. package/src/sast/zip-slip.js +79 -0
  201. package/src/util/string-memo.js +38 -0
@@ -0,0 +1,369 @@
1
+ export const id = 9292;
2
+ export const ids = [9292,119];
3
+ export const modules = {
4
+
5
+ /***/ 69292:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ ImageArchiveError: () => (/* binding */ ImageArchiveError),
10
+ /* harmony export */ t: () => (/* binding */ parseImageArchive)
11
+ /* harmony export */ });
12
+ /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(77598);
13
+ /* harmony import */ var _safe_tar_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(30119);
14
+ // Parses a Docker-save ("docker-archive") or OCI-layout ("oci-archive") tar
15
+ // into one normalized shape, without executing or daemon-mounting the image
16
+ // (PRD 5 Safety Requirements: "OCI inspection should not require executing
17
+ // the image"). Built entirely on `safe-tar.js` — every byte read here passes
18
+ // through the same entry-count/size/path/decompression guards.
19
+
20
+
21
+
22
+ class ImageArchiveError extends Error {
23
+ constructor(message, code) {
24
+ super(message);
25
+ this.name = 'ImageArchiveError';
26
+ this.code = code;
27
+ }
28
+ }
29
+
30
+ function safeJsonParse(buf, what) {
31
+ try {
32
+ return JSON.parse(buf.toString('utf8'));
33
+ } catch (err) {
34
+ throw new ImageArchiveError(`${what} is not valid JSON: ${err.message}`, 'malformed_manifest');
35
+ }
36
+ }
37
+
38
+ function readOuterEntries(buf, limits) {
39
+ const outer = (0,_safe_tar_js__WEBPACK_IMPORTED_MODULE_1__/* .decompressIfGzip */ .zr)(buf, limits);
40
+ const byName = new Map();
41
+ const unsafeEntries = [];
42
+ for (const e of (0,_safe_tar_js__WEBPACK_IMPORTED_MODULE_1__/* .readTarEntries */ .ET)(outer, limits)) {
43
+ if (e.unsafe) {
44
+ unsafeEntries.push({ name: e.name });
45
+ continue;
46
+ }
47
+ byName.set(e.name, e);
48
+ }
49
+ return { byName, unsafeEntries };
50
+ }
51
+
52
+ function extractConfigShape(configObj) {
53
+ const cfg = (configObj && configObj.config) || {};
54
+ return {
55
+ architecture: configObj.architecture || null,
56
+ os: configObj.os || null,
57
+ labels: (cfg.Labels && typeof cfg.Labels === 'object') ? { ...cfg.Labels } : {},
58
+ entrypoint: Array.isArray(cfg.Entrypoint) ? [...cfg.Entrypoint] : null,
59
+ cmd: Array.isArray(cfg.Cmd) ? [...cfg.Cmd] : null,
60
+ history: Array.isArray(configObj.history)
61
+ ? configObj.history.map((h) => ({ createdBy: h.created_by || null, created: h.created || null, emptyLayer: !!h.empty_layer }))
62
+ : [],
63
+ rootfsDiffIds: (configObj.rootfs && Array.isArray(configObj.rootfs.diff_ids)) ? [...configObj.rootfs.diff_ids] : [],
64
+ };
65
+ }
66
+
67
+ function parseDockerArchive(byName, outerUnsafe, limits) {
68
+ const manifestEntry = byName.get('manifest.json');
69
+ const manifestArr = safeJsonParse(manifestEntry.data, 'manifest.json');
70
+ if (!Array.isArray(manifestArr) || manifestArr.length === 0) {
71
+ throw new ImageArchiveError('manifest.json contains no images', 'empty_manifest');
72
+ }
73
+ if (manifestArr.length > 1) {
74
+ throw new ImageArchiveError(`manifest.json describes ${manifestArr.length} images; image scan expects exactly one`, 'multi_image_archive');
75
+ }
76
+ const m = manifestArr[0];
77
+ if (typeof m.Config !== 'string' || !Array.isArray(m.Layers)) {
78
+ throw new ImageArchiveError('manifest.json entry is missing Config or Layers', 'malformed_manifest');
79
+ }
80
+ const configEntry = byName.get(m.Config);
81
+ if (!configEntry) throw new ImageArchiveError(`manifest.json references missing config "${m.Config}"`, 'missing_blob');
82
+ const configObj = safeJsonParse(configEntry.data, m.Config);
83
+ const configDigest = `sha256:${node_crypto__WEBPACK_IMPORTED_MODULE_0__.createHash('sha256').update(configEntry.data).digest('hex')}`;
84
+
85
+ const layers = [];
86
+ for (let i = 0; i < m.Layers.length; i++) {
87
+ const layerPath = m.Layers[i];
88
+ const entry = byName.get(layerPath);
89
+ if (!entry) throw new ImageArchiveError(`manifest.json references missing layer "${layerPath}"`, 'missing_blob');
90
+ const tar = (0,_safe_tar_js__WEBPACK_IMPORTED_MODULE_1__/* .decompressIfGzip */ .zr)(entry.data, limits);
91
+ layers.push({ index: i, path: layerPath, diffId: configObj?.rootfs?.diff_ids?.[i] || null, tar, sizeBytes: entry.size });
92
+ }
93
+
94
+ return {
95
+ format: 'docker-archive',
96
+ digest: configDigest,
97
+ digestKind: 'config',
98
+ repoTags: Array.isArray(m.RepoTags) ? m.RepoTags : [],
99
+ config: extractConfigShape(configObj),
100
+ layers,
101
+ unsafeEntries: outerUnsafe,
102
+ };
103
+ }
104
+
105
+ function parseOciArchive(byName, outerUnsafe, limits) {
106
+ const indexObj = safeJsonParse(byName.get('index.json').data, 'index.json');
107
+ const manifests = Array.isArray(indexObj.manifests) ? indexObj.manifests : [];
108
+ if (manifests.length === 0) throw new ImageArchiveError('index.json lists no manifests', 'empty_manifest');
109
+ if (manifests.length > 1) {
110
+ throw new ImageArchiveError(`index.json describes ${manifests.length} manifests; image scan expects exactly one`, 'multi_image_archive');
111
+ }
112
+ const manifestDigest = manifests[0].digest;
113
+ if (typeof manifestDigest !== 'string' || !manifestDigest.startsWith('sha256:')) {
114
+ throw new ImageArchiveError('index.json manifest entry has no sha256 digest', 'malformed_manifest');
115
+ }
116
+ const manifestBlobPath = `blobs/sha256/${manifestDigest.slice('sha256:'.length)}`;
117
+ const manifestEntry = byName.get(manifestBlobPath);
118
+ if (!manifestEntry) throw new ImageArchiveError(`index.json references missing manifest blob "${manifestBlobPath}"`, 'missing_blob');
119
+ const manifestObj = safeJsonParse(manifestEntry.data, manifestBlobPath);
120
+
121
+ const configDigest = manifestObj.config && manifestObj.config.digest;
122
+ if (typeof configDigest !== 'string' || !configDigest.startsWith('sha256:')) {
123
+ throw new ImageArchiveError('image manifest has no config digest', 'malformed_manifest');
124
+ }
125
+ const configBlobPath = `blobs/sha256/${configDigest.slice('sha256:'.length)}`;
126
+ const configEntry = byName.get(configBlobPath);
127
+ if (!configEntry) throw new ImageArchiveError(`manifest references missing config blob "${configBlobPath}"`, 'missing_blob');
128
+ const configObj = safeJsonParse(configEntry.data, configBlobPath);
129
+
130
+ const layerDescs = Array.isArray(manifestObj.layers) ? manifestObj.layers : [];
131
+ const layers = [];
132
+ for (let i = 0; i < layerDescs.length; i++) {
133
+ const d = layerDescs[i];
134
+ if (typeof d.digest !== 'string' || !d.digest.startsWith('sha256:')) {
135
+ throw new ImageArchiveError(`layer ${i} has no sha256 digest`, 'malformed_manifest');
136
+ }
137
+ const blobPath = `blobs/sha256/${d.digest.slice('sha256:'.length)}`;
138
+ const entry = byName.get(blobPath);
139
+ if (!entry) throw new ImageArchiveError(`manifest references missing layer blob "${blobPath}"`, 'missing_blob');
140
+ const tar = (0,_safe_tar_js__WEBPACK_IMPORTED_MODULE_1__/* .decompressIfGzip */ .zr)(entry.data, limits);
141
+ layers.push({ index: i, path: blobPath, diffId: d.digest, mediaType: d.mediaType || null, tar, sizeBytes: entry.size });
142
+ }
143
+
144
+ return {
145
+ format: 'oci-archive',
146
+ digest: manifestDigest,
147
+ digestKind: 'manifest',
148
+ repoTags: [],
149
+ config: extractConfigShape(configObj),
150
+ layers,
151
+ unsafeEntries: outerUnsafe,
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Parses an in-memory tar buffer (already read from disk/stdin by the
157
+ * caller — this module never touches the filesystem or network) as either a
158
+ * `docker save`-shaped archive or an OCI-layout archive. Throws
159
+ * ImageArchiveError for anything it cannot recognize or whose manifest
160
+ * references a blob that isn't present — never guesses or substitutes a
161
+ * default.
162
+ */
163
+ function parseImageArchive(buf, limits = _safe_tar_js__WEBPACK_IMPORTED_MODULE_1__/* .DEFAULT_LIMITS */ .dR) {
164
+ if (!Buffer.isBuffer(buf)) throw new ImageArchiveError('image archive input must be a Buffer', 'bad_input');
165
+ const { byName, unsafeEntries } = readOuterEntries(buf, limits);
166
+ if (byName.has('manifest.json')) return parseDockerArchive(byName, unsafeEntries, limits);
167
+ if (byName.has('index.json') && byName.has('oci-layout')) return parseOciArchive(byName, unsafeEntries, limits);
168
+ throw new ImageArchiveError(
169
+ 'archive is neither a docker-archive (manifest.json) nor an oci-archive (oci-layout + index.json)',
170
+ 'unrecognized_format',
171
+ );
172
+ }
173
+
174
+
175
+ /***/ }),
176
+
177
+ /***/ 30119:
178
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
179
+
180
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
181
+ /* harmony export */ ET: () => (/* binding */ readTarEntries),
182
+ /* harmony export */ Jn: () => (/* binding */ whiteoutTarget),
183
+ /* harmony export */ TarSafetyError: () => (/* binding */ TarSafetyError),
184
+ /* harmony export */ dR: () => (/* binding */ DEFAULT_LIMITS),
185
+ /* harmony export */ l0: () => (/* binding */ isWhiteoutName),
186
+ /* harmony export */ zr: () => (/* binding */ decompressIfGzip)
187
+ /* harmony export */ });
188
+ /* unused harmony export isSafeEntryName */
189
+ /* harmony import */ var node_zlib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(38522);
190
+ // Hand-rolled, hardened POSIX/GNU tar reader for OCI/Docker image archives.
191
+ //
192
+ // Why hand-rolled instead of a dependency: this repo has no tar library (see
193
+ // `src/artifact/CLAUDE.md`), and PRD 5's own Safety Requirements ask for exact
194
+ // control over the failure modes a generic tar library treats as implementation
195
+ // detail: malicious entry paths, decompression bombs, giant layers, oversized
196
+ // archives. Rolling it ourselves means every one of those limits is a named,
197
+ // tested constant in this file rather than a library default we'd have to audit.
198
+ //
199
+ // Scope: ustar + GNU longname ('L' typeflag) + base-256 size fields. PAX extended
200
+ // headers ('x'/'g') and GNU longlink ('K') are structurally skipped (their data
201
+ // block is consumed so offsets stay correct, but their content is not applied) —
202
+ // a disclosed limitation, not a silent one. Real docker-save/oci-archive output
203
+ // from Go's archive/tar for images in this size/complexity class essentially
204
+ // never needs them; synthetic fixtures in this codebase never emit them either.
205
+
206
+
207
+ const BLOCK = 512;
208
+
209
+ const DEFAULT_LIMITS = Object.freeze({
210
+ maxEntries: 200_000,
211
+ maxEntrySize: 512 * 1024 * 1024, // 512MB per tar entry
212
+ maxTotalSize: 4 * 1024 * 1024 * 1024, // 4GB per archive (sum of entry sizes)
213
+ maxGunzipOutput: 4 * 1024 * 1024 * 1024, // decompression-bomb guard
214
+ });
215
+
216
+ class TarSafetyError extends Error {
217
+ constructor(message, code) {
218
+ super(message);
219
+ this.name = 'TarSafetyError';
220
+ this.code = code;
221
+ }
222
+ }
223
+
224
+ function parseSizeField(buf) {
225
+ if (buf[0] & 0x80) {
226
+ // GNU base-256 extension for sizes that don't fit in octal-ASCII.
227
+ let n = 0n;
228
+ for (let i = 1; i < buf.length; i++) n = (n << 8n) | BigInt(buf[i]);
229
+ return Number(n);
230
+ }
231
+ const s = buf.toString('ascii').replace(/\0.*$/, '').trim();
232
+ if (!s) return 0;
233
+ const n = parseInt(s, 8);
234
+ return Number.isFinite(n) ? n : 0;
235
+ }
236
+
237
+ function cstr(buf) {
238
+ return buf.toString('utf8').replace(/\0.*$/, '');
239
+ }
240
+
241
+ // A tar entry name is unsafe when it would escape a virtual extraction root:
242
+ // absolute paths, `..` segments, embedded NULs, or an unreasonable length.
243
+ // Because this module never writes to a real filesystem path (every caller
244
+ // keeps extracted content in an in-memory Map keyed by this string), the
245
+ // classic zip-slip *write* vulnerability cannot occur here — but a hostile
246
+ // name must still never be silently treated as an ordinary path by downstream
247
+ // manifest/package-DB matching, so unsafe entries are flagged, never dropped
248
+ // silently and never accepted as-is.
249
+ function isSafeEntryName(name) {
250
+ if (typeof name !== 'string' || name.length === 0 || name.length > 4096) return false;
251
+ if (name.includes('\0')) return false;
252
+ const norm = name.replace(/\\/g, '/');
253
+ if (norm.startsWith('/')) return false;
254
+ if (norm.split('/').some((p) => p === '..')) return false;
255
+ return true;
256
+ }
257
+
258
+ function isWhiteoutName(name) {
259
+ return (name.split('/').pop() || '').startsWith('.wh.');
260
+ }
261
+
262
+ // Resolves a whiteout entry name to what it deletes. `.wh..wh..opq` marks a
263
+ // directory as "opaque" (everything from earlier layers under it is hidden,
264
+ // not individually deleted) — reported distinctly so callers don't conflate
265
+ // it with a single-file deletion.
266
+ function whiteoutTarget(name) {
267
+ const parts = name.split('/');
268
+ const base = parts.pop();
269
+ if (base === '.wh..wh..opq') return { opaque: true, dir: parts.join('/') };
270
+ return { opaque: false, path: [...parts, base.slice(4)].join('/') };
271
+ }
272
+
273
+ // Decompresses a gzip member with a hard output-size cap (Node's native
274
+ // ERR_BUFFER_TOO_LARGE on overflow — no custom streaming counter needed).
275
+ // Returns the input unchanged when it isn't gzip-magic (plain tar).
276
+ function decompressIfGzip(buf, limits = DEFAULT_LIMITS) {
277
+ if (buf.length >= 2 && buf[0] === 0x1f && buf[1] === 0x8b) {
278
+ try {
279
+ return node_zlib__WEBPACK_IMPORTED_MODULE_0__.gunzipSync(buf, { maxOutputLength: limits.maxGunzipOutput });
280
+ } catch (err) {
281
+ if (err && err.code === 'ERR_BUFFER_TOO_LARGE') {
282
+ throw new TarSafetyError('decompressed layer exceeds the decompression-bomb guard', 'gunzip_too_large');
283
+ }
284
+ throw new TarSafetyError(`gzip stream is malformed: ${err.message}`, 'gunzip_malformed');
285
+ }
286
+ }
287
+ return buf;
288
+ }
289
+
290
+ /**
291
+ * Reads a decompressed tar buffer and yields one record per entry:
292
+ * { name, size, typeflag, linkname, data, unsafe }
293
+ * `data` is a Buffer view into `buf` (never copied) for safe entries, `null`
294
+ * for unsafe ones. Throws TarSafetyError (never returns a partial result) the
295
+ * moment any configured limit is exceeded.
296
+ */
297
+ function* readTarEntries(buf, limits = DEFAULT_LIMITS) {
298
+ if (!Buffer.isBuffer(buf)) throw new TarSafetyError('tar input must be a Buffer', 'bad_input');
299
+ let offset = 0;
300
+ let count = 0;
301
+ let totalSize = 0;
302
+ let pendingLongName = null;
303
+
304
+ while (offset + BLOCK <= buf.length) {
305
+ const header = buf.subarray(offset, offset + BLOCK);
306
+ if (header.every((b) => b === 0)) {
307
+ offset += BLOCK;
308
+ continue; // end-of-archive zero-fill padding
309
+ }
310
+
311
+ count += 1;
312
+ if (count > limits.maxEntries) {
313
+ throw new TarSafetyError(`tar archive has more than ${limits.maxEntries} entries`, 'too_many_entries');
314
+ }
315
+
316
+ let name = cstr(header.subarray(0, 100));
317
+ const typeflag = String.fromCharCode(header[156] || 0);
318
+ const linkname = cstr(header.subarray(157, 257));
319
+ const magic = header.subarray(257, 263).toString('ascii');
320
+ const prefix = magic.startsWith('ustar') ? cstr(header.subarray(345, 500)) : '';
321
+ if (prefix) name = `${prefix}/${name}`;
322
+ const size = parseSizeField(header.subarray(124, 136));
323
+
324
+ offset += BLOCK;
325
+
326
+ if (size < 0 || !Number.isFinite(size)) {
327
+ throw new TarSafetyError(`tar entry "${name}" has an invalid size field`, 'bad_size');
328
+ }
329
+ if (size > limits.maxEntrySize) {
330
+ throw new TarSafetyError(`tar entry "${name}" (${size} bytes) exceeds the per-entry size limit`, 'entry_too_large');
331
+ }
332
+ totalSize += size;
333
+ if (totalSize > limits.maxTotalSize) {
334
+ throw new TarSafetyError('tar archive exceeds the total-size limit', 'archive_too_large');
335
+ }
336
+
337
+ const dataBlocks = Math.ceil(size / BLOCK);
338
+ if (offset + size > buf.length) {
339
+ throw new TarSafetyError(`tar entry "${name}" claims more data than the archive contains`, 'truncated');
340
+ }
341
+ const data = size > 0 ? buf.subarray(offset, offset + size) : Buffer.alloc(0);
342
+ offset += dataBlocks * BLOCK;
343
+
344
+ const resolvedName = pendingLongName ? pendingLongName : name;
345
+ pendingLongName = null;
346
+
347
+ if (typeflag === 'L') {
348
+ // GNU longname: this entry's data IS the next entry's real name.
349
+ pendingLongName = cstr(data);
350
+ continue;
351
+ }
352
+ if (typeflag === 'K' || typeflag === 'x' || typeflag === 'g') {
353
+ // GNU longlink / PAX extended header — structurally skipped (see header comment).
354
+ continue;
355
+ }
356
+
357
+ if (!isSafeEntryName(resolvedName)) {
358
+ yield { name: resolvedName, size, typeflag, linkname, data: null, unsafe: true };
359
+ continue;
360
+ }
361
+
362
+ yield { name: resolvedName.replace(/\\/g, '/'), size, typeflag, linkname, data, unsafe: false };
363
+ }
364
+ }
365
+
366
+
367
+ /***/ })
368
+
369
+ };
@@ -421,7 +421,8 @@ async function adjudicate(candidates, opts = {}) {
421
421
  origin: 'context-intelligence', questionPack: c.questionPack, witnesses: c.witnesses, witnessCount: c.witnesses.length,
422
422
  sourceSpanEvidence: c.sourceSpanEvidence, scopeIntegrity: c.scopeIntegrity, reviewStatus: c.reviewStatus,
423
423
  uncertainties: c.uncertainties, recommendedValidation: c.recommendedValidation,
424
- note: 'A model hypothesis checked for citations, not a proof. Severity comes from the confirmation tier only.',
424
+ ...(c.deterministic ? { deterministic: c.deterministic } : {}),
425
+ note: c.deterministic ? 'Derived from the code analysis and a deterministic counterexample attempt; no model produced this. Severity comes from the confirmation tier only.' : 'A model hypothesis checked for citations, not a proof. Severity comes from the confirmation tier only.',
425
426
  },
426
427
  };
427
428
  };
@@ -774,7 +775,8 @@ const BUILD = [
774
775
  { language: 'javascript/typescript', kind: 'http', frameworks: 'Express-style call registration (get/post/put/patch/delete/all, router.use, app.use)', status: 'verified', evidence: [T('build-js.test.js')], limits: 'Router mounts in another file and middleware factories are not followed (reported as router-mount-not-resolved); destructured and named imports are followed, re-exports and tsconfig path aliases are not.' },
775
776
  { language: 'python', kind: 'http', frameworks: 'decorator registration (Flask/FastAPI-style @app.route / @router.get), FastAPI Depends()/Security() guards', status: 'experimental', evidence: [T('build-python-haskell.test.js'), T('frameworks.test.js')], limits: 'Decorator guards are read from the decorator lines and Depends() guards from the signature (only dependencies named like an auth or user provider bind the principal); framework-level before_request hooks and Django URL conf are not modelled.' },
776
777
  { language: 'haskell', kind: 'http', frameworks: 'Scotty, WAI, Servant, Yesod (through the existing web model)', status: 'verified', evidence: [T('build-python-haskell.test.js'), T('frameworks.test.js')], limits: 'Static source analysis; the existing web model decides what a route is. Dynamic routes are reported unresolved.' },
777
- { language: 'java/kotlin/c#/go/php/ruby/rust', kind: 'http', frameworks: 'whatever the engine route scan recognises', status: 'unsupported', evidence: [], limits: 'No fixture proves this, so it is listed as unsupported even though some routes may resolve: Handlers are resolved only for decorator/annotation-style registration; no dedicated fixtures here.' },
778
+ { language: 'php', kind: 'http', frameworks: 'Laravel Route:: registration (the entry point only)', status: 'experimental', evidence: ['test/engineering-excellence/e4-01-dci-audit.test.js'], limits: 'Measured by the E4-01 audit on one fixture: the routes resolve, but no control or sensitive operation was extracted from the handlers.' },
779
+ { language: 'java/kotlin/c#/go/ruby/rust', kind: 'http', frameworks: 'whatever the engine route scan recognises', status: 'unsupported', evidence: [], limits: 'Measured by the E4-01 audit (docs/engineering-excellence/DCI_MATURITY.md) on one fixture per language (Spring, net/http, ASP.NET, Rails): no entry point was found, so no bundle is built.' },
778
780
  { language: 'any with a recognised surface', kind: 'webhook/queue/cron/upload/cli', frameworks: 'existing entry-point inventory', status: 'experimental', evidence: [T('build-js.test.js')], limits: 'Handler is the enclosing declaration of the surface; no framework wiring beyond that.' },
779
781
  { language: 'haskell', kind: 'compiler metadata import', frameworks: 'hie-export/1 JSON export of GHC declarations and references', status: 'experimental', evidence: [T('hie-adapter.test.js')], limits: 'Reads an export; never runs GHC or hiedb. The export is validated against the source snapshot. No export produced by a real GHC run is checked in.' },
780
782
  { language: 'any', kind: 'compiler metadata build', frameworks: 'automatic metadata generation', status: 'blocked', evidence: [], limits: 'Running a compiler on untrusted source needs a sandbox proof this build does not have (Template Haskell, custom Setup and plugins run code).' },
@@ -1146,6 +1148,8 @@ async function cmdReview(root, args, format) {
1146
1148
  maxCalls: num(flag(args, 'max-calls')) || config.review.maxCalls,
1147
1149
  maxWallSeconds: num(flag(args, 'max-wall-seconds')) || config.review.maxWallSeconds,
1148
1150
  maxTokensPerJob: config.review.maxTokensPerJob,
1151
+ maxTokensPerRepo: config.review.maxTokensPerRepo,
1152
+ anonymizePrompts: config.review.anonymizePrompts,
1149
1153
  maxRetries: config.review.maxRetries,
1150
1154
  jobTimeoutMs: (num(flag(args, 'job-timeout-seconds')) || 120) * 1000,
1151
1155
  maxCostUsd: num(flag(args, 'max-cost-usd')) ?? config.review.maxCostUsd,
@@ -1258,7 +1262,7 @@ const DEFAULT_CONFIG = Object.freeze({
1258
1262
  maxFiles: 3000,
1259
1263
  semanticMetadata: { mode: 'off', haskellHieDir: null },
1260
1264
  context: { profile: 'standard', maxTokens: null, pruneLowValue: true, maxSymbols: 60, maxDepth: 12 },
1261
- review: { enabled: false, allowRemote: false, questionPacks: ['authz-idor'], concurrency: 2, maxCalls: 100, maxWallSeconds: 900, maxCostUsd: null, maxTokensPerJob: 40000, maxRetries: 3 },
1265
+ review: { enabled: false, allowRemote: false, questionPacks: ['authz-idor'], concurrency: 2, maxCalls: 100, maxWallSeconds: 900, maxCostUsd: null, maxTokensPerJob: 40000, maxRetries: 3, maxTokensPerRepo: null, anonymizePrompts: false },
1262
1266
  replay: { contentAddressed: true, retention: 'inherit' },
1263
1267
  });
1264
1268
 
@@ -1307,6 +1311,8 @@ function normalizeConfig(raw) {
1307
1311
  maxCostUsd: Number.isFinite(rv.maxCostUsd) && rv.maxCostUsd >= 0 ? rv.maxCostUsd : null,
1308
1312
  maxTokensPerJob: clamp(rv.maxTokensPerJob, 1000, 400000, d.review.maxTokensPerJob),
1309
1313
  maxRetries: clamp(rv.maxRetries, 0, 3, d.review.maxRetries),
1314
+ maxTokensPerRepo: Number.isFinite(rv.maxTokensPerRepo) && rv.maxTokensPerRepo >= 1000 ? Math.trunc(rv.maxTokensPerRepo) : null,
1315
+ anonymizePrompts: rv.anonymizePrompts === true,
1310
1316
  },
1311
1317
  replay: { contentAddressed: rp.contentAddressed !== false, retention: typeof rp.retention === 'string' ? rp.retention : 'inherit' },
1312
1318
  };
@@ -1333,6 +1339,7 @@ function loadConfig(root) {
1333
1339
  /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
1334
1340
 
1335
1341
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1342
+ /* harmony export */ $E: () => (/* binding */ blankComments),
1336
1343
  /* harmony export */ TM: () => (/* binding */ looksLikeTelemetry),
1337
1344
  /* harmony export */ gs: () => (/* binding */ signatureText),
1338
1345
  /* harmony export */ kl: () => (/* binding */ analyzeSymbolText),
@@ -1480,6 +1487,51 @@ function clip(s) { return s.trim().slice(0, 160); }
1480
1487
  * lines the FILE's lines (0-based array)
1481
1488
  * Returns [{ symbolId, line, category:'control'|'sensitive', kind, subkind, evidence, basis, rejectionSeen, text }]
1482
1489
  */
1490
+ /**
1491
+ * Blank the comments (and Python docstrings) of a source file, keeping every character position and newline, so line numbers do not move.
1492
+ * The analysis looks for calls and comparisons in code; a comment that SAYS `ownerId: req.user.id` or `requireUser(req)` is prose, and prose is the
1493
+ * easiest thing for a hostile or merely careless author to write. Strings are left alone (the principal and SQL rules already read around them).
1494
+ */
1495
+ function blankComments(text, file = '') {
1496
+ const src = String(text || '');
1497
+ const hash = /\.(?:py|rb|sh|php|pl|r)$/i.test(file);
1498
+ const dashes = /\.l?hs$/i.test(file);
1499
+ const cLike = !dashes && !/\.(?:py|rb|sh|pl|r)$/i.test(file);
1500
+ const out = src.split('');
1501
+ const n = src.length;
1502
+ let i = 0;
1503
+ const blank = (a, b) => { for (let k = a; k < b && k < n; k++) if (out[k] !== '\n') out[k] = ' '; };
1504
+ while (i < n) {
1505
+ const c = src[i]; const d = src[i + 1];
1506
+ if (c === '"' || c === "'" || c === '`') {
1507
+ if (/\.py$/i.test(file) && src.startsWith(c.repeat(3), i)) {
1508
+ const end = src.indexOf(c.repeat(3), i + 3); const stop = end < 0 ? n : end + 3;
1509
+ blank(i, stop); i = stop; continue; // a docstring or triple-quoted block is prose
1510
+ }
1511
+ let j = i + 1;
1512
+ while (j < n && src[j] !== c && !(src[j] === '\n' && c !== '`')) { if (src[j] === '\\') j++; j++; }
1513
+ i = j + 1; continue;
1514
+ }
1515
+ if (cLike && c === '/' && d === '*') { const end = src.indexOf('*/', i + 2); const stop = end < 0 ? n : end + 2; blank(i, stop); i = stop; continue; }
1516
+ if (cLike && c === '/' && d === '/' && (i === 0 || /[\s;,)}{\]]/.test(src[i - 1]))) { let j = src.indexOf('\n', i); if (j < 0) j = n; blank(i, j); i = j; continue; }
1517
+ if (hash && c === '#' && !(file.endsWith('.php') && d === '[')) { let j = src.indexOf('\n', i); if (j < 0) j = n; blank(i, j); i = j; continue; }
1518
+ if (dashes && c === '-' && d === '-' && !/[!#$%&*+./<=>?@\\^|~:]/.test(src[i + 2] || ' ')) { let j = src.indexOf('\n', i); if (j < 0) j = n; blank(i, j); i = j; continue; }
1519
+ if (dashes && c === '{' && d === '-') { const end = src.indexOf('-}', i + 2); const stop = end < 0 ? n : end + 2; blank(i, stop); i = stop; continue; }
1520
+ i++;
1521
+ }
1522
+ return out.join('');
1523
+ }
1524
+
1525
+ function bodyAfterSignature(raw) {
1526
+ const arrow = raw.indexOf('=>');
1527
+ let rest = null;
1528
+ if (arrow >= 0) rest = raw.slice(arrow + 2);
1529
+ else { const m = /\)\s*\{(.*)$/.exec(raw); if (m) rest = m[1]; }
1530
+ if (rest === null) return '';
1531
+ const brace = /^\s*\{(.*)$/.exec(rest);
1532
+ return brace ? brace[1] : rest;
1533
+ }
1534
+
1483
1535
  function analyzeSymbolText(symbol, lines, { haskell = false } = {}) {
1484
1536
  const out = [];
1485
1537
  const seen = new Set();
@@ -1512,7 +1564,10 @@ function analyzeSymbolText(symbol, lines, { haskell = false } = {}) {
1512
1564
  const calls = [];
1513
1565
  const re = /([A-Za-z_$][\w$]*(?:(?:\.|::)[A-Za-z_$][\w$]*)*)\s*\(/g;
1514
1566
  let m;
1515
- if (!isDeclLine) { while ((m = re.exec(raw))) calls.push(m[1]); }
1567
+ // A one-line handler (`app.get('/x', guard, async (req, res) => { res.json(await db.docs.findOne({ id })); })`) is its own declaration
1568
+ // line, so skipping the whole line also skipped its body. Only the SIGNATURE is skipped: the calls after the arrow or the opening brace count.
1569
+ const scanText = isDeclLine ? bodyAfterSignature(raw) : raw;
1570
+ while ((m = re.exec(scanText))) calls.push(m[1]);
1516
1571
  if (haskell && !isDeclLine) {
1517
1572
  const hre = /\b([A-Z][\w.]*\.[a-z][\w']*|[a-z][\w']*)\b/g;
1518
1573
  while ((m = hre.exec(raw))) if (AUTH_NAME.test(m[1]) || HS_SENSITIVE.test(m[1])) calls.push(m[1]);
@@ -3021,6 +3076,8 @@ async function runQueue(jobs, ctx) {
3021
3076
  maxCalls: Math.min(HARD_LIMITS.maxCalls, Math.max(1, ctx.limits.maxCalls || 100)),
3022
3077
  maxWallMs: Math.min(HARD_LIMITS.maxWallSeconds, Math.max(1, ctx.limits.maxWallSeconds || 900)) * 1000,
3023
3078
  maxTokensPerJob: ctx.limits.maxTokensPerJob || 40000,
3079
+ maxTokensPerRepo: Number.isFinite(ctx.limits.maxTokensPerRepo) ? ctx.limits.maxTokensPerRepo : null,
3080
+ anonymizePrompts: ctx.limits.anonymizePrompts === true,
3024
3081
  maxRetries: Math.min(HARD_LIMITS.maxRetries, Math.max(0, ctx.limits.maxRetries ?? 3)),
3025
3082
  jobTimeoutMs: ctx.limits.jobTimeoutMs || 120000,
3026
3083
  maxCostUsd: Number.isFinite(ctx.limits.maxCostUsd) ? ctx.limits.maxCostUsd : null,
@@ -3034,6 +3091,7 @@ async function runQueue(jobs, ctx) {
3034
3091
  const results = new Map();
3035
3092
  let calls = 0;
3036
3093
  let estCost = 0;
3094
+ let tokensUsed = 0;
3037
3095
  let cooldownUntil = 0;
3038
3096
  let budgetStop = null;
3039
3097
  let persistChain = Promise.resolve();
@@ -3083,6 +3141,7 @@ async function runQueue(jobs, ctx) {
3083
3141
  if (ctx.signal && ctx.signal.aborted) return 'cancelled';
3084
3142
  if (now() - started >= L.maxWallMs) return 'budget-exhausted:wall-time';
3085
3143
  if (calls >= L.maxCalls) return 'budget-exhausted:max-calls';
3144
+ if (L.maxTokensPerRepo !== null && tokensUsed >= L.maxTokensPerRepo) return 'budget-exhausted:max-tokens-per-repo';
3086
3145
  if (L.maxCostUsd !== null && L.costPerCallUsd !== null && estCost + L.costPerCallUsd > L.maxCostUsd) return 'budget-exhausted:max-cost';
3087
3146
  return null;
3088
3147
  };
@@ -3106,8 +3165,9 @@ async function runQueue(jobs, ctx) {
3106
3165
  state.jobs[job.key] = { state: 'running', reason: null, attempts, label: job.label };
3107
3166
  const out = await (0,_review_js__WEBPACK_IMPORTED_MODULE_2__/* .runReviewOnce */ .z6)({
3108
3167
  pack: loadedJob.pack, entrypoints: loadedJob.entrypoints, manifests: loadedJob.manifests, packageIndexes: job.packageIndexes, packageTexts: loadedJob.packageTexts,
3109
- fileContents: ctx.fileContents, scanRoot: ctx.root, invoker: ctx.invoker, model: ctx.model, limits: { maxTokensPerJob: L.maxTokensPerJob, jobTimeoutMs: L.jobTimeoutMs },
3168
+ fileContents: ctx.fileContents, scanRoot: ctx.root, invoker: ctx.invoker, model: ctx.model, limits: { maxTokensPerJob: L.maxTokensPerJob, jobTimeoutMs: L.jobTimeoutMs, anonymizePrompts: L.anonymizePrompts },
3110
3169
  });
3170
+ if (out.usage) tokensUsed += (out.usage.inputTokensEstimated || 0) + (out.usage.outputTokensEstimated || 0);
3111
3171
  if (out.state === 'skipped_by_policy') calls--; // nothing was sent
3112
3172
  if (out.state === 'failed' && out.retryable && attempts <= L.maxRetries) {
3113
3173
  // exponential backoff with jitter; the shared cooldown slows every worker, which is what a 429 storm needs
@@ -3284,7 +3344,7 @@ function reachFrom({ graph, fileContents, entrypoint, limits = {} }) {
3284
3344
  const lim = { ...DEFAULT_LIMITS, ...limits };
3285
3345
  const { byId, edgesFrom } = indexGraph(graph);
3286
3346
  const linesCache = new Map();
3287
- const linesOf = (f) => { if (!linesCache.has(f)) linesCache.set(f, (fileContents[f] || '').split('\n')); return linesCache.get(f); };
3347
+ const linesOf = (f) => { if (!linesCache.has(f)) linesCache.set(f, (0,_controls_js__WEBPACK_IMPORTED_MODULE_0__/* .blankComments */ .$E)(fileContents[f] || '', f).split('\n')); return linesCache.get(f); };
3288
3348
 
3289
3349
  const visited = [];
3290
3350
  const seen = new Set();
@@ -3450,7 +3510,7 @@ function evaluateProtection({ facts, unknowns, omittedWithControls = false, capp
3450
3510
  /* harmony export */ sj: () => (/* binding */ verifyReview),
3451
3511
  /* harmony export */ z6: () => (/* binding */ runReviewOnce)
3452
3512
  /* harmony export */ });
3453
- /* unused harmony exports CONCLUSIONS, LIKELIHOODS, isLoopbackEndpoint, claimGrounded, resolveCitation, isRetryableError */
3513
+ /* unused harmony exports CONCLUSIONS, LIKELIHOODS, isLoopbackEndpoint, stripUntrustedCommands, pseudonymizer, claimGrounded, resolveCitation, isRetryableError */
3454
3514
  /* harmony import */ var _util_untrusted_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(17097);
3455
3515
  /* harmony import */ var _egress_redact_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(11723);
3456
3516
  /* harmony import */ var _egress_policy_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(45712);
@@ -3599,12 +3659,69 @@ function checkEgressForFiles(files, { scanRoot, model }) {
3599
3659
  // other endpoint needs --allow-remote (or review.allowRemote), on top of the egress policy below, never instead of it.
3600
3660
  if (model.remote && !model.remoteApproved) return { allowed: false, reason: 'remote-endpoint-not-approved (pass --allow-remote or set review.allowRemote after reviewing the egress policy)', file: files[0] || null, evaluated: false };
3601
3661
  for (const file of files) {
3602
- const d = (0,_egress_policy_js__WEBPACK_IMPORTED_MODULE_2__/* .evaluateEgress */ .nn)({ scanRoot, purpose: 'context-review', endpoint: model.endpoint, provider: model.provider, model: model.model, path: file });
3603
- if (!d.allowed) return { allowed: false, reason: d.reason, file, evaluated: true };
3662
+ // Fail closed. A policy check that throws (a corrupt policy file, an unreadable path, a bug) is not an allow: the answer is "deny", with the
3663
+ // reason, and nothing is sent. This is the only place that decides whether a prompt may leave the machine.
3664
+ let d;
3665
+ try { d = (0,_egress_policy_js__WEBPACK_IMPORTED_MODULE_2__/* .evaluateEgress */ .nn)({ scanRoot, purpose: 'context-review', endpoint: model.endpoint, provider: model.provider, model: model.model, path: file }); }
3666
+ catch (err) { return { allowed: false, reason: `egress-policy-error: ${String((err && err.message) || err).slice(0, 120)}`, file, evaluated: true, failedClosed: true }; }
3667
+ if (!d || d.allowed !== true) return { allowed: false, reason: (d && d.reason) || 'egress-policy-returned-no-decision', file, evaluated: true, ...(d ? {} : { failedClosed: true }) };
3604
3668
  }
3605
3669
  return { allowed: true, evaluated: true };
3606
3670
  }
3607
3671
 
3672
+ // ---- model output hygiene -------------------------------------------------------------------------------------
3673
+
3674
+ /**
3675
+ * A model's text is untrusted. It can carry a command for a human or a script to run ("run curl ... | sh to confirm"). Nothing here executes model text, but
3676
+ * these strings reach reports, tickets and fix suggestions that people copy from, so shell-looking content is removed and counted, never passed on.
3677
+ */
3678
+ const FENCE = /```[\s\S]*?```|~~~[\s\S]*?~~~/g;
3679
+ const CMD_WORD = '(?:curl|wget|bash|sh|zsh|sudo|su|rm|chmod|chown|nc|ncat|netcat|python3?|node|perl|ruby|php|powershell|pwsh|cmd(?:\\.exe)?|eval|exec|scp|ssh|dd|mkfs|kill|pkill|git\\s+push|npm\\s+(?:i|install|publish)|pip3?\\s+install|docker\\s+run)';
3680
+ const INLINE_CMD = new RegExp('`[^`\\n]*\\b' + CMD_WORD + '\\b[^`\\n]*`|\\$\\([^)]*\\)|\\b(?:curl|wget)\\s+[^\\s]+[^\\n]*\\|\\s*(?:sh|bash)\\b|(?:^|\\n)\\s*(?:\\$|#)\\s+' + CMD_WORD + '\\b[^\\n]*', 'gi');
3681
+ function stripUntrustedCommands(text) {
3682
+ if (typeof text !== 'string' || !text) return { text: text ?? '', stripped: 0 };
3683
+ let n = 0;
3684
+ let out = text.replace(FENCE, () => { n++; return '[command block removed]'; });
3685
+ out = out.replace(INLINE_CMD, () => { n++; return '[command removed]'; });
3686
+ return { text: out, stripped: n };
3687
+ }
3688
+ function sanitizeReviewStrings(review) {
3689
+ let total = 0;
3690
+ const walk = (v, key) => {
3691
+ if (typeof v === 'string') {
3692
+ if (['promptHash', 'responseHash', 'contextHash', 'entrypointId', 'file', 'role', 'kind', 'conclusion', 'likelihood', 'questionPack', 'status'].includes(key)) return v;
3693
+ const r = stripUntrustedCommands(v); total += r.stripped; return r.text;
3694
+ }
3695
+ if (Array.isArray(v)) return v.map((x) => walk(x, key));
3696
+ if (v && typeof v === 'object') { const o = {}; for (const [k, x] of Object.entries(v)) o[k] = walk(x, k); return o; }
3697
+ return v;
3698
+ };
3699
+ const out = walk(review, '');
3700
+ if (total) out.untrustedCommandsStripped = total;
3701
+ return out;
3702
+ }
3703
+
3704
+ /** Where a suggestion came from and what checked it. Attached to every review so a reader never mistakes a model's words for a finding. */
3705
+ function provenanceOf(model, promptHash) {
3706
+ let host = null; try { host = model.endpoint ? new URL(model.endpoint).hostname : null; } catch { host = null; }
3707
+ const loopback = host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
3708
+ return { producer: 'model', provider: model.provider ?? null, model: model.model ?? null, endpointClass: !model.endpoint ? 'none' : loopback ? 'loopback' : 'remote', promptHash, checkedBy: 'deterministic-citation-verifier', promotedToFinding: false };
3709
+ }
3710
+
3711
+ // ---- prompt anonymization ---------------------------------------------------------------------------------------
3712
+
3713
+ /** Replace each real file path with a stable pseudonym (`f_<hash>.<ext>`). The mapping stays in memory; only the pseudonyms leave. */
3714
+ function pseudonymizer(files) {
3715
+ const toPseudo = new Map(); const toReal = new Map();
3716
+ for (const f of [...new Set(files)].sort()) {
3717
+ const ext = (/\.[A-Za-z0-9]{1,6}$/.exec(f) || [''])[0];
3718
+ const p = `f_${(0,_canonical_js__WEBPACK_IMPORTED_MODULE_5__/* .sha256 */ .sc)(f).slice(0, 10)}${ext}`;
3719
+ toPseudo.set(f, p); toReal.set(p, f);
3720
+ }
3721
+ const swap = (text, map) => { let t = text; for (const [a, b] of [...map].sort((x, y) => y[0].length - x[0].length)) t = t.split(a).join(b); return t; };
3722
+ return { toPseudo: (t) => swap(t, toPseudo), toReal: (t) => swap(t, toReal), size: toPseudo.size };
3723
+ }
3724
+
3608
3725
  // ---- response parsing and verification ------------------------------------------------------------------------
3609
3726
 
3610
3727
  function extractJson(raw) {
@@ -3813,7 +3930,12 @@ async function runReviewOnce({ pack, entrypoints, manifests, packageIndexes, pac
3813
3930
  const badRedact = redacted.find(r => !r.ok);
3814
3931
  if (badRedact) return { state: 'skipped_by_policy', reason: `redaction refused: ${badRedact.reason}` };
3815
3932
 
3816
- const { prompt, promptHash } = buildReviewPrompt({ pack, entrypoints, packageTexts: redacted.map(r => r.text), contextHash: manifests[0].contextHash });
3933
+ // Policy-driven anonymization: file paths are replaced by pseudonyms before the prompt is built, and mapped back in the answer before its citations
3934
+ // are checked against the real snapshot. The model never sees a real path, and what is hashed is what was sent.
3935
+ const anon = limits.anonymizePrompts ? pseudonymizer(files) : null;
3936
+ const sendTexts = anon ? redacted.map(r => anon.toPseudo(r.text)) : redacted.map(r => r.text);
3937
+ const sendEntrypoints = anon ? entrypoints.map(e => JSON.parse(anon.toPseudo(JSON.stringify(e)))) : entrypoints;
3938
+ const { prompt, promptHash } = buildReviewPrompt({ pack, entrypoints: sendEntrypoints, packageTexts: sendTexts, contextHash: manifests[0].contextHash });
3817
3939
  const est = (0,_packer_js__WEBPACK_IMPORTED_MODULE_6__/* .estimateTokens */ .bP)(prompt);
3818
3940
  if (est.tokens > limits.maxTokensPerJob) return { state: 'skipped_by_policy', reason: `prompt is about ${est.tokens} tokens, over the ${limits.maxTokensPerJob} per-job limit` };
3819
3941
 
@@ -3825,7 +3947,9 @@ async function runReviewOnce({ pack, entrypoints, manifests, packageIndexes, pac
3825
3947
  return { state: 'failed', reason: `model call failed: ${String((err && err.message) || err).slice(0, 160)}`, retryable: isRetryableError(err) };
3826
3948
  }
3827
3949
  const usage = { inputTokensEstimated: est.tokens, outputTokensEstimated: Math.ceil(String(raw || '').length / 4), source: 'estimate', redactions: redacted.reduce((n, r) => n + r.redactions, 0) };
3828
- const review = verifyReview({ raw, pack, entrypoints, manifests, packageIndexes, fileContents, promptHash });
3950
+ const rawReal = anon && typeof raw === 'string' ? anon.toReal(raw) : raw;
3951
+ const review0 = verifyReview({ raw: rawReal, pack, entrypoints, manifests, packageIndexes, fileContents, promptHash });
3952
+ const review = { ...sanitizeReviewStrings(review0), modelProvenance: provenanceOf(model, promptHash), ...(anon ? { promptAnonymized: true } : {}) };
3829
3953
  if (review.status === 'declined') return { state: 'partial', reason: `answer declined: ${review.declineReasons.join(',')}`, review, usage };
3830
3954
  return { state: 'succeeded', reason: null, review, usage };
3831
3955
  }