@sikkuumiii/defuse 0.0.0-stage → 0.5.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 (236) hide show
  1. package/LICENSE +235 -0
  2. package/README.md +561 -2
  3. package/dist/src/cli.d.ts +20 -0
  4. package/dist/src/cli.js +619 -0
  5. package/dist/src/cli.js.map +1 -0
  6. package/dist/src/core/analyze.d.ts +178 -0
  7. package/dist/src/core/analyze.js +834 -0
  8. package/dist/src/core/analyze.js.map +1 -0
  9. package/dist/src/core/coverage.d.ts +87 -0
  10. package/dist/src/core/coverage.js +181 -0
  11. package/dist/src/core/coverage.js.map +1 -0
  12. package/dist/src/core/finding.d.ts +196 -0
  13. package/dist/src/core/finding.js +178 -0
  14. package/dist/src/core/finding.js.map +1 -0
  15. package/dist/src/core/licence.d.ts +50 -0
  16. package/dist/src/core/licence.js +56 -0
  17. package/dist/src/core/licence.js.map +1 -0
  18. package/dist/src/core/paths.d.ts +21 -0
  19. package/dist/src/core/paths.js +58 -0
  20. package/dist/src/core/paths.js.map +1 -0
  21. package/dist/src/core/score.d.ts +137 -0
  22. package/dist/src/core/score.js +232 -0
  23. package/dist/src/core/score.js.map +1 -0
  24. package/dist/src/dast/targets.d.ts +30 -0
  25. package/dist/src/dast/targets.js +79 -0
  26. package/dist/src/dast/targets.js.map +1 -0
  27. package/dist/src/dast/zap.d.ts +42 -0
  28. package/dist/src/dast/zap.js +184 -0
  29. package/dist/src/dast/zap.js.map +1 -0
  30. package/dist/src/engine/scan.d.ts +80 -0
  31. package/dist/src/engine/scan.js +129 -0
  32. package/dist/src/engine/scan.js.map +1 -0
  33. package/dist/src/engine/shapes.d.ts +47 -0
  34. package/dist/src/engine/shapes.js +328 -0
  35. package/dist/src/engine/shapes.js.map +1 -0
  36. package/dist/src/engine/walk.d.ts +60 -0
  37. package/dist/src/engine/walk.js +156 -0
  38. package/dist/src/engine/walk.js.map +1 -0
  39. package/dist/src/parse/astDump.d.ts +31 -0
  40. package/dist/src/parse/astDump.js +66 -0
  41. package/dist/src/parse/astDump.js.map +1 -0
  42. package/dist/src/parse/languages.d.ts +70 -0
  43. package/dist/src/parse/languages.js +174 -0
  44. package/dist/src/parse/languages.js.map +1 -0
  45. package/dist/src/parse/location.d.ts +17 -0
  46. package/dist/src/parse/location.js +38 -0
  47. package/dist/src/parse/location.js.map +1 -0
  48. package/dist/src/parse/node-grammars.d.ts +12 -0
  49. package/dist/src/parse/node-grammars.js +35 -0
  50. package/dist/src/parse/node-grammars.js.map +1 -0
  51. package/dist/src/parse/parser.d.ts +126 -0
  52. package/dist/src/parse/parser.js +202 -0
  53. package/dist/src/parse/parser.js.map +1 -0
  54. package/dist/src/parse/tree-store.d.ts +139 -0
  55. package/dist/src/parse/tree-store.js +236 -0
  56. package/dist/src/parse/tree-store.js.map +1 -0
  57. package/dist/src/platform/dependency-rule.d.ts +27 -0
  58. package/dist/src/platform/dependency-rule.js +21 -0
  59. package/dist/src/platform/dependency-rule.js.map +1 -0
  60. package/dist/src/platform/deps.d.ts +86 -0
  61. package/dist/src/platform/deps.js +304 -0
  62. package/dist/src/platform/deps.js.map +1 -0
  63. package/dist/src/platform/php-facts.d.ts +49 -0
  64. package/dist/src/platform/php-facts.js +67 -0
  65. package/dist/src/platform/php-facts.js.map +1 -0
  66. package/dist/src/platform/runtime-version.d.ts +78 -0
  67. package/dist/src/platform/runtime-version.js +573 -0
  68. package/dist/src/platform/runtime-version.js.map +1 -0
  69. package/dist/src/report/colors.d.ts +50 -0
  70. package/dist/src/report/colors.js +97 -0
  71. package/dist/src/report/colors.js.map +1 -0
  72. package/dist/src/report/coverage-table.d.ts +90 -0
  73. package/dist/src/report/coverage-table.js +291 -0
  74. package/dist/src/report/coverage-table.js.map +1 -0
  75. package/dist/src/report/html-report.d.ts +66 -0
  76. package/dist/src/report/html-report.js +489 -0
  77. package/dist/src/report/html-report.js.map +1 -0
  78. package/dist/src/report/human.d.ts +44 -0
  79. package/dist/src/report/human.js +575 -0
  80. package/dist/src/report/human.js.map +1 -0
  81. package/dist/src/report/json.d.ts +24 -0
  82. package/dist/src/report/json.js +178 -0
  83. package/dist/src/report/json.js.map +1 -0
  84. package/dist/src/report/mark.d.ts +17 -0
  85. package/dist/src/report/mark.js +39 -0
  86. package/dist/src/report/mark.js.map +1 -0
  87. package/dist/src/report/pricing.d.ts +33 -0
  88. package/dist/src/report/pricing.js +343 -0
  89. package/dist/src/report/pricing.js.map +1 -0
  90. package/dist/src/report/sarif.d.ts +39 -0
  91. package/dist/src/report/sarif.js +219 -0
  92. package/dist/src/report/sarif.js.map +1 -0
  93. package/dist/src/report/site.d.ts +87 -0
  94. package/dist/src/report/site.js +742 -0
  95. package/dist/src/report/site.js.map +1 -0
  96. package/dist/src/rules/code-injection.d.ts +47 -0
  97. package/dist/src/rules/code-injection.js +326 -0
  98. package/dist/src/rules/code-injection.js.map +1 -0
  99. package/dist/src/rules/command-injection.d.ts +30 -0
  100. package/dist/src/rules/command-injection.js +286 -0
  101. package/dist/src/rules/command-injection.js.map +1 -0
  102. package/dist/src/rules/contract.d.ts +198 -0
  103. package/dist/src/rules/contract.js +39 -0
  104. package/dist/src/rules/contract.js.map +1 -0
  105. package/dist/src/rules/format-string.d.ts +46 -0
  106. package/dist/src/rules/format-string.js +146 -0
  107. package/dist/src/rules/format-string.js.map +1 -0
  108. package/dist/src/rules/hardcoded-secret.d.ts +38 -0
  109. package/dist/src/rules/hardcoded-secret.js +440 -0
  110. package/dist/src/rules/hardcoded-secret.js.map +1 -0
  111. package/dist/src/rules/lib/guards.d.ts +168 -0
  112. package/dist/src/rules/lib/guards.js +1179 -0
  113. package/dist/src/rules/lib/guards.js.map +1 -0
  114. package/dist/src/rules/lib/html-safety.d.ts +75 -0
  115. package/dist/src/rules/lib/html-safety.js +755 -0
  116. package/dist/src/rules/lib/html-safety.js.map +1 -0
  117. package/dist/src/rules/lib/local-build.d.ts +59 -0
  118. package/dist/src/rules/lib/local-build.js +320 -0
  119. package/dist/src/rules/lib/local-build.js.map +1 -0
  120. package/dist/src/rules/lib/strings.d.ts +208 -0
  121. package/dist/src/rules/lib/strings.js +1970 -0
  122. package/dist/src/rules/lib/strings.js.map +1 -0
  123. package/dist/src/rules/path-traversal.d.ts +33 -0
  124. package/dist/src/rules/path-traversal.js +96 -0
  125. package/dist/src/rules/path-traversal.js.map +1 -0
  126. package/dist/src/rules/registry.d.ts +19 -0
  127. package/dist/src/rules/registry.js +77 -0
  128. package/dist/src/rules/registry.js.map +1 -0
  129. package/dist/src/rules/sql-injection.d.ts +36 -0
  130. package/dist/src/rules/sql-injection.js +299 -0
  131. package/dist/src/rules/sql-injection.js.map +1 -0
  132. package/dist/src/rules/ssrf.d.ts +39 -0
  133. package/dist/src/rules/ssrf.js +111 -0
  134. package/dist/src/rules/ssrf.js.map +1 -0
  135. package/dist/src/rules/unbounded-copy.d.ts +37 -0
  136. package/dist/src/rules/unbounded-copy.js +147 -0
  137. package/dist/src/rules/unbounded-copy.js.map +1 -0
  138. package/dist/src/rules/unsafe-deserialization.d.ts +43 -0
  139. package/dist/src/rules/unsafe-deserialization.js +276 -0
  140. package/dist/src/rules/unsafe-deserialization.js.map +1 -0
  141. package/dist/src/rules/weak-hash.d.ts +37 -0
  142. package/dist/src/rules/weak-hash.js +197 -0
  143. package/dist/src/rules/weak-hash.js.map +1 -0
  144. package/dist/src/rules/xss.d.ts +38 -0
  145. package/dist/src/rules/xss.js +527 -0
  146. package/dist/src/rules/xss.js.map +1 -0
  147. package/dist/src/taint/content-type.d.ts +19 -0
  148. package/dist/src/taint/content-type.js +20 -0
  149. package/dist/src/taint/content-type.js.map +1 -0
  150. package/dist/src/taint/dictionaries.d.ts +23 -0
  151. package/dist/src/taint/dictionaries.js +1778 -0
  152. package/dist/src/taint/dictionaries.js.map +1 -0
  153. package/dist/src/taint/fold.d.ts +59 -0
  154. package/dist/src/taint/fold.js +1177 -0
  155. package/dist/src/taint/fold.js.map +1 -0
  156. package/dist/src/taint/java-scope.d.ts +61 -0
  157. package/dist/src/taint/java-scope.js +212 -0
  158. package/dist/src/taint/java-scope.js.map +1 -0
  159. package/dist/src/taint/local-map.d.ts +79 -0
  160. package/dist/src/taint/local-map.js +603 -0
  161. package/dist/src/taint/local-map.js.map +1 -0
  162. package/dist/src/taint/project.d.ts +172 -0
  163. package/dist/src/taint/project.js +617 -0
  164. package/dist/src/taint/project.js.map +1 -0
  165. package/dist/src/taint/tracer.d.ts +265 -0
  166. package/dist/src/taint/tracer.js +3817 -0
  167. package/dist/src/taint/tracer.js.map +1 -0
  168. package/dist/src/taint/types.d.ts +484 -0
  169. package/dist/src/taint/types.js +47 -0
  170. package/dist/src/taint/types.js.map +1 -0
  171. package/dist/src/taint/visibility.d.ts +25 -0
  172. package/dist/src/taint/visibility.js +96 -0
  173. package/dist/src/taint/visibility.js.map +1 -0
  174. package/docs/examples/defuse.json +20 -0
  175. package/docs/examples/dvwa-urls.json +14 -0
  176. package/docs/examples/github-code-scanning.yml +54 -0
  177. package/docs/examples/tiny.js +4 -0
  178. package/package.json +73 -4
  179. package/src/cli.ts +747 -0
  180. package/src/core/analyze.ts +1072 -0
  181. package/src/core/coverage.ts +269 -0
  182. package/src/core/finding.ts +311 -0
  183. package/src/core/licence.ts +61 -0
  184. package/src/core/paths.ts +59 -0
  185. package/src/core/score.ts +353 -0
  186. package/src/dast/targets.ts +121 -0
  187. package/src/dast/zap.ts +246 -0
  188. package/src/engine/scan.ts +200 -0
  189. package/src/engine/shapes.ts +348 -0
  190. package/src/engine/walk.ts +196 -0
  191. package/src/parse/astDump.ts +85 -0
  192. package/src/parse/languages.ts +232 -0
  193. package/src/parse/location.ts +44 -0
  194. package/src/parse/node-grammars.ts +41 -0
  195. package/src/parse/parser.ts +262 -0
  196. package/src/parse/tree-store.ts +301 -0
  197. package/src/platform/dependency-rule.ts +33 -0
  198. package/src/platform/deps.ts +374 -0
  199. package/src/platform/php-facts.ts +85 -0
  200. package/src/platform/runtime-version.ts +710 -0
  201. package/src/report/colors.ts +107 -0
  202. package/src/report/coverage-table.ts +359 -0
  203. package/src/report/html-report.ts +601 -0
  204. package/src/report/human.ts +811 -0
  205. package/src/report/json.ts +189 -0
  206. package/src/report/mark.ts +47 -0
  207. package/src/report/pricing.ts +374 -0
  208. package/src/report/sarif.ts +235 -0
  209. package/src/report/site.ts +760 -0
  210. package/src/rules/code-injection.ts +346 -0
  211. package/src/rules/command-injection.ts +296 -0
  212. package/src/rules/contract.ts +220 -0
  213. package/src/rules/format-string.ts +158 -0
  214. package/src/rules/hardcoded-secret.ts +485 -0
  215. package/src/rules/lib/guards.ts +1235 -0
  216. package/src/rules/lib/html-safety.ts +793 -0
  217. package/src/rules/lib/local-build.ts +329 -0
  218. package/src/rules/lib/strings.ts +2034 -0
  219. package/src/rules/path-traversal.ts +101 -0
  220. package/src/rules/registry.ts +90 -0
  221. package/src/rules/sql-injection.ts +311 -0
  222. package/src/rules/ssrf.ts +118 -0
  223. package/src/rules/unbounded-copy.ts +162 -0
  224. package/src/rules/unsafe-deserialization.ts +291 -0
  225. package/src/rules/weak-hash.ts +226 -0
  226. package/src/rules/xss.ts +572 -0
  227. package/src/taint/content-type.ts +20 -0
  228. package/src/taint/dictionaries.ts +1855 -0
  229. package/src/taint/fold.ts +1206 -0
  230. package/src/taint/java-scope.ts +200 -0
  231. package/src/taint/local-map.ts +594 -0
  232. package/src/taint/project.ts +774 -0
  233. package/src/taint/tracer.ts +4298 -0
  234. package/src/taint/types.ts +511 -0
  235. package/src/taint/visibility.ts +100 -0
  236. package/tsconfig.json +61 -0
@@ -0,0 +1,774 @@
1
+ /**
2
+ * THE PROJECT INDEX (cross-file resolution)
3
+ * ==========================================
4
+ *
5
+ * Up to now the tracer could follow a value anywhere inside one file and no
6
+ * further. `helper(dirty)` where `helper` lives in `./helpers.js` was the end of
7
+ * the road, and on real code that is where most of the road is: applications put
8
+ * their database access in `db.js`, their escaping in `util/html.py`, their
9
+ * command runners in `Shell.java`.
10
+ *
11
+ * This module answers one question: **"file F calls a function named N - which
12
+ * file, if any, is that?"**
13
+ *
14
+ * THE RULE IT PLAYS BY: answer only when the answer is unambiguous.
15
+ *
16
+ * If two imported modules both define `sanitize`, we say nothing and record
17
+ * that we declined. Picking one at random would let the tool print a
18
+ * step-by-step "proof" through a function that never runs - which is worse than
19
+ * printing nothing, because it looks like evidence. The count of declined
20
+ * resolutions is reported in every scan.
21
+ *
22
+ * HOW IMPORTS ARE FOUND, per language:
23
+ *
24
+ * JS/TS `import x from './a'` and `require('./a')`, resolved relative to the
25
+ * importing file, trying the usual extensions and `/index`.
26
+ * Python `from .a import x`, `from pkg.a import x`, `import pkg.a` - resolved
27
+ * against the files actually present in the scan.
28
+ * Java `import com.x.Bar;` resolves to a file named `Bar.java`, PLUS every
29
+ * file in the same directory, because Java classes in the same
30
+ * package see each other with no import at all.
31
+ * Go every file in the same directory, because that is one package. Go
32
+ * imports name packages rather than files, and resolving those needs
33
+ * module metadata we do not read.
34
+ *
35
+ * WHAT IT DOES NOT DO, stated here and in the scan report:
36
+ * - It does not check that the function is actually EXPORTED. A module-private
37
+ * helper with the same name as an exported one could be entered.
38
+ * - It does not follow re-exports (`export * from './a'`).
39
+ * - It does not resolve node_modules, third-party packages, or aliases from
40
+ * tsconfig `paths` / webpack config.
41
+ * - Dynamic imports and computed requires are invisible.
42
+ */
43
+
44
+ import type { Node } from 'web-tree-sitter';
45
+ import { basenameOf, dirnameOf, joinPath, resolveFrom, toPosix } from '../core/paths.js';
46
+ import type { LanguageId } from '../parse/languages.js';
47
+ import { nodeAtRange, type TreeStore } from '../parse/tree-store.js';
48
+ import { collectFunctions, type CrossFileResolver } from './tracer.js';
49
+ import { hasModifier } from './java-scope.js';
50
+
51
+ /**
52
+ * A function recorded as COORDINATES rather than as a syntax node.
53
+ *
54
+ * This is the change that lets the memory ceiling go away. The index used to
55
+ * hold a live Node for every function body in the project, and a Node keeps its
56
+ * whole tree alive - so indexing the project pinned every tree in it, whether
57
+ * or not anything ever traced into them. Holding two integers and a string
58
+ * instead lets the trees be freed, at the cost of parsing a file again on the
59
+ * rare occasion the tracer actually descends into it.
60
+ *
61
+ * bodyType is recorded alongside the offsets because a byte range alone is not
62
+ * always unique: a node can share its exact extent with its parent. The type is
63
+ * what tells them apart, and a lookup that cannot match both is refused.
64
+ */
65
+ export interface IndexedFunction {
66
+ readonly name: string;
67
+ readonly params: readonly string[];
68
+ /** The argument range the function accepts - see LocalFunction.minArgs. */
69
+ readonly minArgs?: number | undefined;
70
+ readonly maxArgs?: number | undefined;
71
+ readonly bodyStart: number;
72
+ readonly bodyEnd: number;
73
+ readonly bodyType: string;
74
+ }
75
+
76
+ /**
77
+ * What buildProjectIndex needs from each file. Everything here is extracted
78
+ * while the file's tree is hot during the first pass, so the index itself never
79
+ * touches a tree and never holds one.
80
+ */
81
+ export interface IndexedFileInput {
82
+ readonly path: string;
83
+ readonly language: LanguageId;
84
+ readonly functions: ReadonlyMap<string, IndexedFunction>;
85
+ /** Raw module specifiers, already read out of the tree. */
86
+ readonly importSpecifiers: readonly string[];
87
+ /** Java only - see javaTypeFacts(). */
88
+ readonly javaTypes?: JavaTypeFacts | undefined;
89
+ }
90
+
91
+ /**
92
+ * What a Java file says about the types it declares and names, read from the
93
+ * top level of its tree in the first pass. staticMethod() below needs these to
94
+ * answer "which class does `Utils` mean here?" without holding the tree.
95
+ */
96
+ export interface JavaTypeFacts {
97
+ /** From `package a.b;` - the empty string for the unnamed package. */
98
+ readonly packageName: string;
99
+ /** Top-level type declarations of every kind - class, interface, enum, record, annotation. */
100
+ readonly types: readonly { readonly name: string; readonly kind: string }[];
101
+ /** `import a.b.C;` as `a.b.C`. Not `import a.b.*;`, not `import static`. */
102
+ readonly singleTypeImports: readonly string[];
103
+ /** The last name of each `import static a.b.C.m;` - a member that is then in scope by that name. */
104
+ readonly staticImports: readonly string[];
105
+ /** Whether any `import static a.b.C.*;` brings in members nobody here can list. */
106
+ readonly staticOnDemand: boolean;
107
+ /** Every type declared anywhere in the file, nested ones included. */
108
+ readonly allTypeNames: readonly string[];
109
+ }
110
+
111
+ const JAVA_TYPE_DECLARATIONS = new Set([
112
+ 'class_declaration', 'interface_declaration', 'enum_declaration', 'record_declaration',
113
+ 'annotation_type_declaration',
114
+ ]);
115
+
116
+ export function javaTypeFacts(root: Node): JavaTypeFacts {
117
+ /*
118
+ * A dotted name read from its identifiers, not its text - so a comment
119
+ * between `import` and the name is not part of the name. A regex over the
120
+ * line missed such an import entirely, and the class it named lost to one in
121
+ * the same package that the import outranks (tests/probes/p08).
122
+ */
123
+ const dotted = (n: Node | null | undefined): string => {
124
+ const parts: string[] = [];
125
+ const visit = (x: Node): void => {
126
+ if (x.type === 'identifier') parts.push(x.text ?? '');
127
+ for (const c of x.namedChildren) if (c && c.type !== 'block_comment' && c.type !== 'line_comment') visit(c);
128
+ };
129
+ if (n) visit(n);
130
+ return parts.join('.');
131
+ };
132
+ let packageName = '';
133
+ const types: { name: string; kind: string }[] = [];
134
+ const singleTypeImports: string[] = [];
135
+ const staticImports: string[] = [];
136
+ let staticOnDemand = false;
137
+ for (const child of root.namedChildren) {
138
+ if (!child) continue;
139
+ if (child.type === 'package_declaration') {
140
+ packageName = dotted(child.namedChildren.find((c) => c?.type === 'scoped_identifier' || c?.type === 'identifier'));
141
+ } else if (JAVA_TYPE_DECLARATIONS.has(child.type)) {
142
+ const name = child.childForFieldName('name')?.text;
143
+ if (name) types.push({ name, kind: child.type });
144
+ } else if (child.type === 'import_declaration') {
145
+ const isStatic = child.children.some((c) => c?.type === 'static');
146
+ const onDemand = child.namedChildren.some((c) => c?.type === 'asterisk');
147
+ const name = dotted(child.namedChildren.find((c) => c?.type === 'scoped_identifier' || c?.type === 'identifier'));
148
+ if (isStatic && onDemand) staticOnDemand = true;
149
+ else if (isStatic) staticImports.push(name.split('.').pop() ?? '');
150
+ else if (!onDemand && name) singleTypeImports.push(name);
151
+ }
152
+ }
153
+ /*
154
+ * Nested types count for "is this type in the scan?": a superclass that is
155
+ * a nested class of this or another file is as able to supply members as a
156
+ * top-level one (tests/probes/j05, j06).
157
+ */
158
+ const allTypeNames = root
159
+ .descendantsOfType([...JAVA_TYPE_DECLARATIONS])
160
+ .map((n) => n?.childForFieldName('name')?.text ?? '')
161
+ .filter(Boolean);
162
+ return { packageName, types, singleTypeImports, staticImports, staticOnDemand, allTypeNames };
163
+ }
164
+
165
+ /** Turn a file's functions into coordinates, using the tracer's own definition. */
166
+ export function indexFunctions(
167
+ root: Node,
168
+ language: LanguageId,
169
+ ): Map<string, IndexedFunction> {
170
+ const indexed = new Map<string, IndexedFunction>();
171
+ for (const [name, fn] of collectFunctions(root, language)) {
172
+ indexed.set(name, {
173
+ name: fn.name,
174
+ params: fn.params,
175
+ minArgs: fn.minArgs,
176
+ maxArgs: fn.maxArgs,
177
+ bodyStart: fn.body.startIndex,
178
+ bodyEnd: fn.body.endIndex,
179
+ bodyType: fn.body.type,
180
+ });
181
+ }
182
+ return indexed;
183
+ }
184
+
185
+ interface IndexedFile {
186
+ /** The path exactly as the shell gave it to us - what reports will show. */
187
+ readonly path: string;
188
+ /** The same path, slashes normalised. All comparisons use this one. */
189
+ readonly posix: string;
190
+ readonly directory: string;
191
+ readonly language: LanguageId;
192
+ readonly functions: ReadonlyMap<string, IndexedFunction>;
193
+ /** Absolute paths of files this one imports, as far as we could resolve them. */
194
+ readonly imports: Set<string>;
195
+ readonly javaTypes?: JavaTypeFacts | undefined;
196
+ }
197
+
198
+ export interface ProjectIndexStats {
199
+ readonly filesIndexed: number;
200
+ readonly functionsIndexed: number;
201
+ readonly importEdges: number;
202
+ /**
203
+ * Import statements we READ but could not point at a file in this scan.
204
+ *
205
+ * Counted because it is the difference between "this code imports nothing"
206
+ * and "this code imports 117 things and you gave me none of them". A beginner
207
+ * scanned juice-shop's server.ts on its own, got zero findings, and concluded
208
+ * the tool was broken. It was not: server.ts is a wiring file whose 117
209
+ * imports all pointed outside the scan, so every trace stopped at the first
210
+ * module boundary. The same repository's routes/ directory produces seven
211
+ * findings, two of them flow-verified.
212
+ *
213
+ * The report had the evidence - importEdges was 0 - and never said what it
214
+ * meant. An undeclared blind spot is exactly what this project refuses to
215
+ * ship, so it is declared now.
216
+ */
217
+ readonly importsUnresolved: number;
218
+ /** Times a cross-file call WAS followed. */
219
+ readonly resolved: number;
220
+ /** Times several files defined the name and we refused to choose. */
221
+ readonly ambiguous: number;
222
+ /**
223
+ * Times a function was found in the index but could not be located again in
224
+ * the re-parsed tree, and the resolution was declined rather than guessed.
225
+ *
226
+ * This should be zero forever: the parse is deterministic, so a node recorded
227
+ * at bytes 400-980 as a `block` is a `block` at bytes 400-980 every time the
228
+ * same file is parsed with the same grammar. It is counted anyway, because
229
+ * "should be impossible" is how a tool ends up tracing into the wrong
230
+ * function and printing a confident proof of it.
231
+ */
232
+ readonly relocationsFailed: number;
233
+ }
234
+
235
+ export interface ProjectIndex extends CrossFileResolver {
236
+ readonly stats: () => ProjectIndexStats;
237
+ /**
238
+ * The one static method `receiver.method(...)` can run, from a Java file -
239
+ * or null whenever that is in any doubt. See the note in the body.
240
+ */
241
+ readonly staticMethod: (
242
+ fromPath: string,
243
+ receiver: string,
244
+ method: string,
245
+ /** Simple names of the types the call sits inside inherit from - see strings.ts. */
246
+ supertypes?: readonly string[],
247
+ /** The caller created the object with `new X(...)`, so an instance method is exact too. */
248
+ allowInstance?: boolean,
249
+ ) => { readonly path: string; readonly method: Node } | null;
250
+ /** Is a Java type of this simple name - top-level or nested - declared anywhere in the scan? */
251
+ readonly javaTypeInScan: (simpleName: string) => boolean;
252
+ }
253
+
254
+ /* -------------------------------------------------------------------------- *
255
+ * Import extraction - from the syntax tree, not from a text search, for the
256
+ * same reason the rules use the tree: a `require('./db')` inside a comment or a
257
+ * string is not an import.
258
+ * -------------------------------------------------------------------------- */
259
+
260
+ const IMPORT_NODES: Record<LanguageId, readonly string[]> = {
261
+ javascript: ['import_statement', 'call_expression'],
262
+ typescript: ['import_statement', 'call_expression'],
263
+ python: ['import_statement', 'import_from_statement'],
264
+ /*
265
+ * `scoped_type_identifier` is here because an import statement is not the
266
+ * only way Java names another file.
267
+ *
268
+ * new org.owasp.benchmark.helpers.SeparateClassRequest(request)
269
+ *
270
+ * names a class as precisely as any import does and produces no
271
+ * import_declaration at all, so the edge was never built and the call was
272
+ * never followed.
273
+ *
274
+ * This was not a corner. It was EVERY ONE of the 69 cases the OWASP
275
+ * benchmark scored as a complete miss - 48 xss, 20 cmdi, 1 sqli, all of them
276
+ * a request wrapped in a helper class referenced by its full name. Proven by
277
+ * adding a single import line to one test case and changing nothing else,
278
+ * which turned a silent file into a finding.
279
+ *
280
+ * A complete miss is the only failure mode that gets somebody hacked, and
281
+ * this one was a hundred percent of ours.
282
+ */
283
+ java: ['import_declaration', 'scoped_type_identifier'],
284
+ go: [],
285
+ // PHP's require/include are function-like statements, and Composer autoloading
286
+ // resolves classes with no import line at all. Cross-file tracing in PHP is
287
+ // therefore NOT implemented - stated here rather than half-attempted.
288
+ php: [],
289
+ /*
290
+ * C and C++ resolve calls at LINK time, not through anything written in the
291
+ * source, and that is a harder problem than it first looks.
292
+ *
293
+ * `#include` brings in DECLARATIONS, not definitions - the body of the
294
+ * function lives in a .c file the header never names, chosen by a Makefile
295
+ * we do not read. So following an include would lead to a prototype and stop.
296
+ *
297
+ * Same-directory resolution, which is what makes Java and Go work above, is
298
+ * actively unsafe here: `static` gives a C function FILE scope, and every
299
+ * large C project has a dozen different `static int init(void)` definitions
300
+ * sitting in the same folder. Matching by name would let the tracer walk into
301
+ * a function the caller provably cannot see, and print a step-by-step proof
302
+ * through it. That is the exact failure this project exists to refuse, so
303
+ * cross-file tracing in the C family is NOT implemented and says so.
304
+ */
305
+ c: [],
306
+ cpp: [],
307
+ };
308
+
309
+ function unquote(raw: string): string {
310
+ return raw.replace(/^[a-zA-Z]?["'`]/, '').replace(/["'`]$/, '');
311
+ }
312
+
313
+ /** The raw module specifiers this file mentions, e.g. "./db", "app.helpers". */
314
+ export function importSpecifiers(root: Node, language: LanguageId): string[] {
315
+ const wanted = new Set(IMPORT_NODES[language]);
316
+ if (wanted.size === 0) return [];
317
+ const specifiers: string[] = [];
318
+
319
+ /*
320
+ * Six levels is enough for every language whose edges are declarations at
321
+ * the top of the file. Java's are not: a fully-qualified type reference sits
322
+ * inside a method body, which is twenty-odd levels down, so the walk stopped
323
+ * short of the very thing it needed to see.
324
+ *
325
+ * 48 is past any hand-written nesting and still far under the tracer's own
326
+ * MAX_AST_DEPTH of 400, and it applies only to Java - nothing else pays for
327
+ * it.
328
+ */
329
+ const maxDepth = language === 'java' ? 48 : 6;
330
+
331
+ const visit = (node: Node, depth: number): void => {
332
+ if (depth > maxDepth) return;
333
+
334
+ if (wanted.has(node.type)) {
335
+ if (node.type === 'scoped_type_identifier') {
336
+ /*
337
+ * `a.b.C` parses as scoped_type_identifier nested inside
338
+ * scoped_type_identifier: the whole name, then `a.b`, then `a`. Only
339
+ * the outermost is the class being named; the inner ones are package
340
+ * fragments and would each try to resolve as a class of their own.
341
+ *
342
+ * The resolver below already requires exactly one file to match the
343
+ * final segment, so a fragment that matches nothing is harmless and a
344
+ * name that matches two files is declined rather than guessed. This
345
+ * check is about not asking the question, rather than about surviving
346
+ * the answer.
347
+ */
348
+ if (node.parent?.type !== 'scoped_type_identifier') {
349
+ specifiers.push((node.text ?? '').trim());
350
+ }
351
+ } else if (node.type === 'call_expression') {
352
+ // require('./db') - only when the callee really is `require`.
353
+ const callee = node.childForFieldName('function');
354
+ if (callee?.text === 'require') {
355
+ const args = node.childForFieldName('arguments');
356
+ const first = args?.namedChildren.find((c): c is Node => c !== null);
357
+ if (first) specifiers.push(unquote(first.text ?? ''));
358
+ }
359
+ } else if (
360
+ node.type === 'import_from_statement' &&
361
+ /^\.+$/.test(node.childForFieldName('module_name')?.text ?? '')
362
+ ) {
363
+ /*
364
+ * `from . import runner, shell` names no module in the `from` part: the
365
+ * imported NAMES are the modules (runner.py, shell/__init__.py). Read
366
+ * as just ".", it resolved to the package folder and never to a file -
367
+ * so every edge in a package written this way was lost. One specifier
368
+ * per name, `.runner`, resolves them like `from .runner import x`.
369
+ * ponytail: a name defined in the package's own __init__.py rather than
370
+ * a module of that name stays unresolved; add a fallback when it shows.
371
+ */
372
+ const dots = node.childForFieldName('module_name')?.text ?? '.';
373
+ const names = node.childrenForFieldName('name').filter((n): n is Node => n !== null);
374
+ if (names.length === 0) specifiers.push(dots); // `from . import *`: the package itself
375
+ for (const name of names) {
376
+ const module = name.type === 'aliased_import' ? name.childForFieldName('name') : name;
377
+ if (module?.text) specifiers.push(`${dots}${module.text}`);
378
+ }
379
+ } else {
380
+ const source =
381
+ node.childForFieldName('source') ??
382
+ node.childForFieldName('module_name') ??
383
+ node.childForFieldName('name') ??
384
+ null;
385
+ const raw = (source ?? node).text ?? '';
386
+ specifiers.push(unquote(raw.replace(/^import\s+|;$/g, '').trim()));
387
+ }
388
+ }
389
+
390
+ for (const child of node.namedChildren) if (child) visit(child, depth + 1);
391
+ };
392
+ visit(root, 0);
393
+ return specifiers.filter(Boolean);
394
+ }
395
+
396
+ const JS_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts'];
397
+
398
+ /** Turn a specifier into a path we actually have, or null. */
399
+ /*
400
+ * Every known path by its file name, built once per scan. Java resolves each
401
+ * `a.b.C` by looking for C.java, and BenchmarkJava names a class in full
402
+ * thousands of times - filtering all 2,766 paths for every one of those was
403
+ * the single largest cost in its scan.
404
+ */
405
+ const basenameIndex = new WeakMap<ReadonlySet<string>, Map<string, string[]>>();
406
+ function byBasename(known: ReadonlySet<string>): Map<string, string[]> {
407
+ let index = basenameIndex.get(known);
408
+ if (!index) {
409
+ index = new Map();
410
+ for (const p of known) {
411
+ const name = basenameOf(p);
412
+ const list = index.get(name);
413
+ if (list) list.push(p);
414
+ else index.set(name, [p]);
415
+ }
416
+ basenameIndex.set(known, index);
417
+ }
418
+ return index;
419
+ }
420
+
421
+ function resolveSpecifier(
422
+ specifier: string,
423
+ fromFile: IndexedFile,
424
+ known: ReadonlySet<string>,
425
+ ): string | null {
426
+ const tryAll = (base: string): string | null => {
427
+ if (known.has(base)) return base;
428
+
429
+ /**
430
+ * TypeScript's ESM convention: source says `import './languages.js'` but the
431
+ * file on disk is `languages.ts`. The compiler rewrites nothing, so a
432
+ * resolver that only tries the literal specifier finds nothing - which is
433
+ * exactly what happened the first time Defuse scanned its own source and
434
+ * reported zero import edges.
435
+ */
436
+ const rewritten = base.replace(/\.(js|mjs|cjs)$/, '');
437
+ if (rewritten !== base) {
438
+ for (const extension of JS_EXTENSIONS) {
439
+ if (known.has(rewritten + extension)) return rewritten + extension;
440
+ }
441
+ }
442
+
443
+ for (const extension of JS_EXTENSIONS) {
444
+ if (known.has(base + extension)) return base + extension;
445
+ if (known.has(joinPath(base, `index${extension}`))) {
446
+ return joinPath(base, `index${extension}`);
447
+ }
448
+ }
449
+ return null;
450
+ };
451
+
452
+ if (fromFile.language === 'javascript' || fromFile.language === 'typescript') {
453
+ // Only relative specifiers. A bare 'express' is a package, not our code.
454
+ if (!specifier.startsWith('.')) return null;
455
+ return tryAll(resolveFrom(fromFile.directory, specifier));
456
+ }
457
+
458
+ if (fromFile.language === 'python') {
459
+ const leadingDots = /^\.+/.exec(specifier)?.[0].length ?? 0;
460
+ const body = specifier.slice(leadingDots).replace(/\./g, '/');
461
+ // A module is a file (`shell.py`) or a package folder (`shell/__init__.py`).
462
+ if (leadingDots > 0) {
463
+ let base = fromFile.directory;
464
+ for (let i = 1; i < leadingDots; i++) base = dirnameOf(base);
465
+ const candidate = body ? joinPath(base, body) : base;
466
+ if (body && known.has(`${candidate}.py`)) return `${candidate}.py`;
467
+ return known.has(`${candidate}/__init__.py`) ? `${candidate}/__init__.py` : null;
468
+ }
469
+ // Absolute-ish: match any known file whose path ends with the module path.
470
+ const asFile = `${body}.py`;
471
+ const asPackage = `${body}/__init__.py`;
472
+ const matches = [...known].filter(
473
+ (p) => p === asFile || p === asPackage || p.endsWith(`/${asFile}`) || p.endsWith(`/${asPackage}`),
474
+ );
475
+ return matches.length === 1 ? (matches[0] ?? null) : null;
476
+ }
477
+
478
+ if (fromFile.language === 'java') {
479
+ // `com.example.Helper` -> a file called Helper.java, if there is exactly one.
480
+ const className = specifier.split('.').pop() ?? '';
481
+ if (!className || className === '*') return null;
482
+ const matches = byBasename(known).get(`${className}.java`) ?? [];
483
+ return matches.length === 1 ? (matches[0] ?? null) : null;
484
+ }
485
+
486
+ return null;
487
+ }
488
+
489
+ /**
490
+ * @param files one record per scanned file, with functions and import
491
+ * specifiers ALREADY extracted from its tree.
492
+ * @param store where trees come from. Only touched when a resolution actually
493
+ * succeeds, which on real projects is a small fraction of files -
494
+ * the index itself is built without a single tree in hand.
495
+ */
496
+ export function buildProjectIndex(
497
+ files: readonly IndexedFileInput[],
498
+ store: TreeStore,
499
+ ): ProjectIndex {
500
+ const byPath = new Map<string, IndexedFile>();
501
+ // Resolution happens in normalised space; lookups map back to the original.
502
+ const known = new Set(files.map((f) => toPosix(f.path)));
503
+ const originalOf = new Map(files.map((f) => [toPosix(f.path), f.path]));
504
+
505
+ for (const file of files) {
506
+ byPath.set(file.path, {
507
+ path: file.path,
508
+ posix: toPosix(file.path),
509
+ directory: dirnameOf(file.path),
510
+ language: file.language,
511
+ functions: file.functions,
512
+ imports: new Set<string>(),
513
+ javaTypes: file.javaTypes,
514
+ });
515
+ }
516
+
517
+ // Second pass: resolve specifiers now that every path is known.
518
+ let importEdges = 0;
519
+ let importsUnresolved = 0;
520
+ for (const file of files) {
521
+ const entry = byPath.get(file.path);
522
+ if (!entry) continue;
523
+ for (const specifier of file.importSpecifiers) {
524
+ const resolvedPosix = resolveSpecifier(specifier, entry, known);
525
+ const resolved = resolvedPosix ? originalOf.get(resolvedPosix) : undefined;
526
+ if (resolved && resolved !== file.path) {
527
+ entry.imports.add(resolved);
528
+ importEdges++;
529
+ } else if (!/^[a-z@][\w@/.-]*$/i.test(specifier) || specifier.startsWith('.')) {
530
+ // A RELATIVE import that resolved to nothing means the file it names is
531
+ // outside the scan. A bare specifier (`express`, `@angular/core`) is a
532
+ // package and is expected not to resolve - counting those would drown
533
+ // the real signal in node_modules noise.
534
+ importsUnresolved++;
535
+ }
536
+ }
537
+ }
538
+
539
+ // Same-package visibility: Java and Go files in one directory see each other
540
+ // with no import statement at all, so the directory IS the import edge.
541
+ const byDirectory = new Map<string, string[]>();
542
+ for (const entry of byPath.values()) {
543
+ if (entry.language !== 'java' && entry.language !== 'go') continue;
544
+ const list = byDirectory.get(entry.directory) ?? [];
545
+ list.push(entry.path);
546
+ byDirectory.set(entry.directory, list);
547
+ }
548
+
549
+ /**
550
+ * Function name -> the files that define it.
551
+ *
552
+ * The first version of resolve() walked every sibling file in the directory
553
+ * looking for the name. That is fine for a normal package and catastrophic
554
+ * for OWASP's benchmark, which puts 2,766 Java files in ONE directory: every
555
+ * unresolved call scanned all 2,766 siblings, and a 12-second scan became 60.
556
+ *
557
+ * Indexing by name first turns that inner loop from "all files in the
558
+ * package" into "the handful of files that define this exact name".
559
+ */
560
+ const byFunctionName = new Map<string, IndexedFile[]>();
561
+ for (const entry of byPath.values()) {
562
+ for (const name of entry.functions.keys()) {
563
+ const list = byFunctionName.get(name);
564
+ if (list) list.push(entry);
565
+ else byFunctionName.set(name, [entry]);
566
+ }
567
+ }
568
+
569
+ /** Every Java type name declared in the scan, of every kind, nested ones included. */
570
+ const typeNames = new Set<string>();
571
+ for (const entry of byPath.values()) for (const t of entry.javaTypes?.allTypeNames ?? []) typeNames.add(t);
572
+
573
+ let resolved = 0;
574
+ let ambiguous = 0;
575
+ let relocationsFailed = 0;
576
+ let functionsIndexed = 0;
577
+ for (const entry of byPath.values()) functionsIndexed += entry.functions.size;
578
+
579
+ return {
580
+ resolve(fromPath: string, name: string) {
581
+ const from = byPath.get(fromPath);
582
+ if (!from) return null;
583
+
584
+ // Start from "who defines this name at all", then keep only the ones
585
+ // this file can actually see.
586
+ const definers = byFunctionName.get(name);
587
+ if (!definers) return null;
588
+
589
+ const samePackage = from.language === 'java' || from.language === 'go';
590
+ const hits = definers.filter(
591
+ (candidate) =>
592
+ candidate.path !== fromPath &&
593
+ candidate.language === from.language &&
594
+ (from.imports.has(candidate.path) ||
595
+ (samePackage && candidate.directory === from.directory)),
596
+ );
597
+
598
+ if (hits.length === 0) return null;
599
+ if (hits.length > 1) {
600
+ ambiguous++;
601
+ return null; // several files define it - we do not guess.
602
+ }
603
+
604
+ const winner = hits[0];
605
+ const indexed = winner?.functions.get(name);
606
+ if (!winner || !indexed) return null;
607
+
608
+ /* ---- materialise: coordinates back into a live syntax node ----
609
+ *
610
+ * This is the only place in the scan that can pull a tree back off disk,
611
+ * and it is deliberately the LAST thing that happens - after the file has
612
+ * been chosen and the ambiguity check has passed. Resolving first and
613
+ * parsing second means a project with thousands of files that never
614
+ * resolve into each other never re-parses anything.
615
+ *
616
+ * The pin lasts until the caller releases it, because the Node handed
617
+ * back here points into this tree's memory and nothing else keeps it
618
+ * alive. */
619
+ store.pin(winner.path);
620
+ const tree = store.get(winner.path);
621
+ const body = nodeAtRange(
622
+ tree.rootNode,
623
+ indexed.bodyStart,
624
+ indexed.bodyEnd,
625
+ indexed.bodyType,
626
+ );
627
+ if (!body) {
628
+ relocationsFailed++;
629
+ return null;
630
+ }
631
+
632
+ resolved++;
633
+ return {
634
+ path: winner.path,
635
+ fn: { name: indexed.name, params: indexed.params, minArgs: indexed.minArgs, maxArgs: indexed.maxArgs, body },
636
+ };
637
+ },
638
+
639
+ /*
640
+ * `Utils.getOSCommandString("echo")` - A STATIC CALL, NAMED THROUGH ITS CLASS.
641
+ *
642
+ * resolve() above answers by function NAME, which is right for the tracer:
643
+ * it only ever follows taint INTO a function, and entering the wrong one
644
+ * can at worst report a flow that looks odd. The constant proof asks the
645
+ * opposite question - "is what comes back fixed?" - and a wrong answer
646
+ * there deletes a finding. So this answers only when Java itself leaves no
647
+ * choice:
648
+ *
649
+ * - the receiver names a class: `a.b.Utils` by its full name - where
650
+ * `a` must look like a package and not a type - or `Utils` through a
651
+ * single-type import or, failing that, the caller's own package. An
652
+ * import-on-demand (`import a.b.*;`) is not followed;
653
+ * - exactly ONE file in the scan declares that package and a top-level
654
+ * class of that name - two copies is two candidates, and neither wins;
655
+ * - that class declares exactly one method of the name, and it is
656
+ * static. An overload is chosen by argument types, which nobody here
657
+ * reads; an inherited method lives in a class this does not look for.
658
+ *
659
+ * The caller's side - a local variable named `Utils` hides the class - is
660
+ * checked where the call is, in rules/lib/strings.ts.
661
+ */
662
+ staticMethod(
663
+ fromPath: string,
664
+ receiver: string,
665
+ methodName: string,
666
+ supertypes: readonly string[] = [],
667
+ allowInstance = false,
668
+ ) {
669
+ const from = byPath.get(fromPath);
670
+ const facts = from?.javaTypes;
671
+ if (!from || from.language !== 'java' || !facts) return null;
672
+ const dotted = receiver.replace(/\s+/g, '');
673
+ if (!/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(dotted)) return null;
674
+ const segments = dotted.split('.');
675
+ const first = segments[0] ?? '';
676
+ const className = segments[segments.length - 1] ?? '';
677
+
678
+ /*
679
+ * Names that reach this file without a declaration in it: a static import
680
+ * of a member called `first` (a nested type or a field - tests/probes/p04,
681
+ * p05), any `import static a.b.C.*;`, and members inherited from a
682
+ * supertype declared in the scan (p06). Each could be what `first` means.
683
+ * A supertype from OUTSIDE the scan - HttpServlet, say - is not read; the
684
+ * SQL rule's limitations say so.
685
+ */
686
+ if (facts.staticOnDemand || facts.staticImports.includes(first)) return null;
687
+ if (supertypes.some((t) => t.split('.').some((part) => typeNames.has(part)))) return null;
688
+
689
+ let packageName: string;
690
+ if (segments.length > 1) {
691
+ /*
692
+ * `Wrapper.Inner` is a package Wrapper's class Inner ONLY if no type
693
+ * called Wrapper is in scope - a type outranks a package (JLS 6.5.2),
694
+ * and then Inner is Wrapper's nested class. Types in scope include
695
+ * library ones this scan never sees, so the first segment must look
696
+ * like a package: not capitalised (the convention every package
697
+ * follows), not a type of ANY kind anywhere in the scan (an interface
698
+ * called `app` - p07), not imported by name.
699
+ */
700
+ if (
701
+ /^[A-Z]/.test(first) ||
702
+ typeNames.has(first) ||
703
+ facts.singleTypeImports.some((i) => i === first || i.endsWith(`.${first}`))
704
+ ) {
705
+ return null;
706
+ }
707
+ packageName = segments.slice(0, -1).join('.');
708
+ } else {
709
+ const imported = facts.singleTypeImports.filter((i) => i.endsWith(`.${className}`));
710
+ if (imported.length > 1) return null;
711
+ packageName = imported[0]
712
+ ? imported[0].slice(0, imported[0].length - className.length - 1)
713
+ : facts.packageName;
714
+ }
715
+
716
+ // Every kind of type counts as a candidate: a class and an interface
717
+ // with one full name are two candidates, and neither wins (p09).
718
+ const hits = [...byPath.values()].filter(
719
+ (f) =>
720
+ f.language === 'java' &&
721
+ f.javaTypes?.packageName === packageName &&
722
+ f.javaTypes.types.some((t) => t.name === className),
723
+ );
724
+ if (hits.length !== 1 || !hits[0]) return null;
725
+ const winner = hits[0];
726
+ const declared = winner.javaTypes?.types.filter((t) => t.name === className) ?? [];
727
+ if (declared.length !== 1 || declared[0]?.kind !== 'class_declaration') return null;
728
+
729
+ store.pin(winner.path);
730
+ const root = store.get(winner.path).rootNode;
731
+ const klass = root.namedChildren.find(
732
+ (c): c is Node => c?.type === 'class_declaration' && c.childForFieldName('name')?.text === className,
733
+ );
734
+ if (!klass) {
735
+ relocationsFailed++;
736
+ return null;
737
+ }
738
+ /*
739
+ * A class that inherits from anything can inherit a method of this name
740
+ * - a more specific overload, or the only one that takes this many
741
+ * arguments - and that is the one that runs (p01, p02, p03).
742
+ */
743
+ if (klass.childForFieldName('superclass')) return null;
744
+ // An instance method can also come from an interface's default method.
745
+ if (allowInstance && klass.childForFieldName('interfaces')) return null;
746
+ const methods = (klass.childForFieldName('body')?.namedChildren ?? []).filter(
747
+ (m): m is Node => m?.type === 'method_declaration' && m.childForFieldName('name')?.text === methodName,
748
+ );
749
+ const method = methods.length === 1 ? methods[0] : undefined;
750
+ // Static - or, when the caller holds a `new X(...)` and so knows the
751
+ // object is exactly an X, an instance method (strings.ts, heldCreation).
752
+ if (!method || (!allowInstance && !hasModifier(method, 'static'))) return null;
753
+ // A private method cannot be called from another class at all.
754
+ if (winner.path !== fromPath && hasModifier(method, 'private')) return null;
755
+ return { path: winner.path, method };
756
+ },
757
+
758
+ javaTypeInScan: (simpleName: string) => typeNames.has(simpleName),
759
+
760
+ noteAmbiguous() {
761
+ ambiguous++;
762
+ },
763
+
764
+ stats: () => ({
765
+ filesIndexed: byPath.size,
766
+ functionsIndexed,
767
+ importEdges,
768
+ importsUnresolved,
769
+ resolved,
770
+ ambiguous,
771
+ relocationsFailed,
772
+ }),
773
+ };
774
+ }