circle-ir 4.17.0 → 4.18.1

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 (280) hide show
  1. package/dist/analysis/advisories.d.ts +186 -0
  2. package/dist/analysis/advisories.d.ts.map +1 -0
  3. package/dist/analysis/advisories.js +391 -0
  4. package/dist/analysis/advisories.js.map +1 -0
  5. package/dist/analysis/comment-lines.d.ts +14 -0
  6. package/dist/analysis/comment-lines.d.ts.map +1 -0
  7. package/dist/analysis/comment-lines.js +34 -0
  8. package/dist/analysis/comment-lines.js.map +1 -0
  9. package/dist/analysis/config-loader.d.ts.map +1 -1
  10. package/dist/analysis/config-loader.js +59 -5
  11. package/dist/analysis/config-loader.js.map +1 -1
  12. package/dist/analysis/constant-caller-gate.d.ts +2 -2
  13. package/dist/analysis/constant-caller-gate.d.ts.map +1 -1
  14. package/dist/analysis/constant-caller-gate.js +205 -29
  15. package/dist/analysis/constant-caller-gate.js.map +1 -1
  16. package/dist/analysis/constant-propagation/propagator.d.ts +4 -0
  17. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  18. package/dist/analysis/constant-propagation/propagator.js +99 -0
  19. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  20. package/dist/analysis/entry-point-detection.d.ts.map +1 -1
  21. package/dist/analysis/entry-point-detection.js +72 -0
  22. package/dist/analysis/entry-point-detection.js.map +1 -1
  23. package/dist/analysis/interprocedural.d.ts.map +1 -1
  24. package/dist/analysis/interprocedural.js +75 -8
  25. package/dist/analysis/interprocedural.js.map +1 -1
  26. package/dist/analysis/maven-declared.d.ts +72 -0
  27. package/dist/analysis/maven-declared.d.ts.map +1 -0
  28. package/dist/analysis/maven-declared.js +216 -0
  29. package/dist/analysis/maven-declared.js.map +1 -0
  30. package/dist/analysis/maven-version.d.ts +28 -0
  31. package/dist/analysis/maven-version.d.ts.map +1 -0
  32. package/dist/analysis/maven-version.js +172 -0
  33. package/dist/analysis/maven-version.js.map +1 -0
  34. package/dist/analysis/passes/constant-propagation-pass.d.ts.map +1 -1
  35. package/dist/analysis/passes/constant-propagation-pass.js +29 -1
  36. package/dist/analysis/passes/constant-propagation-pass.js.map +1 -1
  37. package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
  38. package/dist/analysis/passes/cross-file-pass.js +81 -1
  39. package/dist/analysis/passes/cross-file-pass.js.map +1 -1
  40. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +7 -0
  41. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
  42. package/dist/analysis/passes/csharp-sanitizer-credit.js +44 -1
  43. package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
  44. package/dist/analysis/passes/insecure-cookie-pass.d.ts +2 -0
  45. package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
  46. package/dist/analysis/passes/insecure-cookie-pass.js +25 -3
  47. package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
  48. package/dist/analysis/passes/interprocedural-pass.d.ts +11 -14
  49. package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
  50. package/dist/analysis/passes/interprocedural-pass.js +562 -0
  51. package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
  52. package/dist/analysis/passes/language-sources-pass.d.ts +18 -1
  53. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  54. package/dist/analysis/passes/language-sources-pass.js +1057 -14
  55. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  56. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
  57. package/dist/analysis/passes/param-source-flow-gate-pass.js +66 -3
  58. package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
  59. package/dist/analysis/passes/sink-filter-pass.d.ts +44 -2
  60. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  61. package/dist/analysis/passes/sink-filter-pass.js +690 -12
  62. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  63. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  64. package/dist/analysis/passes/taint-propagation-pass.js +133 -10
  65. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  66. package/dist/analysis/passes/tls-verify-disabled-pass.d.ts +18 -0
  67. package/dist/analysis/passes/tls-verify-disabled-pass.d.ts.map +1 -1
  68. package/dist/analysis/passes/tls-verify-disabled-pass.js +161 -0
  69. package/dist/analysis/passes/tls-verify-disabled-pass.js.map +1 -1
  70. package/dist/analysis/passes/weak-random-pass.d.ts +6 -0
  71. package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
  72. package/dist/analysis/passes/weak-random-pass.js +29 -0
  73. package/dist/analysis/passes/weak-random-pass.js.map +1 -1
  74. package/dist/analysis/promote-cross-file-flows.d.ts +17 -0
  75. package/dist/analysis/promote-cross-file-flows.d.ts.map +1 -0
  76. package/dist/analysis/promote-cross-file-flows.js +32 -0
  77. package/dist/analysis/promote-cross-file-flows.js.map +1 -0
  78. package/dist/analysis/sbom.d.ts +100 -0
  79. package/dist/analysis/sbom.d.ts.map +1 -1
  80. package/dist/analysis/sbom.js +514 -3
  81. package/dist/analysis/sbom.js.map +1 -1
  82. package/dist/analysis/semver-version.d.ts +30 -0
  83. package/dist/analysis/semver-version.d.ts.map +1 -0
  84. package/dist/analysis/semver-version.js +76 -0
  85. package/dist/analysis/semver-version.js.map +1 -0
  86. package/dist/analysis/taint-matcher.d.ts +5 -0
  87. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  88. package/dist/analysis/taint-matcher.js +225 -4
  89. package/dist/analysis/taint-matcher.js.map +1 -1
  90. package/dist/analyzer.d.ts +57 -0
  91. package/dist/analyzer.d.ts.map +1 -1
  92. package/dist/analyzer.js +69 -6
  93. package/dist/analyzer.js.map +1 -1
  94. package/dist/browser/circle-ir.js +2936 -291
  95. package/dist/core/circle-ir-core.cjs +634 -203
  96. package/dist/core/circle-ir-core.js +634 -203
  97. package/dist/core/extractors/dfg.js +205 -2
  98. package/dist/core/extractors/dfg.js.map +1 -1
  99. package/dist/core/extractors/ts-declaration-detail.d.ts +39 -0
  100. package/dist/core/extractors/ts-declaration-detail.d.ts.map +1 -0
  101. package/dist/core/extractors/ts-declaration-detail.js +266 -0
  102. package/dist/core/extractors/ts-declaration-detail.js.map +1 -0
  103. package/dist/graph/analysis-pass.d.ts +4 -1
  104. package/dist/graph/analysis-pass.d.ts.map +1 -1
  105. package/dist/graph/analysis-pass.js +4 -1
  106. package/dist/graph/analysis-pass.js.map +1 -1
  107. package/dist/graph-facts/dead-code.d.ts +62 -0
  108. package/dist/graph-facts/dead-code.d.ts.map +1 -0
  109. package/dist/graph-facts/dead-code.js +90 -0
  110. package/dist/graph-facts/dead-code.js.map +1 -0
  111. package/dist/graph-facts/dependency-calls.d.ts +86 -0
  112. package/dist/graph-facts/dependency-calls.d.ts.map +1 -0
  113. package/dist/graph-facts/dependency-calls.js +93 -0
  114. package/dist/graph-facts/dependency-calls.js.map +1 -0
  115. package/dist/graph-facts/index.d.ts +10 -0
  116. package/dist/graph-facts/index.d.ts.map +1 -0
  117. package/dist/graph-facts/index.js +10 -0
  118. package/dist/graph-facts/index.js.map +1 -0
  119. package/dist/graph-facts/maven-packages.d.ts +42 -0
  120. package/dist/graph-facts/maven-packages.d.ts.map +1 -0
  121. package/dist/graph-facts/maven-packages.js +133 -0
  122. package/dist/graph-facts/maven-packages.js.map +1 -0
  123. package/dist/graph-facts/reach.d.ts +203 -0
  124. package/dist/graph-facts/reach.d.ts.map +1 -0
  125. package/dist/graph-facts/reach.js +219 -0
  126. package/dist/graph-facts/reach.js.map +1 -0
  127. package/dist/graph-facts/removal-reach.d.ts +73 -0
  128. package/dist/graph-facts/removal-reach.d.ts.map +1 -0
  129. package/dist/graph-facts/removal-reach.js +64 -0
  130. package/dist/graph-facts/removal-reach.js.map +1 -0
  131. package/dist/index.d.ts +21 -6
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.js +20 -3
  134. package/dist/index.js.map +1 -1
  135. package/dist/navigation/go-graph.d.ts +68 -0
  136. package/dist/navigation/go-graph.d.ts.map +1 -0
  137. package/dist/navigation/go-graph.js +753 -0
  138. package/dist/navigation/go-graph.js.map +1 -0
  139. package/dist/navigation/index.d.ts +7 -1
  140. package/dist/navigation/index.d.ts.map +1 -1
  141. package/dist/navigation/index.js +6 -0
  142. package/dist/navigation/index.js.map +1 -1
  143. package/dist/navigation/java-entry-tables.d.ts +43 -0
  144. package/dist/navigation/java-entry-tables.d.ts.map +1 -0
  145. package/dist/navigation/java-entry-tables.js +56 -0
  146. package/dist/navigation/java-entry-tables.js.map +1 -0
  147. package/dist/navigation/java-graph.d.ts +64 -0
  148. package/dist/navigation/java-graph.d.ts.map +1 -0
  149. package/dist/navigation/java-graph.js +466 -0
  150. package/dist/navigation/java-graph.js.map +1 -0
  151. package/dist/navigation/navigation-index.d.ts +51 -1
  152. package/dist/navigation/navigation-index.d.ts.map +1 -1
  153. package/dist/navigation/navigation-index.js +72 -0
  154. package/dist/navigation/navigation-index.js.map +1 -1
  155. package/dist/navigation/python-graph.d.ts +73 -0
  156. package/dist/navigation/python-graph.d.ts.map +1 -0
  157. package/dist/navigation/python-graph.js +867 -0
  158. package/dist/navigation/python-graph.js.map +1 -0
  159. package/dist/navigation/ts-call-edges.d.ts +193 -0
  160. package/dist/navigation/ts-call-edges.d.ts.map +1 -0
  161. package/dist/navigation/ts-call-edges.js +2089 -0
  162. package/dist/navigation/ts-call-edges.js.map +1 -0
  163. package/dist/navigation/ts-entry-tables.d.ts +88 -0
  164. package/dist/navigation/ts-entry-tables.d.ts.map +1 -0
  165. package/dist/navigation/ts-entry-tables.js +50 -0
  166. package/dist/navigation/ts-entry-tables.js.map +1 -0
  167. package/dist/path-set/branches.d.ts +52 -0
  168. package/dist/path-set/branches.d.ts.map +1 -0
  169. package/dist/path-set/branches.js +124 -0
  170. package/dist/path-set/branches.js.map +1 -0
  171. package/dist/path-set/finding-diff.d.ts +63 -0
  172. package/dist/path-set/finding-diff.d.ts.map +1 -0
  173. package/dist/path-set/finding-diff.js +150 -0
  174. package/dist/path-set/finding-diff.js.map +1 -0
  175. package/dist/path-set/index.d.ts +18 -0
  176. package/dist/path-set/index.d.ts.map +1 -0
  177. package/dist/path-set/index.js +12 -0
  178. package/dist/path-set/index.js.map +1 -0
  179. package/dist/path-set/line-diff.d.ts +19 -0
  180. package/dist/path-set/line-diff.d.ts.map +1 -0
  181. package/dist/path-set/line-diff.js +98 -0
  182. package/dist/path-set/line-diff.js.map +1 -0
  183. package/dist/path-set/path-set.d.ts +88 -0
  184. package/dist/path-set/path-set.d.ts.map +1 -0
  185. package/dist/path-set/path-set.js +716 -0
  186. package/dist/path-set/path-set.js.map +1 -0
  187. package/dist/path-set/templates.d.ts +42 -0
  188. package/dist/path-set/templates.d.ts.map +1 -0
  189. package/dist/path-set/templates.js +119 -0
  190. package/dist/path-set/templates.js.map +1 -0
  191. package/dist/path-set/types.d.ts +195 -0
  192. package/dist/path-set/types.d.ts.map +1 -0
  193. package/dist/path-set/types.js +8 -0
  194. package/dist/path-set/types.js.map +1 -0
  195. package/dist/remedy-class/index.d.ts +12 -0
  196. package/dist/remedy-class/index.d.ts.map +1 -0
  197. package/dist/remedy-class/index.js +9 -0
  198. package/dist/remedy-class/index.js.map +1 -0
  199. package/dist/remedy-class/match.d.ts +165 -0
  200. package/dist/remedy-class/match.d.ts.map +1 -0
  201. package/dist/remedy-class/match.js +705 -0
  202. package/dist/remedy-class/match.js.map +1 -0
  203. package/dist/remedy-class/output-context.d.ts +25 -0
  204. package/dist/remedy-class/output-context.d.ts.map +1 -0
  205. package/dist/remedy-class/output-context.js +150 -0
  206. package/dist/remedy-class/output-context.js.map +1 -0
  207. package/dist/remedy-class/sha256.d.ts +18 -0
  208. package/dist/remedy-class/sha256.d.ts.map +1 -0
  209. package/dist/remedy-class/sha256.js +89 -0
  210. package/dist/remedy-class/sha256.js.map +1 -0
  211. package/dist/remedy-class/table.d.ts +130 -0
  212. package/dist/remedy-class/table.d.ts.map +1 -0
  213. package/dist/remedy-class/table.js +457 -0
  214. package/dist/remedy-class/table.js.map +1 -0
  215. package/dist/resolution/cross-file.d.ts +9 -0
  216. package/dist/resolution/cross-file.d.ts.map +1 -1
  217. package/dist/resolution/cross-file.js +60 -5
  218. package/dist/resolution/cross-file.js.map +1 -1
  219. package/dist/stable-ids/builder.d.ts +20 -0
  220. package/dist/stable-ids/builder.d.ts.map +1 -0
  221. package/dist/stable-ids/builder.js +65 -0
  222. package/dist/stable-ids/builder.js.map +1 -0
  223. package/dist/stable-ids/diff.d.ts +18 -0
  224. package/dist/stable-ids/diff.d.ts.map +1 -0
  225. package/dist/stable-ids/diff.js +37 -0
  226. package/dist/stable-ids/diff.js.map +1 -0
  227. package/dist/stable-ids/go.d.ts +28 -0
  228. package/dist/stable-ids/go.d.ts.map +1 -0
  229. package/dist/stable-ids/go.js +160 -0
  230. package/dist/stable-ids/go.js.map +1 -0
  231. package/dist/stable-ids/hash.d.ts +22 -0
  232. package/dist/stable-ids/hash.d.ts.map +1 -0
  233. package/dist/stable-ids/hash.js +167 -0
  234. package/dist/stable-ids/hash.js.map +1 -0
  235. package/dist/stable-ids/index.d.ts +30 -0
  236. package/dist/stable-ids/index.d.ts.map +1 -0
  237. package/dist/stable-ids/index.js +59 -0
  238. package/dist/stable-ids/index.js.map +1 -0
  239. package/dist/stable-ids/java.d.ts +28 -0
  240. package/dist/stable-ids/java.d.ts.map +1 -0
  241. package/dist/stable-ids/java.js +224 -0
  242. package/dist/stable-ids/java.js.map +1 -0
  243. package/dist/stable-ids/moves.d.ts +87 -0
  244. package/dist/stable-ids/moves.d.ts.map +1 -0
  245. package/dist/stable-ids/moves.js +222 -0
  246. package/dist/stable-ids/moves.js.map +1 -0
  247. package/dist/stable-ids/nodes.d.ts +32 -0
  248. package/dist/stable-ids/nodes.d.ts.map +1 -0
  249. package/dist/stable-ids/nodes.js +92 -0
  250. package/dist/stable-ids/nodes.js.map +1 -0
  251. package/dist/stable-ids/normalize.d.ts +35 -0
  252. package/dist/stable-ids/normalize.d.ts.map +1 -0
  253. package/dist/stable-ids/normalize.js +174 -0
  254. package/dist/stable-ids/normalize.js.map +1 -0
  255. package/dist/stable-ids/parse-errors.d.ts +24 -0
  256. package/dist/stable-ids/parse-errors.d.ts.map +1 -0
  257. package/dist/stable-ids/parse-errors.js +46 -0
  258. package/dist/stable-ids/parse-errors.js.map +1 -0
  259. package/dist/stable-ids/python.d.ts +27 -0
  260. package/dist/stable-ids/python.d.ts.map +1 -0
  261. package/dist/stable-ids/python.js +212 -0
  262. package/dist/stable-ids/python.js.map +1 -0
  263. package/dist/stable-ids/ts-parse-errors.d.ts +27 -0
  264. package/dist/stable-ids/ts-parse-errors.d.ts.map +1 -0
  265. package/dist/stable-ids/ts-parse-errors.js +75 -0
  266. package/dist/stable-ids/ts-parse-errors.js.map +1 -0
  267. package/dist/stable-ids/types.d.ts +106 -0
  268. package/dist/stable-ids/types.d.ts.map +1 -0
  269. package/dist/stable-ids/types.js +16 -0
  270. package/dist/stable-ids/types.js.map +1 -0
  271. package/dist/stable-ids/typescript.d.ts +30 -0
  272. package/dist/stable-ids/typescript.d.ts.map +1 -0
  273. package/dist/stable-ids/typescript.js +362 -0
  274. package/dist/stable-ids/typescript.js.map +1 -0
  275. package/dist/types/config.d.ts +9 -0
  276. package/dist/types/config.d.ts.map +1 -1
  277. package/dist/types/index.d.ts +35 -0
  278. package/dist/types/index.d.ts.map +1 -1
  279. package/dist/types/index.js.map +1 -1
  280. package/package.json +3 -3
@@ -0,0 +1,705 @@
1
+ /**
2
+ * Remedy-class matcher: for every sink of a path that a version pair closed,
3
+ * report which row of a caller-supplied `safe-construct-table@0.1` the fixed
4
+ * code matches, or that none does, or that the matcher could not observe a
5
+ * row. Facts only: `class` and `remedyClass` are copied from the table, and
6
+ * nothing here judges whether a fix is adequate.
7
+ *
8
+ * Steps are looked for in the same declaration at the fix version as the
9
+ * baseline sink, on the IR circle-ir already produces: calls (receiver,
10
+ * receiver type, arguments, imports), DFG defs, uses and def-to-def chains,
11
+ * and the fix version's taint sources.
12
+ *
13
+ * Observed: `call` (type, methods, arg, ctor, static), `arg-is: tainted |
14
+ * untainted | from-allowlist`, `arg-literal-in`, `role: sink-replacement |
15
+ * binder | sanitizer | guard | normalizer`, `on`, `name` / `order`, `any`,
16
+ * `all`, `missing`, `sink-removed`. A guard needs branch facts
17
+ * (`deriveBranchFacts`) for the fix version's files. Not observed:
18
+ * `template` and `receiver-from` on a step. A row that needs a step it
19
+ * cannot observe **abstains**; it is never reported as unmatched.
20
+ *
21
+ * Pure: IR in, values out. Browser and Node compatible.
22
+ */
23
+ import { sideOf } from '../path-set/branches.js';
24
+ import { handedNames, lineDominates, qualifiedMethodName, typePath } from '../path-set/path-set.js';
25
+ import { htmlContextAt, javaStringLiterals, markupBefore } from './output-context.js';
26
+ /** Thrown when the table was refused by the reader: no observation exists on a refused table. */
27
+ export class TableRefused extends Error {
28
+ problems;
29
+ digest;
30
+ constructor(digest, problems) {
31
+ super(`safe-construct table ${digest} refused: ${problems.length} problem(s); first: ${problems[0]?.path} ${problems[0]?.message}`);
32
+ this.name = 'TableRefused';
33
+ this.problems = problems;
34
+ this.digest = digest;
35
+ }
36
+ }
37
+ function inRange(line, r) {
38
+ return line >= r.start && line <= r.end;
39
+ }
40
+ /** The line range of a declaration at one version, or undefined when it is gone. */
41
+ function declRange(file, ir, decl, ids) {
42
+ if (decl.scheme === 'ids@0.1') {
43
+ const d = ids?.decls.find(x => x.id === decl.id);
44
+ return d ? { start: d.start_line, end: d.end_line } : undefined;
45
+ }
46
+ const prefix = `${file}#`;
47
+ if (!decl.id.startsWith(prefix))
48
+ return undefined;
49
+ const qualified = decl.id.slice(prefix.length);
50
+ for (const t of ir.types) {
51
+ for (const m of t.methods) {
52
+ if (qualifiedMethodName(t, m) === qualified)
53
+ return { start: m.start_line, end: m.end_line };
54
+ }
55
+ if (qualified === `${typePath(t)}.<init>`)
56
+ return { start: t.start_line, end: t.end_line };
57
+ }
58
+ if (qualified.startsWith('<module>'))
59
+ return 'by-method';
60
+ return undefined;
61
+ }
62
+ const IDENT = /[A-Za-z_$][\w$]*/g;
63
+ const STRING_LITERAL = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g;
64
+ /** Identifiers in an expression, string literals removed. */
65
+ function identifiers(expr) {
66
+ return new Set((expr ?? '').replace(STRING_LITERAL, '""').match(IDENT) ?? []);
67
+ }
68
+ function buildCtx(file, ir, decl, ids, seedParameters) {
69
+ const range = declRange(file, ir, decl, ids);
70
+ if (!range)
71
+ return undefined;
72
+ let calls;
73
+ let lineOk;
74
+ if (range === 'by-method') {
75
+ const method = decl.name.startsWith('<module>.') ? decl.name.slice('<module>.'.length) : null;
76
+ calls = ir.calls.filter(c => (c.in_method ?? null) === method);
77
+ const lines = new Set(calls.map(c => c.location.line));
78
+ lineOk = l => lines.has(l);
79
+ }
80
+ else {
81
+ calls = ir.calls.filter(c => inRange(c.location.line, range));
82
+ lineOk = l => inRange(l, range);
83
+ }
84
+ const defs = ir.dfg.defs.filter(d => lineOk(d.line));
85
+ const defIds = new Set(defs.map(d => d.id));
86
+ const next = new Map();
87
+ for (const ch of ir.dfg.chains ?? []) {
88
+ if (!defIds.has(ch.from_def) || !defIds.has(ch.to_def))
89
+ continue;
90
+ const list = next.get(ch.from_def);
91
+ if (list)
92
+ list.push(ch.to_def);
93
+ else
94
+ next.set(ch.from_def, [ch.to_def]);
95
+ }
96
+ const reachCache = new Map();
97
+ const reach = (from) => {
98
+ const hit = reachCache.get(from);
99
+ if (hit)
100
+ return hit;
101
+ const seen = new Set([from]);
102
+ const stack = [from];
103
+ while (stack.length) {
104
+ const d = stack.pop();
105
+ for (const n of next.get(d) ?? []) {
106
+ if (!seen.has(n)) {
107
+ seen.add(n);
108
+ stack.push(n);
109
+ }
110
+ }
111
+ }
112
+ reachCache.set(from, seen);
113
+ return seen;
114
+ };
115
+ const seeds = [];
116
+ for (const s of ir.taint.sources) {
117
+ if (!lineOk(s.line))
118
+ continue;
119
+ for (const d of defs) {
120
+ if (d.line === s.line && (s.variable === undefined || d.variable === s.variable))
121
+ seeds.push(d.id);
122
+ }
123
+ }
124
+ if (seedParameters)
125
+ for (const d of defs)
126
+ if (d.kind === 'param')
127
+ seeds.push(d.id);
128
+ const tainted = new Set();
129
+ for (const s of seeds)
130
+ for (const d of reach(s))
131
+ tainted.add(d);
132
+ return { file, ir, calls, defs, tainted, reach };
133
+ }
134
+ /** Uses at `line` whose variable appears in `expr`, as their reaching def ids. */
135
+ function defsUsedIn(ctx, line, expr) {
136
+ const names = identifiers(expr);
137
+ const out = [];
138
+ for (const u of ctx.ir.dfg.uses) {
139
+ if (u.line === line && u.def_id !== null && names.has(u.variable))
140
+ out.push(u.def_id);
141
+ }
142
+ return out;
143
+ }
144
+ /** Does a source call appear inline in the expression at this line (`f(req.getParameter("x"))`)? */
145
+ function inlineSource(ctx, line, expr) {
146
+ return ctx.ir.taint.sources.some(s => {
147
+ if (s.line !== line)
148
+ return false;
149
+ const name = (s.method ?? s.location.split(' in ')[0].replace(/\(\)$/, '').split('.').pop() ?? '').trim();
150
+ return name.length > 0 && expr.includes(`${name}(`);
151
+ });
152
+ }
153
+ function argTainted(ctx, call, expr) {
154
+ if (inlineSource(ctx, call.location.line, expr))
155
+ return true;
156
+ return defsUsedIn(ctx, call.location.line, expr).some(d => ctx.tainted.has(d));
157
+ }
158
+ // ---------------------------------------------------------------------------
159
+ // Call shapes
160
+ // ---------------------------------------------------------------------------
161
+ const tail = (fqn) => fqn.slice(fqn.lastIndexOf('.') + 1);
162
+ const pkg = (fqn) => fqn.slice(0, Math.max(0, fqn.lastIndexOf('.')));
163
+ const RANK = { fqn: 0, import: 1, any: 2, 'simple-name': 3, unresolved: 4, template: 5 };
164
+ function isCtor(c) {
165
+ return c.is_constructor === true || (c.receiver === null && !!c.receiver_type && c.receiver_type === c.method_name);
166
+ }
167
+ /** A receiver written as a type (`Runtime`, `org.owasp.encoder.Encode`): a static call. */
168
+ function staticReceiver(c) {
169
+ const r = c.receiver;
170
+ if (!r || r.includes('('))
171
+ return null;
172
+ const last = tail(r);
173
+ return /^[A-Z]/.test(last) ? r : null;
174
+ }
175
+ function typeMatch(want, c, ir) {
176
+ if (want === 'java.lang.Object')
177
+ return 'any';
178
+ const fqn = c.receiver_type_fqn ?? (staticReceiver(c)?.includes('.') ? staticReceiver(c) : null);
179
+ if (fqn)
180
+ return fqn === want ? 'fqn' : null;
181
+ const simple = c.receiver_type ?? staticReceiver(c);
182
+ if (!simple) {
183
+ // receiver is a call expression or an untyped variable; an unqualified call is the enclosing class
184
+ return c.receiver === null && !isCtor(c) ? null : 'unresolved';
185
+ }
186
+ if (simple.includes('.'))
187
+ return simple === want ? 'fqn' : null;
188
+ if (simple !== tail(want))
189
+ return null;
190
+ const explicit = ir.imports.find(i => !i.is_wildcard && i.imported_name === simple);
191
+ if (explicit)
192
+ return `${explicit.from_package}.${simple}` === want ? 'import' : null;
193
+ if (ir.imports.some(i => i.is_wildcard && i.from_package === pkg(want)))
194
+ return 'import';
195
+ if (pkg(want) === 'java.lang')
196
+ return 'import';
197
+ return 'simple-name';
198
+ }
199
+ function methodMatches(patterns, name) {
200
+ return patterns.some(p => p === '*' || p === name || (p.endsWith('*') && name.startsWith(p.slice(0, -1))));
201
+ }
202
+ function shapeMatch(want, c, ir) {
203
+ if (want.ctor) {
204
+ if (!isCtor(c))
205
+ return null;
206
+ if (want.type === 'java.lang.Object')
207
+ return 'any';
208
+ const simple = c.receiver_type ?? c.method_name;
209
+ return typeMatch(want.type, { ...c, receiver_type: simple, receiver: simple }, ir);
210
+ }
211
+ if (isCtor(c)) {
212
+ // `methods: ["*"]` on `java.lang.Object` is any call at the sink, constructors included
213
+ return want.type === 'java.lang.Object' && want.methods.includes('*') ? 'any' : null;
214
+ }
215
+ if (!methodMatches(want.methods, c.method_name))
216
+ return null;
217
+ return typeMatch(want.type, c, ir);
218
+ }
219
+ function argsAt(c, arg) {
220
+ if (arg === undefined)
221
+ return null;
222
+ if (arg === 'all')
223
+ return c.arguments.map(a => a.expression);
224
+ const a = c.arguments.find(x => x.position === arg);
225
+ return a ? [a.expression] : [];
226
+ }
227
+ const VECTOR_LITERAL = /^\s*(?:new\s+[\w.]+\s*\[\s*\]\s*\{|[\w.]+\.(?:of|asList)\s*\()/;
228
+ /** Element 0 of an array or list literal (`new String[]{a, b}`, `List.of(a, b)`), as text; else null. */
229
+ function leadingElement(expr) {
230
+ const m = VECTOR_LITERAL.exec(expr);
231
+ if (!m)
232
+ return null;
233
+ const rest = expr.slice(m[0].length);
234
+ let depth = 0;
235
+ let quote = null;
236
+ for (let i = 0; i < rest.length; i++) {
237
+ const ch = rest[i];
238
+ if (quote) {
239
+ if (ch === '\\')
240
+ i++;
241
+ else if (ch === quote)
242
+ quote = null;
243
+ continue;
244
+ }
245
+ if (ch === '"' || ch === "'")
246
+ quote = ch;
247
+ else if (ch === '(' || ch === '[' || ch === '{')
248
+ depth++;
249
+ else if ((ch === ')' || ch === ']' || ch === '}') && depth > 0)
250
+ depth--;
251
+ else if ((ch === ',' || ch === ')' || ch === '}') && depth === 0)
252
+ return rest.slice(0, i).trim();
253
+ }
254
+ return rest.trim() || null;
255
+ }
256
+ /** The literal at an argument, or element 0 of an array or list literal there. */
257
+ function leadingLiteral(c, arg) {
258
+ const a = c.arguments.find(x => x.position === (arg === 'all' || arg === undefined ? 0 : arg));
259
+ if (!a)
260
+ return null;
261
+ if (a.literal !== null && a.literal !== undefined)
262
+ return a.literal.trim();
263
+ const first = leadingElement(a.expression);
264
+ const m = first === null ? null : /^"((?:[^"\\]|\\.)*)"$/.exec(first);
265
+ return m ? m[1].trim() : null;
266
+ }
267
+ /** Def produced by a call: a def on its line when the call is the outermost call there. */
268
+ function resultDef(ctx, c) {
269
+ const line = c.location.line;
270
+ const others = ctx.calls.filter(x => x !== c && x.location.line === line);
271
+ const inner = others.some(o => (o.receiver ?? '').includes(`${c.method_name}(`) || o.arguments.some(a => a.expression.includes(`${c.method_name}(`)));
272
+ if (inner)
273
+ return undefined;
274
+ // several defs on one line (`try (Connection c = ..; PreparedStatement ps = ..)`): the def written
275
+ // nearest before the call, by column; without columns, the only def on the line
276
+ const onLine = ctx.defs.filter(d => d.line === line && d.kind !== 'param');
277
+ if (onLine.length <= 1)
278
+ return onLine[0];
279
+ const before = onLine
280
+ .filter(d => d.column !== undefined && d.column <= c.location.column)
281
+ .sort((a, b) => b.column - a.column);
282
+ return before[0];
283
+ }
284
+ /** The call on the same line whose receiver or an argument holds `c` (`c` is nested in it). */
285
+ function enclosingCall(ctx, c) {
286
+ return ctx.calls.find(o => o !== c && o.location.line === c.location.line &&
287
+ ((o.receiver ?? '').includes(`${c.method_name}(`) || o.arguments.some(a => a.expression.includes(`${c.method_name}(`))));
288
+ }
289
+ /** Does `a`'s result (or receiver object) reach `b`'s receiver or an argument? */
290
+ function reaches(ctx, a, b, depth = 0) {
291
+ const texts = [b.receiver ?? '', ...b.arguments.map(x => x.expression)];
292
+ if (texts.some(t => t.includes(`${a.method_name}(`)) && a.location.line === b.location.line)
293
+ return true;
294
+ // a nested call reaches whatever the call holding it reaches (`base.resolve(n).normalize()`)
295
+ const outer = enclosingCall(ctx, a);
296
+ if (outer && depth < 8)
297
+ return outer === b || reaches(ctx, outer, b, depth + 1);
298
+ const def = resultDef(ctx, a);
299
+ if (def) {
300
+ const reach = ctx.reach(def.id);
301
+ return texts.some(t => defsUsedIn(ctx, b.location.line, t).some(d => reach.has(d)));
302
+ }
303
+ return false;
304
+ }
305
+ function unobservableReason(step, env) {
306
+ if (step.template && env.templateFacts === null)
307
+ return 'template-not-read';
308
+ if (step.call?.receiverFrom)
309
+ return 'step-not-observed:receiver-from';
310
+ if (step.argLiteralIn && !step.call)
311
+ return 'step-not-observed:arg-literal-in-without-call';
312
+ if ((step.role === 'guard' || step.on !== undefined) && env.ctx && !env.ctx.branches)
313
+ return 'step-not-observed:guard-without-branch-facts';
314
+ return undefined;
315
+ }
316
+ function evalStep(step, path, env) {
317
+ const why = unobservableReason(step, env);
318
+ if (why) {
319
+ env.observed.push({ path, holds: false });
320
+ return { holds: false, unobservable: why, hits: [] };
321
+ }
322
+ let result;
323
+ if (step.any || step.all) {
324
+ const children = (step.any ?? step.all);
325
+ const q = step.any ? 'any' : 'all';
326
+ const rs = children.filter(c => !(c.order && Object.keys(c).length === 1))
327
+ .map((c) => evalStep(c, `${path}/${q}/${children.indexOf(c)}`, env));
328
+ if (step.any) {
329
+ const holds = rs.some(r => r.holds);
330
+ result = { holds, hits: rs.flatMap(r => r.hits) };
331
+ if (!holds) {
332
+ const u = rs.find(r => r.unobservable)?.unobservable;
333
+ if (u)
334
+ result.unobservable = u;
335
+ }
336
+ }
337
+ else {
338
+ const failed = rs.filter(r => !r.holds);
339
+ const u = failed.find(r => r.unobservable)?.unobservable;
340
+ result = { holds: failed.length === 0, hits: rs.flatMap(r => r.hits) };
341
+ if (u && failed.every(r => r.unobservable))
342
+ result.unobservable = u;
343
+ // order steps after their siblings have bound their names
344
+ children.forEach((c, i) => {
345
+ if (!(c.order && Object.keys(c).length === 1))
346
+ return;
347
+ const holds = result.holds && orderHolds(c.order, env);
348
+ env.observed.push({ path: `${path}/${q}/${i}`, holds });
349
+ if (!holds)
350
+ result.holds = false;
351
+ });
352
+ }
353
+ }
354
+ else if (step.missing) {
355
+ const inner = evalStep(step.missing, `${path}/missing`, env);
356
+ result = inner.unobservable ? { holds: false, unobservable: inner.unobservable, hits: [] } : { holds: !inner.holds, hits: [] };
357
+ env.observed.push({ path, holds: result.holds });
358
+ }
359
+ else if (step.sinkRemoved) {
360
+ const holds = env.pathRemoved && env.sinkCalls.length === 0;
361
+ result = { holds, hits: [] };
362
+ env.observed.push({ path, holds });
363
+ }
364
+ else if (step.template) {
365
+ const want = step.template;
366
+ const hit = (env.templateFacts ?? []).find(f => templateMatches(want, f));
367
+ result = { holds: hit !== undefined, hits: [] };
368
+ const obs = { path, holds: result.holds };
369
+ if (hit) {
370
+ obs.at = { file: hit.file, line: hit.line };
371
+ obs.typeMatch = 'template';
372
+ }
373
+ env.observed.push(obs);
374
+ }
375
+ else if (step.call) {
376
+ const hits = callHits(step, env);
377
+ result = { holds: hits.length > 0, hits };
378
+ const obs = { path, holds: result.holds };
379
+ if (hits[0]) {
380
+ obs.at = { file: env.ctx?.file ?? '', line: hits[0].call.location.line };
381
+ obs.typeMatch = hits[0].typeMatch;
382
+ }
383
+ env.observed.push(obs);
384
+ }
385
+ else {
386
+ // a step with only `order` outside an `all`, or `name`/`role` alone: nothing to observe
387
+ result = { holds: false, unobservable: 'step-not-observed:empty', hits: [] };
388
+ env.observed.push({ path, holds: false });
389
+ }
390
+ if (step.name)
391
+ env.names.set(step.name, result.hits);
392
+ return result;
393
+ }
394
+ function callHits(step, env) {
395
+ const ctx = env.ctx;
396
+ if (!ctx || !step.call)
397
+ return [];
398
+ const want = step.call;
399
+ const out = [];
400
+ for (const c of ctx.calls) {
401
+ const tm = shapeMatch(want, c, ctx.ir);
402
+ if (!tm)
403
+ continue;
404
+ // a sink-replacement is a call at the sink: it must have the shape of one of the row's sinks
405
+ if (step.role === 'sink-replacement' && !env.sinkCalls.includes(c))
406
+ continue;
407
+ if (step.argIs) {
408
+ let exprs = argsAt(c, want.arg ?? 0) ?? [];
409
+ if (exprs.length === 0)
410
+ continue;
411
+ if (step.argIs === 'from-allowlist') {
412
+ if (!allowlisted(ctx, c, exprs))
413
+ continue;
414
+ }
415
+ else {
416
+ // `untainted` on an argument vector (array or list literal) reads its program element, element 0
417
+ if (step.argIs === 'untainted')
418
+ exprs = exprs.map(e => leadingElement(e) ?? e);
419
+ const flags = exprs.map(e => argTainted(ctx, c, e));
420
+ if (step.argIs === 'tainted' && !flags.some(Boolean))
421
+ continue;
422
+ if (step.argIs === 'untainted' && flags.some(Boolean))
423
+ continue;
424
+ }
425
+ }
426
+ if ((step.role === 'guard' || step.on !== undefined) && !guards(ctx, c, step.on ?? true, env))
427
+ continue;
428
+ if (step.argLiteralIn) {
429
+ const lit = leadingLiteral(c, want.arg);
430
+ if (lit === null || !step.argLiteralIn.includes(lit))
431
+ continue;
432
+ }
433
+ if (step.role === 'sanitizer' && !env.sinkCalls.some(s => s !== c && reaches(ctx, c, s)))
434
+ continue;
435
+ out.push({ call: c, typeMatch: tm });
436
+ }
437
+ out.sort((a, b) => a.call.location.line - b.call.location.line || a.call.location.column - b.call.location.column);
438
+ return out;
439
+ }
440
+ /** Identifiers of an expression that name values, not called methods. */
441
+ function valueNames(expr) {
442
+ return identifiers(expr.replace(/[A-Za-z_$][\w$]*\s*\(/g, '('));
443
+ }
444
+ /**
445
+ * A local variable chosen from a finite set of literals: at least two defs in
446
+ * the declaration, and every def written with no call and no other variable on
447
+ * its line (a `switch` or `if` chain of `v = "literal";`, or a bare declaration).
448
+ */
449
+ function literalChoice(ctx, name) {
450
+ const defs = ctx.defs.filter(d => d.variable === name);
451
+ if (defs.length < 2 || defs.some(d => d.kind !== 'local'))
452
+ return false;
453
+ return defs.every(d => !ctx.calls.some(c => c.location.line === d.line) &&
454
+ !ctx.ir.dfg.uses.some(u => u.line === d.line && u.variable !== name));
455
+ }
456
+ /**
457
+ * `arg-is: from-allowlist`: no argument is tainted, and at least one names a
458
+ * variable chosen from literals (`literalChoice`); the rest are literals or
459
+ * untainted values. A `contains` guard over a literal set is not read in this
460
+ * version (the switch and if-chain forms are).
461
+ */
462
+ function allowlisted(ctx, c, exprs) {
463
+ if (exprs.some(e => argTainted(ctx, c, e)))
464
+ return false;
465
+ return exprs.some(e => [...valueNames(e)].some(n => literalChoice(ctx, n)));
466
+ }
467
+ /**
468
+ * `role: guard` with `on`: a branch whose condition makes this call decides
469
+ * which side the endpoint sinks run on, and on that side the call returned
470
+ * `on`; the condition's block dominates each of them in the CFG. The targets
471
+ * are the endpoint sinks with a tainted argument, else all endpoint sinks.
472
+ */
473
+ function guards(ctx, c, on, env) {
474
+ const fact = ctx.branches?.find(f => f.calls.some(bc => bc.line === c.location.line && bc.method === c.method_name));
475
+ if (!fact)
476
+ return false;
477
+ const bc = fact.calls.find(x => x.line === c.location.line && x.method === c.method_name);
478
+ const tainted = env.endpointCalls.filter(t => t.arguments.some(a => argTainted(ctx, t, a.expression)));
479
+ const targets = tainted.length > 0 ? tainted : env.endpointCalls;
480
+ if (targets.length === 0)
481
+ return false;
482
+ return targets.every(t => {
483
+ if (t.location.line <= fact.line)
484
+ return false;
485
+ const side = sideOf(fact, t.location.line);
486
+ if (side === 'both')
487
+ return false;
488
+ return (side !== bc.negated) === on && lineDominates(ctx.ir, fact.line, t.location.line);
489
+ });
490
+ }
491
+ function orderHolds(order, env) {
492
+ const ctx = env.ctx;
493
+ const a = env.names.get(order[0]) ?? [];
494
+ const b = env.names.get(order[1]) ?? [];
495
+ if (!ctx)
496
+ return false;
497
+ return a.some(x => b.some(y => x.call !== y.call && reaches(ctx, x.call, y.call)));
498
+ }
499
+ // ---------------------------------------------------------------------------
500
+ // Table sinks
501
+ // ---------------------------------------------------------------------------
502
+ function tableSinkOf(table, sink, ir) {
503
+ const atLine = ir.calls.filter(c => c.location.line === sink.line);
504
+ const direct = atLine.filter(c => !sink.call.method || c.method_name === sink.call.method);
505
+ // the call that made the sink's receiver (`new ProcessBuilder(..)` for `pb.start()`)
506
+ const viaReceiver = [];
507
+ for (const c of direct) {
508
+ if (!c.receiver || c.receiver.includes('('))
509
+ continue;
510
+ const use = ir.dfg.uses.find(u => u.line === c.location.line && u.variable === c.receiver && u.def_id !== null);
511
+ const def = use ? ir.dfg.defs.find(d => d.id === use.def_id) : undefined;
512
+ if (def)
513
+ viaReceiver.push(...ir.calls.filter(x => x.location.line === def.line && isCtor(x)));
514
+ }
515
+ let best;
516
+ for (const ts of table.sinks) {
517
+ if (!ts.call)
518
+ continue;
519
+ for (const [via, calls] of [['call', direct], ['receiver-def', viaReceiver]]) {
520
+ for (const c of calls) {
521
+ const tm = shapeMatch(ts.call, c, ir);
522
+ if (!tm)
523
+ continue;
524
+ if (!best || RANK[tm] < RANK[best.typeMatch])
525
+ best = { id: ts.id, typeMatch: tm, via };
526
+ }
527
+ }
528
+ }
529
+ return best;
530
+ }
531
+ /** Does a template fact have the construct (and attribute value) a table entry names? */
532
+ function templateMatches(want, f) {
533
+ if (want.engine !== f.engine || want.construct !== f.construct)
534
+ return false;
535
+ if (want.attribute === undefined)
536
+ return true;
537
+ const actual = f.attributes[want.attribute];
538
+ if (want.value === undefined)
539
+ return actual !== undefined;
540
+ return want.value.includes(actual === undefined ? 'absent' : actual);
541
+ }
542
+ /** The template facts that render a name the sink call hands on. */
543
+ function linkedFacts(facts, names) {
544
+ if (!facts)
545
+ return null;
546
+ return Object.values(facts).flat().filter(f => f.names.some(n => names.includes(n)));
547
+ }
548
+ /**
549
+ * A sink that hands its value to a view takes its label from the table's
550
+ * template sinks: the construct that renders it at the baseline, else at the fix.
551
+ */
552
+ function templateSinkOf(table, before, after) {
553
+ for (const facts of [before, after]) {
554
+ for (const ts of table.sinks) {
555
+ if (ts.template && (facts ?? []).some(f => templateMatches(ts.template, f)))
556
+ return ts.id;
557
+ }
558
+ }
559
+ return undefined;
560
+ }
561
+ /**
562
+ * Observe, for every sink of a path `diff` reports `removed` or `sanitized`,
563
+ * which row of `table` the fix version matches.
564
+ *
565
+ * `table` is the reader's result; a refused table throws `TableRefused`.
566
+ */
567
+ export function remedyClassOf(diff, before, after, loaded, options = {}) {
568
+ if (!loaded.ok)
569
+ throw new TableRefused(loaded.digest, loaded.problems);
570
+ const table = loaded.table;
571
+ if (`CWE-${table.cwe}` !== diff.cwe)
572
+ throw new Error(`remedy-class: table is CWE-${table.cwe}, path set is ${diff.cwe}`);
573
+ const tableInfo = { format: table.format, cwe: table.cwe, language: table.language, version: table.version, digest: loaded.digest };
574
+ const irBefore = new Map(before.files.map(f => [f.file, f.analysis]));
575
+ const irAfter = new Map(after.files.map(f => [f.file, f.analysis]));
576
+ const rows = table.constructs.filter(r => r.retired === undefined);
577
+ const sinks = [];
578
+ for (const e of diff.entries) {
579
+ // a path still open in code whose view now renders it differently was fixed in the view:
580
+ // observed in both scopes, with `templateChanged`
581
+ const observed = e.change === 'removed' || e.change === 'sanitized' || e.change === 'guarded'
582
+ || (e.change === 'unchanged' && (options.scope === 'all' || e.templateChanged === true));
583
+ if (!observed || !e.before)
584
+ continue;
585
+ const base = e.before;
586
+ const seedParameters = base.source.decl.id !== base.sink.decl.id;
587
+ const obs = {
588
+ anchor: `${e.key}:sink`,
589
+ pathKey: e.key,
590
+ change: e.change,
591
+ ...(e.templateChanged ? { templateChanged: true } : {}),
592
+ sink: { tableSink: 'unlisted', file: base.sink.file, line: base.sink.line, decl: base.sink.decl },
593
+ match: 'unrecognized',
594
+ rows: [],
595
+ abstainedRows: [],
596
+ taintSeeds: seedParameters ? 'sources+parameters' : 'sources',
597
+ table: tableInfo,
598
+ };
599
+ sinks.push(obs);
600
+ const beforeIr = irBefore.get(base.sink.file);
601
+ const sinkCall = beforeIr?.calls.find(c => c.location.line === base.sink.line && (!base.sink.call.method || c.method_name === base.sink.call.method));
602
+ const handed = handedNames(sinkCall);
603
+ const factsBefore = handed.length > 0 ? linkedFacts(options.templates?.before, handed) : null;
604
+ const factsAfter = handed.length > 0 ? linkedFacts(options.templates?.after, handed) : null;
605
+ let label = beforeIr ? tableSinkOf(table, base.sink, beforeIr) : undefined;
606
+ if (!label) {
607
+ const id = templateSinkOf(table, factsBefore, factsAfter);
608
+ if (id)
609
+ label = { id, typeMatch: 'template', via: 'template' };
610
+ }
611
+ if (!label) {
612
+ obs.reason = 'sink-not-in-table';
613
+ continue;
614
+ }
615
+ obs.sink.tableSink = label.id;
616
+ obs.sink.tableSinkMatch = label.typeMatch;
617
+ obs.sink.via = label.via;
618
+ const afterIr = irAfter.get(base.sink.file);
619
+ const ids = options.stableIds?.after?.[base.sink.file];
620
+ const ctx = afterIr ? buildCtx(base.sink.file, afterIr, base.sink.decl, ids, seedParameters) : undefined;
621
+ const br = options.branches?.after?.[base.sink.file];
622
+ if (ctx && br)
623
+ ctx.branches = br;
624
+ for (const row of rows) {
625
+ if (!row.appliesTo.includes(label.id))
626
+ continue;
627
+ const rowSinks = table.sinks.filter(s => row.appliesTo.includes(s.id) && s.call);
628
+ const sinkCalls = ctx ? ctx.calls.filter(c => rowSinks.some(s => shapeMatch(s.call, c, ctx.ir))) : [];
629
+ const endpointCalls = ctx ? sinkCalls.filter(c => !sinkCalls.some(o => o !== c && reaches(ctx, c, o))) : [];
630
+ const env = { ctx, sinkCalls, endpointCalls, pathRemoved: e.change === 'removed', names: new Map(), observed: [], templateFacts: options.templates ? (factsAfter ?? []) : null };
631
+ const r = evalStep(row.match, '/match', env);
632
+ if (r.holds)
633
+ obs.rows.push(matchedRow(row, env.observed));
634
+ else if (r.unobservable)
635
+ obs.abstainedRows.push({ id: row.id, reason: r.unobservable });
636
+ }
637
+ if (options.outputContext && ctx) {
638
+ const labelled = table.sinks.find(x => x.id === label?.id);
639
+ const oc = labelled?.call ? outputContextOf(ctx, labelled.call) : 'unknown';
640
+ obs.sink.outputContext = oc;
641
+ obs.contextExcludedRows = [];
642
+ if (oc !== 'unknown') {
643
+ const keep = [];
644
+ for (const r of obs.rows) {
645
+ if (r.context !== undefined && r.context !== oc)
646
+ obs.contextExcludedRows.push({ id: r.id, context: r.context });
647
+ else
648
+ keep.push(r);
649
+ }
650
+ obs.rows = keep;
651
+ }
652
+ }
653
+ if (obs.rows.length === 1) {
654
+ obs.match = obs.rows[0].id;
655
+ obs.class = obs.rows[0].class;
656
+ }
657
+ else if (obs.rows.length > 1) {
658
+ obs.match = 'multiple';
659
+ }
660
+ else if (obs.abstainedRows.length > 0) {
661
+ obs.match = 'abstained';
662
+ }
663
+ else {
664
+ obs.reason = ctx ? 'no-row-holds' : 'declaration-not-found';
665
+ }
666
+ }
667
+ sinks.sort((a, b) => (a.anchor < b.anchor ? -1 : a.anchor > b.anchor ? 1 : 0));
668
+ return { table: tableInfo, sinks };
669
+ }
670
+ /**
671
+ * The output context at the fix version's writer sinks of this shape: the
672
+ * markup written before the first tainted value in each call's argument,
673
+ * after any literal-only writes to the same writer earlier in the declaration.
674
+ * One context across the calls, else `unknown`.
675
+ */
676
+ function outputContextOf(ctx, sinkShape) {
677
+ const names = new Set(ctx.defs.filter(d => ctx.tainted.has(d.id)).map(d => d.variable));
678
+ const calls = ctx.calls
679
+ .filter(c => shapeMatch(sinkShape, c, ctx.ir) !== null)
680
+ .sort((a, b) => a.location.line - b.location.line || a.location.column - b.location.column);
681
+ const seen = new Set();
682
+ for (const c of calls) {
683
+ const expr = c.arguments[0]?.expression;
684
+ if (!expr)
685
+ continue;
686
+ const local = markupBefore(expr, names);
687
+ if (local === null)
688
+ continue;
689
+ const earlier = calls
690
+ .filter(o => o !== c && o.receiver === c.receiver &&
691
+ (o.location.line < c.location.line || (o.location.line === c.location.line && o.location.column < c.location.column)))
692
+ .map(o => o.arguments[0]?.expression ?? '')
693
+ .filter(x => x.trim().startsWith('"') && markupBefore(x, names) === null && !/[A-Za-z_$]\s*\(/.test(x.replace(/"(?:[^"\\]|\\.)*"/g, '')))
694
+ .map(x => javaStringLiterals(x).join(''));
695
+ seen.add(htmlContextAt(earlier.join('') + local));
696
+ }
697
+ return seen.size === 1 ? [...seen][0] : 'unknown';
698
+ }
699
+ function matchedRow(row, observed) {
700
+ const m = { id: row.id, class: row.class, remedyClass: row.remedyClass, steps: [...observed].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) };
701
+ if (row.context !== undefined)
702
+ m.context = row.context;
703
+ return m;
704
+ }
705
+ //# sourceMappingURL=match.js.map