@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,156 @@
1
+ /**
2
+ * FILE DISCOVERY
3
+ *
4
+ * Turning "scan this path" into "here is the list of files". Boring code, but
5
+ * two decisions in it matter:
6
+ *
7
+ * 1. We skip dependency and build directories. Scanning node_modules finds
8
+ * thousands of "vulnerabilities" in other people's code that you cannot fix
9
+ * and did not write. Dependency risk is a real and separate problem
10
+ * (Software Composition Analysis) and it deserves its own tool, not noise
11
+ * inside this one.
12
+ *
13
+ * 2. We record the extensions we skipped. That list feeds the coverage report,
14
+ * so a scan of a repo that is 70% Ruby says "we did not look at 412 .rb
15
+ * files" instead of quietly reporting "no issues found".
16
+ */
17
+ import { readdir, stat } from 'node:fs/promises';
18
+ import path from 'node:path';
19
+ import { runtimeFileKind } from '../platform/runtime-version.js';
20
+ const IGNORED_DIRECTORIES = new Set([
21
+ '.git', '.hg', '.svn',
22
+ 'node_modules', 'bower_components', 'jspm_packages',
23
+ 'dist', 'build', 'out', 'target', 'bin', 'obj',
24
+ 'vendor', 'third_party',
25
+ '.venv', 'venv', 'env', '__pycache__', '.tox', 'site-packages', '.mypy_cache',
26
+ '.next', '.nuxt', '.svelte-kit', '.turbo', '.parcel-cache',
27
+ 'coverage', '.nyc_output', '.gradle', '.m2',
28
+ '.idea', '.vscode', '.cache', '.pytest_cache',
29
+ ]);
30
+ /** 2 MB. Anything larger is almost always generated, minified or a data blob. */
31
+ const MAX_FILE_BYTES = 2 * 1024 * 1024;
32
+ /**
33
+ * Turn a glob into a regular expression.
34
+ *
35
+ * Supported, deliberately a small subset:
36
+ * ** any number of path segments
37
+ * * anything except a path separator
38
+ * ? one character
39
+ * A pattern with no wildcards and no separator (e.g. `vendor`) matches any path
40
+ * SEGMENT with that name, which is what people mean by `--exclude=vendor`.
41
+ *
42
+ * Everything is matched against the path with forward slashes, so the same
43
+ * pattern works on Windows and Unix.
44
+ */
45
+ export function globToRegExp(pattern) {
46
+ const cleaned = pattern.trim().replace(/\\/g, '/').replace(/\/+$/, '');
47
+ const bare = !/[*?/]/.test(cleaned);
48
+ const escaped = cleaned
49
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
50
+ .replace(/\*\*/g, '\u0000')
51
+ .replace(/\*/g, '[^/]*')
52
+ .replace(/\?/g, '[^/]')
53
+ .replace(/\u0000/g, '.*');
54
+ // `vendor` -> matches the segment anywhere in the path.
55
+ if (bare)
56
+ return new RegExp(`(^|/)${escaped}(/|$)`);
57
+ // `src/**/test.js` or `*.spec.ts` -> match anywhere the shape fits.
58
+ return new RegExp(`(^|/)${escaped}$|^${escaped}$`);
59
+ }
60
+ export async function collectFiles(target, options) {
61
+ const scannable = new Set(options.scannable.map((e) => e.toLowerCase()));
62
+ const maxBytes = options.maxBytes ?? MAX_FILE_BYTES;
63
+ const excludePatterns = (options.exclude ?? []).map(globToRegExp);
64
+ let excludedCount = 0;
65
+ const excludedDirectories = [];
66
+ /** Matched against the path relative to the scan target, slashes normalised. */
67
+ const isExcluded = (fullPath) => {
68
+ if (excludePatterns.length === 0)
69
+ return false;
70
+ const relative = path.relative(target, fullPath).split(path.sep).join('/');
71
+ return excludePatterns.some((pattern) => pattern.test(relative));
72
+ };
73
+ const files = [];
74
+ const skipped = new Map();
75
+ const oversized = [];
76
+ const runtimeFiles = [];
77
+ const info = await stat(target);
78
+ if (info.isFile()) {
79
+ // An explicitly named file is always scanned - if the user pointed at it,
80
+ // they meant it, even if it is huge or in node_modules.
81
+ files.push(target);
82
+ return {
83
+ files,
84
+ skippedByExtension: skipped,
85
+ oversizedFiles: oversized,
86
+ excludedCount,
87
+ excludedDirectories,
88
+ runtimeFiles: runtimeFileKind(path.basename(target)) ? [target] : [],
89
+ };
90
+ }
91
+ const queue = [target];
92
+ while (queue.length > 0) {
93
+ const dir = queue.pop();
94
+ if (!dir)
95
+ break;
96
+ let entries;
97
+ try {
98
+ entries = await readdir(dir, { withFileTypes: true });
99
+ }
100
+ catch {
101
+ continue; // unreadable directory - permissions, broken symlink, race
102
+ }
103
+ for (const entry of entries) {
104
+ const full = path.join(dir, entry.name);
105
+ if (entry.isDirectory()) {
106
+ if (IGNORED_DIRECTORIES.has(entry.name))
107
+ continue;
108
+ if (entry.name.startsWith('.') && entry.name !== '.')
109
+ continue;
110
+ // Pruning an excluded directory here saves walking its whole subtree.
111
+ if (isExcluded(full)) {
112
+ excludedDirectories.push(path.relative(target, full).split(path.sep).join('/'));
113
+ continue;
114
+ }
115
+ queue.push(full);
116
+ continue;
117
+ }
118
+ if (!entry.isFile())
119
+ continue;
120
+ if (isExcluded(full)) {
121
+ excludedCount++;
122
+ continue;
123
+ }
124
+ if (runtimeFileKind(entry.name))
125
+ runtimeFiles.push(full);
126
+ const ext = path.extname(entry.name).toLowerCase();
127
+ if (!scannable.has(ext)) {
128
+ if (ext)
129
+ skipped.set(ext, (skipped.get(ext) ?? 0) + 1);
130
+ continue;
131
+ }
132
+ try {
133
+ const fileInfo = await stat(full);
134
+ if (fileInfo.size > maxBytes) {
135
+ oversized.push(full);
136
+ continue;
137
+ }
138
+ }
139
+ catch {
140
+ continue;
141
+ }
142
+ files.push(full);
143
+ }
144
+ }
145
+ files.sort();
146
+ excludedDirectories.sort();
147
+ return {
148
+ files,
149
+ skippedByExtension: skipped,
150
+ oversizedFiles: oversized,
151
+ excludedCount,
152
+ excludedDirectories,
153
+ runtimeFiles,
154
+ };
155
+ }
156
+ //# sourceMappingURL=walk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"walk.js","sourceRoot":"","sources":["../../../src/engine/walk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEjE,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC;IAClC,MAAM,EAAE,KAAK,EAAE,MAAM;IACrB,cAAc,EAAE,kBAAkB,EAAE,eAAe;IACnD,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK;IAC9C,QAAQ,EAAE,aAAa;IACvB,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,eAAe,EAAE,aAAa;IAC7E,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,eAAe;IAC1D,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,KAAK;IAC3C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,eAAe;CAC9C,CAAC,CAAC;AAEH,iFAAiF;AACjF,MAAM,cAAc,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAiCvC;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACvE,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAEpC,MAAM,OAAO,GAAG,OAAO;SACpB,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC;SACpC,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC;SAC1B,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC;SACvB,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC;SACtB,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAE5B,wDAAwD;IACxD,IAAI,IAAI;QAAE,OAAO,IAAI,MAAM,CAAC,QAAQ,OAAO,OAAO,CAAC,CAAC;IACpD,oEAAoE;IACpE,OAAO,IAAI,MAAM,CAAC,QAAQ,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAc,EAAE,OAAoB;IACrE,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,cAAc,CAAC;IACpD,MAAM,eAAe,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAClE,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,MAAM,mBAAmB,GAAa,EAAE,CAAC;IAEzC,gFAAgF;IAChF,MAAM,UAAU,GAAG,CAAC,QAAgB,EAAW,EAAE;QAC/C,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC3E,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IACnE,CAAC,CAAC;IAEF,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC1C,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,MAAM,YAAY,GAAa,EAAE,CAAC;IAElC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC;IAChC,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;QAClB,0EAA0E;QAC1E,wDAAwD;QACxD,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnB,OAAO;YACL,KAAK;YACL,kBAAkB,EAAE,OAAO;YAC3B,cAAc,EAAE,SAAS;YACzB,aAAa;YACb,mBAAmB;YACnB,YAAY,EAAE,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE;SACrE,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAa,CAAC,MAAM,CAAC,CAAC;IACjC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC;QACxB,IAAI,CAAC,GAAG;YAAE,MAAM;QAEhB,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACxD,CAAC;QAAC,MAAM,CAAC;YACP,SAAS,CAAC,2DAA2D;QACvE,CAAC;QAED,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAExC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,IAAI,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;oBAAE,SAAS;gBAClD,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,GAAG;oBAAE,SAAS;gBAC/D,sEAAsE;gBACtE,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;oBACrB,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;oBAChF,SAAS;gBACX,CAAC;gBACD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACjB,SAAS;YACX,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;gBAAE,SAAS;YAE9B,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;gBACrB,aAAa,EAAE,CAAC;gBAChB,SAAS;YACX,CAAC;YAED,IAAI,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAEzD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;YACnD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBACxB,IAAI,GAAG;oBAAE,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBACvD,SAAS;YACX,CAAC;YAED,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;gBAClC,IAAI,QAAQ,CAAC,IAAI,GAAG,QAAQ,EAAE,CAAC;oBAC7B,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBACrB,SAAS;gBACX,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS;YACX,CAAC;YAED,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI,EAAE,CAAC;IACb,mBAAmB,CAAC,IAAI,EAAE,CAAC;IAC3B,OAAO;QACL,KAAK;QACL,kBAAkB,EAAE,OAAO;QAC3B,cAAc,EAAE,SAAS;QACzB,aAAa;QACb,mBAAmB;QACnB,YAAY;KACb,CAAC;AACJ,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A human-readable printer for syntax trees.
3
+ *
4
+ * This exists purely so you can SEE what the parser sees. Run:
5
+ * npm run ast -- somefile.js
6
+ * and compare the output to the source. Once the shape of the tree stops
7
+ * looking mysterious, writing detection rules becomes ordinary programming.
8
+ *
9
+ * Two concepts show up in the output:
10
+ *
11
+ * NAMED vs ANONYMOUS nodes. `db.query(x)` contains the parentheses and the
12
+ * dot as nodes too, but they carry no meaning of their own - tree-sitter calls
13
+ * those "anonymous". By default we hide them, because rules almost never care.
14
+ *
15
+ * FIELDS. Some children have a role, not just a position. In
16
+ * `binary_expression`, the children are labelled `left`, `operator`, `right`.
17
+ * Fields are how a rule says "the thing being called" instead of "child #0",
18
+ * which is what makes rules survive grammar updates.
19
+ */
20
+ import type { Node } from 'web-tree-sitter';
21
+ export interface AstDumpOptions {
22
+ /** Show punctuation/keyword nodes too. Default false. */
23
+ readonly includeAnonymous?: boolean;
24
+ /** Stop after this depth. Default 12. */
25
+ readonly maxDepth?: number;
26
+ /** Stop after this many lines, so a huge file doesn't flood the terminal. */
27
+ readonly maxLines?: number;
28
+ /** Show the source text of leaf nodes. Default true. */
29
+ readonly showText?: boolean;
30
+ }
31
+ export declare function dumpAst(root: Node, options?: AstDumpOptions): string;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * A human-readable printer for syntax trees.
3
+ *
4
+ * This exists purely so you can SEE what the parser sees. Run:
5
+ * npm run ast -- somefile.js
6
+ * and compare the output to the source. Once the shape of the tree stops
7
+ * looking mysterious, writing detection rules becomes ordinary programming.
8
+ *
9
+ * Two concepts show up in the output:
10
+ *
11
+ * NAMED vs ANONYMOUS nodes. `db.query(x)` contains the parentheses and the
12
+ * dot as nodes too, but they carry no meaning of their own - tree-sitter calls
13
+ * those "anonymous". By default we hide them, because rules almost never care.
14
+ *
15
+ * FIELDS. Some children have a role, not just a position. In
16
+ * `binary_expression`, the children are labelled `left`, `operator`, `right`.
17
+ * Fields are how a rule says "the thing being called" instead of "child #0",
18
+ * which is what makes rules survive grammar updates.
19
+ */
20
+ export function dumpAst(root, options = {}) {
21
+ const includeAnonymous = options.includeAnonymous ?? false;
22
+ const maxDepth = options.maxDepth ?? 12;
23
+ const maxLines = options.maxLines ?? 400;
24
+ const showText = options.showText ?? true;
25
+ const lines = [];
26
+ const walk = (node, depth, fieldName, prefix, isLast) => {
27
+ if (lines.length >= maxLines)
28
+ return;
29
+ const connector = depth === 0 ? '' : isLast ? '└─ ' : '├─ ';
30
+ const label = fieldName ? `${fieldName}: ${node.type}` : node.type;
31
+ const position = `[${node.startPosition.row + 1}:${node.startPosition.column + 1}]`;
32
+ // Show source text only for leaves - printing it for a whole function is noise.
33
+ let text = '';
34
+ if (showText && node.namedChildCount === 0) {
35
+ const raw = (node.text ?? '').replace(/\s+/g, ' ').trim();
36
+ if (raw.length > 0)
37
+ text = ` "${raw.length > 48 ? `${raw.slice(0, 47)}…` : raw}"`;
38
+ }
39
+ lines.push(`${prefix}${connector}${label} ${position}${text}`);
40
+ if (depth >= maxDepth) {
41
+ if (node.namedChildCount > 0) {
42
+ lines.push(`${prefix}${isLast ? ' ' : '│ '}└─ … (${node.namedChildCount} more, depth limit)`);
43
+ }
44
+ return;
45
+ }
46
+ const children = [];
47
+ for (let i = 0; i < node.childCount; i++) {
48
+ const child = node.child(i);
49
+ if (!child)
50
+ continue;
51
+ if (!includeAnonymous && !child.isNamed)
52
+ continue;
53
+ children.push({ node: child, field: node.fieldNameForChild(i) });
54
+ }
55
+ const childPrefix = depth === 0 ? '' : prefix + (isLast ? ' ' : '│ ');
56
+ children.forEach((child, index) => {
57
+ walk(child.node, depth + 1, child.field, childPrefix, index === children.length - 1);
58
+ });
59
+ };
60
+ walk(root, 0, null, '', true);
61
+ if (lines.length >= maxLines) {
62
+ lines.push(`… output truncated at ${maxLines} lines (use --max-lines to raise)`);
63
+ }
64
+ return lines.join('\n');
65
+ }
66
+ //# sourceMappingURL=astDump.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"astDump.js","sourceRoot":"","sources":["../../../src/parse/astDump.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * LANGUAGE REGISTRY
3
+ * =================
4
+ *
5
+ * A scanner has to answer "what language is this file?" before it can answer
6
+ * "is this file dangerous?". This file is the single place that knows.
7
+ *
8
+ * ARCHITECTURAL DECISION (matters in Phase 5, language expansion):
9
+ * languages are DATA, not code. Adding Ruby later means adding one object to
10
+ * the array below plus one query file per rule - it does not mean editing the
11
+ * parser, the engine, the CLI, or any existing rule. If adding a language ever
12
+ * requires touching the engine, we designed it wrong.
13
+ *
14
+ * WHY WASM GRAMMARS INSTEAD OF NATIVE ONES:
15
+ * Tree-sitter ships two ways to run a grammar - a native C module you compile
16
+ * on your machine, or a WebAssembly (.wasm) file that runs anywhere. We chose
17
+ * WASM for three reasons:
18
+ * 1. No compiler toolchain needed. `npm install` just works, on any OS.
19
+ * 2. The SAME grammar files run in a browser. Phase 2 is a browser UI - it
20
+ * will run this identical engine instead of a reimplementation of it.
21
+ * 3. The tree-sitter-wasms package already ships 36 grammars, so Phase 5's
22
+ * language expansion is mostly unlocking files we already have on disk.
23
+ */
24
+ /** The languages Defuse supports. */
25
+ export type LanguageId = 'javascript' | 'typescript' | 'python' | 'java' | 'go' | 'php' | 'c' | 'cpp';
26
+ export interface Dialect {
27
+ /** File extensions handled by this dialect. */
28
+ readonly extensions: readonly string[];
29
+ /** Grammar name inside tree-sitter-wasms, i.e. tree-sitter-<name>.wasm */
30
+ readonly grammar: string;
31
+ }
32
+ export interface LanguageSpec {
33
+ readonly id: LanguageId;
34
+ readonly displayName: string;
35
+ /** Primary extensions -> primary grammar. */
36
+ readonly extensions: readonly string[];
37
+ readonly grammar: string;
38
+ /**
39
+ * Variants of the same language that need a DIFFERENT grammar file but share
40
+ * rules. e.g. .tsx needs the tsx grammar, but a SQL-injection rule written for
41
+ * TypeScript matches .tsx code perfectly well.
42
+ */
43
+ readonly dialects?: readonly Dialect[];
44
+ /** Interpreter names seen in a `#!` shebang line, for extensionless scripts. */
45
+ readonly shebangs?: readonly string[];
46
+ /** Honest notes about what this language's support does and does not include. */
47
+ readonly notes: string;
48
+ }
49
+ export declare const LANGUAGES: readonly LanguageSpec[];
50
+ export interface LanguageMatch {
51
+ readonly spec: LanguageSpec;
52
+ /** Which .wasm grammar to actually load (may differ from spec.grammar for dialects). */
53
+ readonly grammar: string;
54
+ /** How we decided. Reported in --json so detection is auditable, never magic. */
55
+ readonly detectedBy: 'extension' | 'shebang';
56
+ }
57
+ /**
58
+ * Decide which language a file is.
59
+ *
60
+ * Step 1: file extension. Boring, fast, right ~99% of the time.
61
+ * Step 2: if there is no useful extension, read the first line and look for a
62
+ * "shebang" - the `#!/usr/bin/env python3` line that tells Unix which
63
+ * interpreter to run. This is how we catch a CLI script named `deploy`
64
+ * with no extension at all.
65
+ *
66
+ * Returns null for "we don't handle this", which is a normal answer, not an error.
67
+ */
68
+ export declare function detectLanguage(filePath: string, firstLine?: string): LanguageMatch | null;
69
+ /** Every extension we will attempt to open. Used by the file walker. */
70
+ export declare const SCANNABLE_EXTENSIONS: readonly string[];
@@ -0,0 +1,174 @@
1
+ /**
2
+ * LANGUAGE REGISTRY
3
+ * =================
4
+ *
5
+ * A scanner has to answer "what language is this file?" before it can answer
6
+ * "is this file dangerous?". This file is the single place that knows.
7
+ *
8
+ * ARCHITECTURAL DECISION (matters in Phase 5, language expansion):
9
+ * languages are DATA, not code. Adding Ruby later means adding one object to
10
+ * the array below plus one query file per rule - it does not mean editing the
11
+ * parser, the engine, the CLI, or any existing rule. If adding a language ever
12
+ * requires touching the engine, we designed it wrong.
13
+ *
14
+ * WHY WASM GRAMMARS INSTEAD OF NATIVE ONES:
15
+ * Tree-sitter ships two ways to run a grammar - a native C module you compile
16
+ * on your machine, or a WebAssembly (.wasm) file that runs anywhere. We chose
17
+ * WASM for three reasons:
18
+ * 1. No compiler toolchain needed. `npm install` just works, on any OS.
19
+ * 2. The SAME grammar files run in a browser. Phase 2 is a browser UI - it
20
+ * will run this identical engine instead of a reimplementation of it.
21
+ * 3. The tree-sitter-wasms package already ships 36 grammars, so Phase 5's
22
+ * language expansion is mostly unlocking files we already have on disk.
23
+ */
24
+ export const LANGUAGES = [
25
+ {
26
+ id: 'javascript',
27
+ displayName: 'JavaScript',
28
+ extensions: ['.js', '.mjs', '.cjs', '.jsx'],
29
+ grammar: 'javascript',
30
+ shebangs: ['node'],
31
+ notes: 'The javascript grammar parses JSX natively, so .jsx needs no separate grammar. ' +
32
+ 'Vue/Svelte single-file components are NOT supported - their <script> blocks are ' +
33
+ 'not extracted. Documented gap, not a silent one.',
34
+ },
35
+ {
36
+ id: 'typescript',
37
+ displayName: 'TypeScript',
38
+ extensions: ['.ts', '.mts', '.cts'],
39
+ grammar: 'typescript',
40
+ dialects: [{ extensions: ['.tsx'], grammar: 'tsx' }],
41
+ notes: 'Type annotations are parsed but NOT used for analysis. Knowing that ' +
42
+ 'a parameter is typed `string` tells us nothing about whether it is attacker-' +
43
+ 'controlled, so we deliberately do not pretend it does. .d.ts files are skipped.',
44
+ },
45
+ {
46
+ id: 'python',
47
+ displayName: 'Python',
48
+ extensions: ['.py', '.pyi'],
49
+ grammar: 'python',
50
+ shebangs: ['python', 'python2', 'python3'],
51
+ notes: 'Python 3 grammar. Python 2 print-statements will produce parse errors, which we ' +
52
+ 'report rather than swallow.',
53
+ },
54
+ {
55
+ id: 'java',
56
+ displayName: 'Java',
57
+ extensions: ['.java'],
58
+ grammar: 'java',
59
+ notes: 'Plain Java only. .jsp, .kt (Kotlin) and .scala are separate languages and are not ' +
60
+ 'covered by these rules even though a Java-ish pattern might appear in them.',
61
+ },
62
+ {
63
+ id: 'php',
64
+ displayName: 'PHP',
65
+ extensions: ['.php', '.phtml'],
66
+ grammar: 'php',
67
+ notes: 'PHP source only. A .php file is parsed as a whole document, so HTML outside ' +
68
+ '<?php ?> is text rather than markup - an XSS sink written in raw HTML with a ' +
69
+ 'short echo tag (<?= $x ?>) IS matched, but HTML attributes around it are not ' +
70
+ 'analysed. Blade (.blade.php) and Twig (.twig) templates are NOT parsed.',
71
+ },
72
+ {
73
+ id: 'go',
74
+ displayName: 'Go',
75
+ extensions: ['.go'],
76
+ grammar: 'go',
77
+ notes: 'Go source only; go.mod, go.sum and generated protobuf files are skipped as noise.',
78
+ },
79
+ {
80
+ id: 'c',
81
+ displayName: 'C',
82
+ extensions: ['.c'],
83
+ grammar: 'c',
84
+ notes: 'MEMORY SAFETY IS NOT ANALYSED. C is famous for buffer overflows, use-after-free, ' +
85
+ 'double-free and integer overflow, and this engine models none of them - it tracks ' +
86
+ 'values, not sizes, allocations or pointer aliasing. What IS analysed is the same ' +
87
+ 'thing analysed in every other language: attacker-controlled data reaching a ' +
88
+ 'dangerous call. In C that means shells (system, popen, exec*), format strings ' +
89
+ '(printf-family, where a tainted FORMAT argument is CWE-134 and a taint engine ' +
90
+ 'catches it exactly), and copies with no length bound (strcpy, strcat, sprintf, ' +
91
+ 'gets). A finding that data reaches an unbounded copy is NOT a claim that the ' +
92
+ 'destination is too small - that needs size analysis we do not have - so the ' +
93
+ 'wording says "reaches a copy with no length bound" and means only that. ' +
94
+ 'THE PREPROCESSOR IS NOT RUN: #include is not followed, #ifdef branches are all ' +
95
+ 'parsed as though taken, and a macro that hides a sink (#define RUN(x) system(x)) ' +
96
+ 'is invisible.',
97
+ },
98
+ {
99
+ id: 'cpp',
100
+ displayName: 'C++',
101
+ extensions: ['.cpp', '.cc', '.cxx', '.c++', '.hpp', '.hh', '.hxx', '.h'],
102
+ grammar: 'cpp',
103
+ notes: 'Every C limitation applies, plus: templates are parsed but not instantiated, so a ' +
104
+ 'sink reached only through a template specialisation is missed, and operator ' +
105
+ 'overloading is not resolved - a custom operator<< that executes a command reads ' +
106
+ 'as an ordinary stream write. ' +
107
+ 'THE .h EXTENSION IS TREATED AS C++, deliberately and against appearances. It is ' +
108
+ 'ambiguous - most .h files in the world are C - but the two grammars are not ' +
109
+ 'symmetric: measured on this project\'s own fixtures, the C++ grammar parses C ' +
110
+ 'with zero error nodes while the C grammar produces eleven on C++. Choosing the ' +
111
+ 'superset costs a C project a cosmetic mislabel in the per-language counts; ' +
112
+ 'choosing C would cost a C++ header every std:: sink in it, silently. A wrong ' +
113
+ 'label is better than a missing finding.',
114
+ },
115
+ ];
116
+ /** Extension -> { language, grammar-to-use }. Built once at module load. */
117
+ const BY_EXTENSION = new Map();
118
+ for (const spec of LANGUAGES) {
119
+ for (const ext of spec.extensions) {
120
+ BY_EXTENSION.set(ext, { spec, grammar: spec.grammar });
121
+ }
122
+ for (const dialect of spec.dialects ?? []) {
123
+ for (const ext of dialect.extensions) {
124
+ BY_EXTENSION.set(ext, { spec, grammar: dialect.grammar });
125
+ }
126
+ }
127
+ }
128
+ /** Files we never scan, because scanning them is noise, not security. */
129
+ const SKIP_FILE_PATTERNS = [
130
+ /\.d\.ts$/,
131
+ /\.min\.js$/,
132
+ /\.bundle\.js$/,
133
+ /\.pb\.go$/,
134
+ /_pb2\.py$/,
135
+ /\.generated\./,
136
+ ];
137
+ /**
138
+ * Decide which language a file is.
139
+ *
140
+ * Step 1: file extension. Boring, fast, right ~99% of the time.
141
+ * Step 2: if there is no useful extension, read the first line and look for a
142
+ * "shebang" - the `#!/usr/bin/env python3` line that tells Unix which
143
+ * interpreter to run. This is how we catch a CLI script named `deploy`
144
+ * with no extension at all.
145
+ *
146
+ * Returns null for "we don't handle this", which is a normal answer, not an error.
147
+ */
148
+ export function detectLanguage(filePath, firstLine) {
149
+ const lower = filePath.toLowerCase();
150
+ for (const pattern of SKIP_FILE_PATTERNS) {
151
+ if (pattern.test(lower))
152
+ return null;
153
+ }
154
+ const dot = lower.lastIndexOf('.');
155
+ const slash = Math.max(lower.lastIndexOf('/'), lower.lastIndexOf('\\'));
156
+ if (dot > slash) {
157
+ const hit = BY_EXTENSION.get(lower.slice(dot));
158
+ if (hit)
159
+ return { spec: hit.spec, grammar: hit.grammar, detectedBy: 'extension' };
160
+ }
161
+ if (firstLine?.startsWith('#!')) {
162
+ // "#!/usr/bin/env python3" -> we want the word "python3"
163
+ const words = firstLine.slice(2).trim().split(/[\s/]+/);
164
+ for (const spec of LANGUAGES) {
165
+ if (spec.shebangs?.some((s) => words.includes(s))) {
166
+ return { spec, grammar: spec.grammar, detectedBy: 'shebang' };
167
+ }
168
+ }
169
+ }
170
+ return null;
171
+ }
172
+ /** Every extension we will attempt to open. Used by the file walker. */
173
+ export const SCANNABLE_EXTENSIONS = [...BY_EXTENSION.keys()];
174
+ //# sourceMappingURL=languages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"languages.js","sourceRoot":"","sources":["../../../src/parse/languages.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Turning a tree-sitter node into a human-readable, reproducible location.
3
+ *
4
+ * Tree-sitter counts rows and columns from 0. Editors, terminals and humans
5
+ * count from 1. Every off-by-one bug in a scanner's output lives right here, so
6
+ * the conversion happens in exactly ONE place: this file.
7
+ */
8
+ import type { Node } from 'web-tree-sitter';
9
+ import type { CodeLocation } from '../core/finding.js';
10
+ /** Walk up the tree collecting node types: "program > call_expression > ...". */
11
+ export declare function astPath(node: Node, maxDepth?: number): string;
12
+ /**
13
+ * Build the location record that goes into a finding.
14
+ * `file` is passed in because a node does not know which file it came from -
15
+ * tree-sitter only ever sees a string of text.
16
+ */
17
+ export declare function nodeLocation(node: Node, file: string): CodeLocation;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Turning a tree-sitter node into a human-readable, reproducible location.
3
+ *
4
+ * Tree-sitter counts rows and columns from 0. Editors, terminals and humans
5
+ * count from 1. Every off-by-one bug in a scanner's output lives right here, so
6
+ * the conversion happens in exactly ONE place: this file.
7
+ */
8
+ const MAX_SNIPPET = 160;
9
+ /** Walk up the tree collecting node types: "program > call_expression > ...". */
10
+ export function astPath(node, maxDepth = 5) {
11
+ const parts = [];
12
+ let current = node;
13
+ while (current && parts.length < maxDepth) {
14
+ parts.unshift(current.type);
15
+ current = current.parent;
16
+ }
17
+ return parts.join(' > ');
18
+ }
19
+ /**
20
+ * Build the location record that goes into a finding.
21
+ * `file` is passed in because a node does not know which file it came from -
22
+ * tree-sitter only ever sees a string of text.
23
+ */
24
+ export function nodeLocation(node, file) {
25
+ const raw = node.text ?? '';
26
+ const collapsed = raw.replace(/\s+/g, ' ').trim();
27
+ return {
28
+ file,
29
+ startLine: node.startPosition.row + 1,
30
+ startColumn: node.startPosition.column + 1,
31
+ endLine: node.endPosition.row + 1,
32
+ endColumn: node.endPosition.column + 1,
33
+ astNodeType: node.type,
34
+ astNodePath: astPath(node),
35
+ snippet: collapsed.length > MAX_SNIPPET ? `${collapsed.slice(0, MAX_SNIPPET - 1)}…` : collapsed,
36
+ };
37
+ }
38
+ //# sourceMappingURL=location.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"location.js","sourceRoot":"","sources":["../../../src/parse/location.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,MAAM,WAAW,GAAG,GAAG,CAAC;AAExB,iFAAiF;AACjF,MAAM,UAAU,OAAO,CAAC,IAAU,EAAE,QAAQ,GAAG,CAAC;IAC9C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,OAAO,GAAgB,IAAI,CAAC;IAChC,OAAO,OAAO,IAAI,KAAK,CAAC,MAAM,GAAG,QAAQ,EAAE,CAAC;QAC1C,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAC3B,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,IAAU,EAAE,IAAY;IACnD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;IAC5B,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAClD,OAAO;QACL,IAAI;QACJ,SAAS,EAAE,IAAI,CAAC,aAAa,CAAC,GAAG,GAAG,CAAC;QACrC,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC;QAC1C,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,GAAG,GAAG,CAAC;QACjC,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC;QACtC,WAAW,EAAE,IAAI,CAAC,IAAI;QACtB,WAAW,EAAE,OAAO,CAAC,IAAI,CAAC;QAC1B,OAAO,EACL,SAAS,CAAC,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;KACzF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * THE NODE SHELL for grammar loading and file reading.
3
+ *
4
+ * Everything in here is the half of the tool that only makes sense on a
5
+ * machine with a filesystem. The browser has its own equivalent in
6
+ * ui/worker.js, and the analysis code in between never learns which one it got.
7
+ */
8
+ import { type ParseOutcome } from './parser.js';
9
+ /** Point the parser at node_modules. Called once, before any scan. */
10
+ export declare function useNodeGrammars(): void;
11
+ /** Read a file off disk, then hand the text to the platform-agnostic parser. */
12
+ export declare function parseFile(filePath: string): Promise<ParseOutcome>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * THE NODE SHELL for grammar loading and file reading.
3
+ *
4
+ * Everything in here is the half of the tool that only makes sense on a
5
+ * machine with a filesystem. The browser has its own equivalent in
6
+ * ui/worker.js, and the analysis code in between never learns which one it got.
7
+ */
8
+ import { readFile } from 'node:fs/promises';
9
+ import { createRequire } from 'node:module';
10
+ import path from 'node:path';
11
+ import { configureParsing, parseSourceFile } from './parser.js';
12
+ const require = createRequire(import.meta.url);
13
+ /** Folder inside node_modules that holds the prebuilt .wasm grammar files. */
14
+ const GRAMMAR_DIR = path.join(path.dirname(require.resolve('tree-sitter-wasms/package.json')), 'out');
15
+ /** Point the parser at node_modules. Called once, before any scan. */
16
+ export function useNodeGrammars() {
17
+ configureParsing({
18
+ loadGrammar: (grammar) => path.join(GRAMMAR_DIR, `tree-sitter-${grammar}.wasm`),
19
+ });
20
+ }
21
+ /** Read a file off disk, then hand the text to the platform-agnostic parser. */
22
+ export async function parseFile(filePath) {
23
+ try {
24
+ const source = await readFile(filePath, 'utf8');
25
+ return parseSourceFile(filePath, source);
26
+ }
27
+ catch (error) {
28
+ return {
29
+ status: 'skipped',
30
+ path: filePath,
31
+ reason: `unreadable: ${error.message}`,
32
+ };
33
+ }
34
+ }
35
+ //# sourceMappingURL=node-grammars.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-grammars.js","sourceRoot":"","sources":["../../../src/parse/node-grammars.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAqB,MAAM,aAAa,CAAC;AAEnF,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C,8EAA8E;AAC9E,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAC3B,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,gCAAgC,CAAC,CAAC,EAC/D,KAAK,CACN,CAAC;AAEF,sEAAsE;AACtE,MAAM,UAAU,eAAe;IAC7B,gBAAgB,CAAC;QACf,WAAW,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,eAAe,OAAO,OAAO,CAAC;KAChF,CAAC,CAAC;AACL,CAAC;AAED,gFAAgF;AAChF,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,QAAgB;IAC9C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAChD,OAAO,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC3C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,MAAM,EAAE,SAAS;YACjB,IAAI,EAAE,QAAQ;YACd,MAAM,EAAE,eAAgB,KAAe,CAAC,OAAO,EAAE;SAClD,CAAC;IACJ,CAAC;AACH,CAAC"}