@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
@@ -138,12 +138,69 @@ export function checkEgressForFiles(files, { scanRoot, model }) {
138
138
  // other endpoint needs --allow-remote (or review.allowRemote), on top of the egress policy below, never instead of it.
139
139
  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 };
140
140
  for (const file of files) {
141
- const d = evaluateEgress({ scanRoot, purpose: 'context-review', endpoint: model.endpoint, provider: model.provider, model: model.model, path: file });
142
- if (!d.allowed) return { allowed: false, reason: d.reason, file, evaluated: true };
141
+ // 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
142
+ // reason, and nothing is sent. This is the only place that decides whether a prompt may leave the machine.
143
+ let d;
144
+ try { d = evaluateEgress({ scanRoot, purpose: 'context-review', endpoint: model.endpoint, provider: model.provider, model: model.model, path: file }); }
145
+ catch (err) { return { allowed: false, reason: `egress-policy-error: ${String((err && err.message) || err).slice(0, 120)}`, file, evaluated: true, failedClosed: true }; }
146
+ if (!d || d.allowed !== true) return { allowed: false, reason: (d && d.reason) || 'egress-policy-returned-no-decision', file, evaluated: true, ...(d ? {} : { failedClosed: true }) };
143
147
  }
144
148
  return { allowed: true, evaluated: true };
145
149
  }
146
150
 
151
+ // ---- model output hygiene -------------------------------------------------------------------------------------
152
+
153
+ /**
154
+ * 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
155
+ * these strings reach reports, tickets and fix suggestions that people copy from, so shell-looking content is removed and counted, never passed on.
156
+ */
157
+ const FENCE = /```[\s\S]*?```|~~~[\s\S]*?~~~/g;
158
+ 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)';
159
+ 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');
160
+ export function stripUntrustedCommands(text) {
161
+ if (typeof text !== 'string' || !text) return { text: text ?? '', stripped: 0 };
162
+ let n = 0;
163
+ let out = text.replace(FENCE, () => { n++; return '[command block removed]'; });
164
+ out = out.replace(INLINE_CMD, () => { n++; return '[command removed]'; });
165
+ return { text: out, stripped: n };
166
+ }
167
+ function sanitizeReviewStrings(review) {
168
+ let total = 0;
169
+ const walk = (v, key) => {
170
+ if (typeof v === 'string') {
171
+ if (['promptHash', 'responseHash', 'contextHash', 'entrypointId', 'file', 'role', 'kind', 'conclusion', 'likelihood', 'questionPack', 'status'].includes(key)) return v;
172
+ const r = stripUntrustedCommands(v); total += r.stripped; return r.text;
173
+ }
174
+ if (Array.isArray(v)) return v.map((x) => walk(x, key));
175
+ if (v && typeof v === 'object') { const o = {}; for (const [k, x] of Object.entries(v)) o[k] = walk(x, k); return o; }
176
+ return v;
177
+ };
178
+ const out = walk(review, '');
179
+ if (total) out.untrustedCommandsStripped = total;
180
+ return out;
181
+ }
182
+
183
+ /** 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. */
184
+ function provenanceOf(model, promptHash) {
185
+ let host = null; try { host = model.endpoint ? new URL(model.endpoint).hostname : null; } catch { host = null; }
186
+ const loopback = host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
187
+ 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 };
188
+ }
189
+
190
+ // ---- prompt anonymization ---------------------------------------------------------------------------------------
191
+
192
+ /** Replace each real file path with a stable pseudonym (`f_<hash>.<ext>`). The mapping stays in memory; only the pseudonyms leave. */
193
+ export function pseudonymizer(files) {
194
+ const toPseudo = new Map(); const toReal = new Map();
195
+ for (const f of [...new Set(files)].sort()) {
196
+ const ext = (/\.[A-Za-z0-9]{1,6}$/.exec(f) || [''])[0];
197
+ const p = `f_${sha256(f).slice(0, 10)}${ext}`;
198
+ toPseudo.set(f, p); toReal.set(p, f);
199
+ }
200
+ 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; };
201
+ return { toPseudo: (t) => swap(t, toPseudo), toReal: (t) => swap(t, toReal), size: toPseudo.size };
202
+ }
203
+
147
204
  // ---- response parsing and verification ------------------------------------------------------------------------
148
205
 
149
206
  function extractJson(raw) {
@@ -352,7 +409,12 @@ export async function runReviewOnce({ pack, entrypoints, manifests, packageIndex
352
409
  const badRedact = redacted.find(r => !r.ok);
353
410
  if (badRedact) return { state: 'skipped_by_policy', reason: `redaction refused: ${badRedact.reason}` };
354
411
 
355
- const { prompt, promptHash } = buildReviewPrompt({ pack, entrypoints, packageTexts: redacted.map(r => r.text), contextHash: manifests[0].contextHash });
412
+ // Policy-driven anonymization: file paths are replaced by pseudonyms before the prompt is built, and mapped back in the answer before its citations
413
+ // are checked against the real snapshot. The model never sees a real path, and what is hashed is what was sent.
414
+ const anon = limits.anonymizePrompts ? pseudonymizer(files) : null;
415
+ const sendTexts = anon ? redacted.map(r => anon.toPseudo(r.text)) : redacted.map(r => r.text);
416
+ const sendEntrypoints = anon ? entrypoints.map(e => JSON.parse(anon.toPseudo(JSON.stringify(e)))) : entrypoints;
417
+ const { prompt, promptHash } = buildReviewPrompt({ pack, entrypoints: sendEntrypoints, packageTexts: sendTexts, contextHash: manifests[0].contextHash });
356
418
  const est = estimateTokens(prompt);
357
419
  if (est.tokens > limits.maxTokensPerJob) return { state: 'skipped_by_policy', reason: `prompt is about ${est.tokens} tokens, over the ${limits.maxTokensPerJob} per-job limit` };
358
420
 
@@ -364,7 +426,9 @@ export async function runReviewOnce({ pack, entrypoints, manifests, packageIndex
364
426
  return { state: 'failed', reason: `model call failed: ${String((err && err.message) || err).slice(0, 160)}`, retryable: isRetryableError(err) };
365
427
  }
366
428
  const usage = { inputTokensEstimated: est.tokens, outputTokensEstimated: Math.ceil(String(raw || '').length / 4), source: 'estimate', redactions: redacted.reduce((n, r) => n + r.redactions, 0) };
367
- const review = verifyReview({ raw, pack, entrypoints, manifests, packageIndexes, fileContents, promptHash });
429
+ const rawReal = anon && typeof raw === 'string' ? anon.toReal(raw) : raw;
430
+ const review0 = verifyReview({ raw: rawReal, pack, entrypoints, manifests, packageIndexes, fileContents, promptHash });
431
+ const review = { ...sanitizeReviewStrings(review0), modelProvenance: provenanceOf(model, promptHash), ...(anon ? { promptAnonymized: true } : {}) };
368
432
  if (review.status === 'declined') return { state: 'partial', reason: `answer declined: ${review.declineReasons.join(',')}`, review, usage };
369
433
  return { state: 'succeeded', reason: null, review, usage };
370
434
  }
@@ -67,6 +67,17 @@ export function pathIsCoveredByPrefix(path, prefix) {
67
67
  * - `set = {"x.z"}` does NOT cover "x.y" ✗
68
68
  * - `set = {"x.y.z"}` does NOT cover "x.y" ✗ (we don't propagate UP)
69
69
  */
70
+ /**
71
+ * True iff some path in `set` lies STRICTLY BELOW `path` (`o.cmd` is below `o`). Reading `o` as a whole value (passing it to a function, serialising
72
+ * it) reads every field, so a tainted field taints the whole read; reading `o.safe` does not, because `o.cmd` is not below `o.safe`.
73
+ */
74
+ export function hasTaintedDescendant(set, path) {
75
+ if (!set || typeof path !== 'string') return false;
76
+ const prefix = `${path}.`;
77
+ for (const p of set) if (typeof p === 'string' && p.startsWith(prefix)) return true;
78
+ return false;
79
+ }
80
+
70
81
  export function isCoveredBy(set, path) {
71
82
  if (!set || typeof path !== 'string') return false;
72
83
  if (set.has(path)) return true;
@@ -0,0 +1,66 @@
1
+ // Dynamic features the static analysis cannot resolve, reported instead of silently approximated (E2-03).
2
+ //
3
+ // Where the engine meets one of these it takes the sound direction (a write through an unknown key taints the whole container, a read through
4
+ // an unknown key reads the whole container) and loses precision. That is the right trade, but it must be visible: a result that depended on it is
5
+ // less precise than one that did not, and a reviewer should be able to see which functions contain such constructs.
6
+ //
7
+ // Pure: reads the Layer-1 IR only.
8
+ const LIMIT_KINDS = Object.freeze({
9
+ 'computed-write': 'a write through a key that is not a literal (`o[k] = v`): the engine taints the whole container `o`, so clean sibling fields read afterwards are over-approximated as tainted',
10
+ 'computed-read': 'a read through a key that is not a literal (`o[k]`): the engine cannot tell which field is read, so any tainted field of `o` reaches it',
11
+ 'eval-like': 'code built at run time (`eval`, `new Function`, `setTimeout` with a string): its content is not analysed',
12
+ 'dynamic-require': 'a module name computed at run time (`require(name)`, `import(name)`): the callee is unknown, so the call stays unresolved',
13
+ 'reflective-call': 'a call through reflection (`Reflect.apply`, `fn.call/apply` on a computed target, `obj[name]()`): the callee is unknown',
14
+ 'getter-or-proxy': 'property access that may run user code (`Proxy`, `Object.defineProperty` accessors): reads and writes may have effects the engine does not model',
15
+ });
16
+
17
+ const expressions = (e, acc = []) => {
18
+ if (!e || typeof e !== 'object') return acc;
19
+ acc.push(e);
20
+ for (const k of ['object', 'callee', 'left', 'right', 'value']) if (e[k] && typeof e[k] === 'object') expressions(e[k], acc);
21
+ for (const k of ['args', 'parts', 'elements', 'props', 'branches']) if (Array.isArray(e[k])) for (const x of e[k]) expressions(x && x.value && !x.kind ? x.value : x, acc);
22
+ return acc;
23
+ };
24
+ const nameOf = (c) => (typeof c === 'string' ? c : c && c.kind === 'ident' ? c.name : c && c.kind === 'member' ? `${nameOf(c.object)}.${c.prop}` : '');
25
+
26
+ /** @returns {{kind:string, line:number, detail:string, effect:string}[]} */
27
+ function limitsOfFunction(fn) {
28
+ const out = [];
29
+ const add = (kind, line, detail) => out.push({ kind, line: Number.isInteger(line) ? line : (fn.line || 0), detail, effect: LIMIT_KINDS[kind] });
30
+ for (const n of Object.values(fn.cfg?.nodes || {})) {
31
+ if (n.kind === 'assign') {
32
+ if (typeof n.target === 'string' && /\.\*$/.test(n.target)) add('computed-write', n.line, n.target);
33
+ for (const x of expressions(n.source)) inspect(x, n.line, add);
34
+ } else if (n.kind === 'call') {
35
+ inspect({ kind: 'call', callee: n.callee, args: n.args }, n.line, add);
36
+ for (const a of n.args || []) for (const x of expressions(a)) inspect(x, n.line, add);
37
+ } else if (n.kind === 'return' && n.value) for (const x of expressions(n.value)) inspect(x, n.line, add);
38
+ }
39
+ return out;
40
+ }
41
+
42
+ function inspect(x, line, add) {
43
+ if (x.kind === 'member' && x.prop === '*') add('computed-read', line, `${nameOf(x.object)}[…]`);
44
+ if (x.kind === 'call') {
45
+ const c = nameOf(x.callee);
46
+ if (/^(?:eval|Function|new Function|setTimeout|setInterval)$/.test(c)) { if (c === 'eval' || c === 'Function' || c === 'new Function' || (x.args && x.args[0] && x.args[0].kind !== 'function-value' && x.args[0].kind !== 'ident' && x.args[0].kind !== 'literal')) add('eval-like', line, c); else if ((c === 'setTimeout' || c === 'setInterval') && x.args && x.args[0] && x.args[0].kind === 'literal' && typeof x.args[0].value === 'string') add('eval-like', line, `${c}(string)`); }
47
+ if (c === 'require' && x.args && x.args[0] && x.args[0].kind !== 'literal') add('dynamic-require', line, 'require(<expression>)');
48
+ if (/^Reflect\.(?:apply|construct|get|set)$/.test(c)) add('reflective-call', line, c);
49
+ if (x.callee && x.callee.kind === 'member' && x.callee.prop === '*') add('reflective-call', line, `${nameOf(x.callee.object)}[…](…)`);
50
+ if (/^(?:Object\.defineProperty|Object\.defineProperties|Object\.setPrototypeOf)$/.test(c) || c === 'Proxy' || c === 'new Proxy') add('getter-or-proxy', line, c);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * @param {{perFile:Record<string,{functions:object[]}>}} projectIR
56
+ * @returns {{file:string, qid:string, function:string, kind:string, line:number, detail:string, effect:string}[]} sorted, stable
57
+ */
58
+ export function analysisLimits(projectIR) {
59
+ const out = [];
60
+ for (const file of Object.keys(projectIR?.perFile || {}).sort()) {
61
+ for (const fn of projectIR.perFile[file].functions || []) {
62
+ for (const l of limitsOfFunction(fn)) out.push({ file, qid: String(fn.qid).replace(/@\d+$/, ''), function: fn.name, ...l });
63
+ }
64
+ }
65
+ return out.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line || (a.kind < b.kind ? -1 : 1)));
66
+ }