@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,126 @@
1
+ /**
2
+ * WHAT IS AN AST? (read this once, then the rest of the project makes sense)
3
+ * =========================================================================
4
+ *
5
+ * When you write:
6
+ *
7
+ * const total = price + tax;
8
+ *
9
+ * your computer does not see a sentence. It sees a TREE. "AST" stands for
10
+ * Abstract Syntax Tree: "abstract" because it throws away things that don't
11
+ * change meaning (spaces, most punctuation), "syntax" because it captures the
12
+ * grammar of the language, and "tree" because code nests inside code.
13
+ *
14
+ * That line becomes something like:
15
+ *
16
+ * lexical_declaration
17
+ * +- variable_declarator
18
+ * +- name: identifier "total"
19
+ * +- value: binary_expression
20
+ * +- left: identifier "price"
21
+ * +- operator: "+"
22
+ * +- right: identifier "tax"
23
+ *
24
+ * Why this matters enormously for a security scanner:
25
+ *
26
+ * The old way (what most cheap scanners do, and what earlier versions of this
27
+ * project did) is to search the raw TEXT with regular expressions. Text search
28
+ * cannot tell the difference between:
29
+ *
30
+ * db.query("SELECT * FROM users WHERE id = " + userId) <- real problem
31
+ * // db.query("SELECT * FROM users WHERE id = " + userId) <- a comment
32
+ * log("db.query is unsafe when you do \" + userId\"") <- a string
33
+ *
34
+ * All three contain the same characters. Only one is a vulnerability. An AST
35
+ * knows which is which, because the parser already decided "this is a comment",
36
+ * "this is a string literal", "this is a real function call". We get that
37
+ * accuracy for free, and it is the entire reason we use tree-sitter.
38
+ *
39
+ * Tree-sitter is Github's parser toolkit. It is fast, it is error-tolerant
40
+ * (it produces a usable tree even for code that doesn't compile - important,
41
+ * because real repositories contain broken files), and it has mature grammars
42
+ * for dozens of languages that all behave the same way.
43
+ */
44
+ import { Language, type Node, type Tree } from 'web-tree-sitter';
45
+ import { type LanguageSpec } from './languages.js';
46
+ /**
47
+ * WHERE DO THE .wasm GRAMMARS COME FROM?
48
+ *
49
+ * On Node they are files in node_modules. In a browser they are URLs fetched
50
+ * over HTTP. That is the ONLY difference between the two platforms in the whole
51
+ * analysis path, so it is the only thing abstracted: everything else - parsing,
52
+ * rules, taint - is identical code running in both places.
53
+ *
54
+ * A loader returns whatever `Language.load` accepts: a path, a URL, or bytes.
55
+ */
56
+ export type GrammarLoader = (grammar: string) => string | Uint8Array | Promise<string | Uint8Array>;
57
+ /** Called once by whichever shell is in charge (CLI or browser worker). */
58
+ export declare function configureParsing(options: {
59
+ loadGrammar: GrammarLoader;
60
+ init?: () => Promise<void>;
61
+ }): void;
62
+ /**
63
+ * Boot the WebAssembly runtime. Must happen once before any parsing.
64
+ * We memoise the promise so calling this from ten places still only boots once.
65
+ */
66
+ export declare function initEngine(): Promise<void>;
67
+ export declare function getLanguage(grammar: string): Promise<Language>;
68
+ /** A place where tree-sitter could not make sense of the source. */
69
+ export interface ParseIssue {
70
+ readonly kind: 'ERROR' | 'MISSING';
71
+ readonly line: number;
72
+ readonly column: number;
73
+ readonly text: string;
74
+ }
75
+ /** Everything downstream code needs about one parsed file. */
76
+ export interface ParsedFile {
77
+ readonly path: string;
78
+ readonly language: LanguageSpec;
79
+ /** May differ from language.grammar for dialects (.tsx uses the tsx grammar). */
80
+ readonly grammar: string;
81
+ readonly detectedBy: 'extension' | 'shebang';
82
+ readonly source: string;
83
+ readonly tree: Tree;
84
+ readonly root: Node;
85
+ /**
86
+ * Parse problems, reported not hidden. A file with issues is still scanned -
87
+ * tree-sitter recovers - but the report says so, because findings from a
88
+ * partially-understood file deserve less trust.
89
+ */
90
+ readonly parseIssues: readonly ParseIssue[];
91
+ }
92
+ export type ParseOutcome = {
93
+ readonly status: 'parsed';
94
+ readonly file: ParsedFile;
95
+ } | {
96
+ readonly status: 'skipped';
97
+ readonly path: string;
98
+ readonly reason: string;
99
+ };
100
+ /** Parse a string of code you already have in memory. Used by tests and by `ast`. */
101
+ export declare function parseSource(source: string, grammar: string): Promise<Tree>;
102
+ /**
103
+ * Parse WITHOUT awaiting, for a grammar that has already been loaded.
104
+ *
105
+ * WHY THIS EXISTS. A syntax tree lives in WebAssembly memory, which the
106
+ * JavaScript garbage collector cannot see into and will never reclaim; the only
107
+ * way to free one is to call tree.delete() by hand. Holding every tree in a
108
+ * repository at once therefore costs about sixty times the source - measured,
109
+ * not guessed - and that is the ceiling a large scan dies against.
110
+ *
111
+ * The fix is to free trees and parse a file again if something needs it later.
112
+ * "Later" is inside the tracer, which is synchronous all the way down, so the
113
+ * re-parse has to be synchronous too.
114
+ *
115
+ * It is safe to demand that here because loading is what is actually async, and
116
+ * by the time anything re-parses, every grammar in the scan has already been
117
+ * loaded during the first pass. If that assumption ever breaks, this throws
118
+ * rather than silently returning nothing - a missing tree would look exactly
119
+ * like a file with no vulnerabilities in it.
120
+ */
121
+ export declare function parseSourceSync(source: string, grammar: string): Tree;
122
+ /**
123
+ * Work out a file's language and parse it, from text already in memory.
124
+ * No filesystem, no platform assumptions - this is what the browser calls too.
125
+ */
126
+ export declare function parseSourceFile(filePath: string, source: string): Promise<ParseOutcome>;
@@ -0,0 +1,202 @@
1
+ /**
2
+ * WHAT IS AN AST? (read this once, then the rest of the project makes sense)
3
+ * =========================================================================
4
+ *
5
+ * When you write:
6
+ *
7
+ * const total = price + tax;
8
+ *
9
+ * your computer does not see a sentence. It sees a TREE. "AST" stands for
10
+ * Abstract Syntax Tree: "abstract" because it throws away things that don't
11
+ * change meaning (spaces, most punctuation), "syntax" because it captures the
12
+ * grammar of the language, and "tree" because code nests inside code.
13
+ *
14
+ * That line becomes something like:
15
+ *
16
+ * lexical_declaration
17
+ * +- variable_declarator
18
+ * +- name: identifier "total"
19
+ * +- value: binary_expression
20
+ * +- left: identifier "price"
21
+ * +- operator: "+"
22
+ * +- right: identifier "tax"
23
+ *
24
+ * Why this matters enormously for a security scanner:
25
+ *
26
+ * The old way (what most cheap scanners do, and what earlier versions of this
27
+ * project did) is to search the raw TEXT with regular expressions. Text search
28
+ * cannot tell the difference between:
29
+ *
30
+ * db.query("SELECT * FROM users WHERE id = " + userId) <- real problem
31
+ * // db.query("SELECT * FROM users WHERE id = " + userId) <- a comment
32
+ * log("db.query is unsafe when you do \" + userId\"") <- a string
33
+ *
34
+ * All three contain the same characters. Only one is a vulnerability. An AST
35
+ * knows which is which, because the parser already decided "this is a comment",
36
+ * "this is a string literal", "this is a real function call". We get that
37
+ * accuracy for free, and it is the entire reason we use tree-sitter.
38
+ *
39
+ * Tree-sitter is Github's parser toolkit. It is fast, it is error-tolerant
40
+ * (it produces a usable tree even for code that doesn't compile - important,
41
+ * because real repositories contain broken files), and it has mature grammars
42
+ * for dozens of languages that all behave the same way.
43
+ */
44
+ import { Language, Parser } from 'web-tree-sitter';
45
+ import { detectLanguage } from './languages.js';
46
+ let grammarLoader = null;
47
+ let engineInit = null;
48
+ /** Called once by whichever shell is in charge (CLI or browser worker). */
49
+ export function configureParsing(options) {
50
+ grammarLoader = options.loadGrammar;
51
+ engineInit = options.init ?? null;
52
+ engineReady = null;
53
+ parserCache.clear();
54
+ languageCache.clear();
55
+ }
56
+ let engineReady = null;
57
+ /**
58
+ * Boot the WebAssembly runtime. Must happen once before any parsing.
59
+ * We memoise the promise so calling this from ten places still only boots once.
60
+ */
61
+ export async function initEngine() {
62
+ engineReady ??= engineInit ? engineInit() : Parser.init();
63
+ await engineReady;
64
+ }
65
+ /** Grammar name -> loaded Parser. Loading a grammar costs ~10-40ms, so we cache. */
66
+ const parserCache = new Map();
67
+ async function getParser(grammar) {
68
+ const cached = parserCache.get(grammar);
69
+ if (cached)
70
+ return cached;
71
+ await initEngine();
72
+ if (!grammarLoader) {
73
+ throw new Error('No grammar loader configured. Call configureParsing() first - ' +
74
+ 'src/parse/node-grammars.ts for the CLI, ui/worker.js for the browser.');
75
+ }
76
+ const language = await Language.load(await grammarLoader(grammar));
77
+ const parser = new Parser();
78
+ parser.setLanguage(language);
79
+ parserCache.set(grammar, parser);
80
+ return parser;
81
+ }
82
+ /** The tree-sitter Language object, needed to compile queries. */
83
+ const languageCache = new Map();
84
+ export async function getLanguage(grammar) {
85
+ const cached = languageCache.get(grammar);
86
+ if (cached)
87
+ return cached;
88
+ const parser = await getParser(grammar);
89
+ const language = parser.language;
90
+ if (!language)
91
+ throw new Error(`Grammar ${grammar} loaded but exposed no language`);
92
+ languageCache.set(grammar, language);
93
+ return language;
94
+ }
95
+ const MAX_REPORTED_ISSUES = 25;
96
+ /**
97
+ * Walk the whole tree looking for the two ways tree-sitter admits defeat:
98
+ * ERROR - "I found characters here I cannot fit into the grammar"
99
+ * MISSING - "the grammar says something must be here; I inserted a fake one"
100
+ *
101
+ * We use an explicit stack rather than recursion because deeply nested files
102
+ * (minified code, generated code) can blow the JavaScript call stack.
103
+ */
104
+ function collectParseIssues(root) {
105
+ if (!root.hasError)
106
+ return [];
107
+ const issues = [];
108
+ const stack = [root];
109
+ while (stack.length > 0 && issues.length < MAX_REPORTED_ISSUES) {
110
+ const node = stack.pop();
111
+ if (!node)
112
+ break;
113
+ if (node.type === 'ERROR' || node.isMissing) {
114
+ issues.push({
115
+ kind: node.isMissing ? 'MISSING' : 'ERROR',
116
+ line: node.startPosition.row + 1,
117
+ column: node.startPosition.column + 1,
118
+ text: (node.text ?? '').replace(/\s+/g, ' ').trim().slice(0, 60),
119
+ });
120
+ // Don't descend into an error subtree; its children are noise.
121
+ continue;
122
+ }
123
+ // Only descend where an error actually lives - a huge speed win.
124
+ if (node.hasError) {
125
+ for (let i = node.childCount - 1; i >= 0; i--) {
126
+ const child = node.child(i);
127
+ if (child)
128
+ stack.push(child);
129
+ }
130
+ }
131
+ }
132
+ return issues;
133
+ }
134
+ /** Parse a string of code you already have in memory. Used by tests and by `ast`. */
135
+ export async function parseSource(source, grammar) {
136
+ const parser = await getParser(grammar);
137
+ const tree = parser.parse(source);
138
+ if (!tree)
139
+ throw new Error(`Parser returned no tree for grammar ${grammar}`);
140
+ return tree;
141
+ }
142
+ /**
143
+ * Parse WITHOUT awaiting, for a grammar that has already been loaded.
144
+ *
145
+ * WHY THIS EXISTS. A syntax tree lives in WebAssembly memory, which the
146
+ * JavaScript garbage collector cannot see into and will never reclaim; the only
147
+ * way to free one is to call tree.delete() by hand. Holding every tree in a
148
+ * repository at once therefore costs about sixty times the source - measured,
149
+ * not guessed - and that is the ceiling a large scan dies against.
150
+ *
151
+ * The fix is to free trees and parse a file again if something needs it later.
152
+ * "Later" is inside the tracer, which is synchronous all the way down, so the
153
+ * re-parse has to be synchronous too.
154
+ *
155
+ * It is safe to demand that here because loading is what is actually async, and
156
+ * by the time anything re-parses, every grammar in the scan has already been
157
+ * loaded during the first pass. If that assumption ever breaks, this throws
158
+ * rather than silently returning nothing - a missing tree would look exactly
159
+ * like a file with no vulnerabilities in it.
160
+ */
161
+ export function parseSourceSync(source, grammar) {
162
+ const parser = parserCache.get(grammar);
163
+ if (!parser) {
164
+ throw new Error(`Grammar ${grammar} is not loaded, so it cannot be parsed synchronously. ` +
165
+ 'Every grammar must be loaded by the first (async) pass before anything re-parses.');
166
+ }
167
+ const tree = parser.parse(source);
168
+ if (!tree)
169
+ throw new Error(`Parser returned no tree for grammar ${grammar}`);
170
+ return tree;
171
+ }
172
+ /**
173
+ * Work out a file's language and parse it, from text already in memory.
174
+ * No filesystem, no platform assumptions - this is what the browser calls too.
175
+ */
176
+ export async function parseSourceFile(filePath, source) {
177
+ // A binary file that happens to end in .js would waste a parse and produce
178
+ // garbage findings. NUL bytes are the cheap, reliable binary tell.
179
+ if (source.includes('\u0000')) {
180
+ return { status: 'skipped', path: filePath, reason: 'binary file' };
181
+ }
182
+ const firstLine = source.slice(0, source.indexOf('\n') + 1 || 200);
183
+ const match = detectLanguage(filePath, firstLine);
184
+ if (!match) {
185
+ return { status: 'skipped', path: filePath, reason: 'unsupported file type' };
186
+ }
187
+ const tree = await parseSource(source, match.grammar);
188
+ return {
189
+ status: 'parsed',
190
+ file: {
191
+ path: filePath,
192
+ language: match.spec,
193
+ grammar: match.grammar,
194
+ detectedBy: match.detectedBy,
195
+ source,
196
+ tree,
197
+ root: tree.rootNode,
198
+ parseIssues: collectParseIssues(tree.rootNode),
199
+ },
200
+ };
201
+ }
202
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.js","sourceRoot":"","sources":["../../../src/parse/parser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * THE TREE STORE (how a scan stops running out of memory)
3
+ * ========================================================
4
+ *
5
+ * THE PROBLEM, STATED HONESTLY.
6
+ *
7
+ * A tree-sitter syntax tree does not live in JavaScript memory. It lives in a
8
+ * WebAssembly heap that the garbage collector cannot see into, so a tree is
9
+ * never collected, however dead it looks - the only thing that frees one is an
10
+ * explicit call to tree.delete().
11
+ *
12
+ * Until this module existed, nothing in this project called delete(). Every file
13
+ * parsed stayed parsed until the process exited. Measured on Django:
14
+ *
15
+ * 248 files 0.9 MB source 173 MB peak
16
+ * 992 files 6.9 MB source 525 MB peak ~59x per marginal byte
17
+ *
18
+ * which puts the hard ceiling at roughly 44 MB of source on an 8 GB machine.
19
+ * Most serious repositories are bigger than that. The scan does not slow down
20
+ * at the ceiling; it is killed by the operating system, which is a worse
21
+ * failure than a slow answer because it produces no answer at all.
22
+ *
23
+ * THE EXPLANATION THAT WAS IN THE CODE, AND WHY IT WAS WRONG.
24
+ *
25
+ * A comment above the parse loop said the trees had to be held because
26
+ * cross-file tracing must be able to resolve into any file at any time, and
27
+ * that this was why `--no-cross-file` existed. Plausible, and false. Measured:
28
+ *
29
+ * cross-file ON 528.8 MB
30
+ * cross-file OFF 535.8 MB
31
+ *
32
+ * Turning the feature off freed nothing, because the trees were never freed in
33
+ * either mode. The stated cause was a rationalisation of an accident. It is
34
+ * recorded here because a wrong explanation is more expensive than no
35
+ * explanation: it stops anybody looking.
36
+ *
37
+ * WHAT THIS MODULE DOES.
38
+ *
39
+ * It owns every tree in the scan and holds as many as a stated budget allows,
40
+ * evicting the least recently used one when the budget is exceeded. A file
41
+ * whose tree was evicted is simply parsed again when something needs it. So:
42
+ *
43
+ * - a project that fits in the budget behaves exactly as before, parses each
44
+ * file exactly once, and runs at exactly the old speed;
45
+ * - a project that does not fit gets slower instead of getting killed.
46
+ *
47
+ * Time is a renewable resource and a dead process is not.
48
+ *
49
+ * PINNING - THE PART THAT WOULD BE A CRASH IF IT WERE WRONG.
50
+ *
51
+ * Evicting a tree frees the WebAssembly memory that every Node object from it
52
+ * points into. A Node whose tree has been deleted is not an error; it is a
53
+ * dangling pointer into a heap that now holds something else, and reading it
54
+ * yields silent nonsense. So a tree is pinned for as long as anything might
55
+ * still hold a node from it, and eviction skips pinned entries entirely, even
56
+ * when that means going over budget. Going over budget is visible in the stats
57
+ * and is the right answer: correctness first, then memory, then speed.
58
+ */
59
+ import type { Node, Tree } from 'web-tree-sitter';
60
+ /**
61
+ * How many bytes of WebAssembly heap one byte of source costs once parsed.
62
+ *
63
+ * MEASURED, not guessed: 6.9 MB of Django source produced a 525 MB peak against
64
+ * a ~120 MB floor (Node plus seven loaded grammars), which is about 59x. Sixty
65
+ * is used because the number varies by language and an underestimate is the
66
+ * dangerous direction - it would let the store believe it is inside a budget it
67
+ * has already blown.
68
+ *
69
+ * The estimate only decides WHEN to evict. Nothing is ever wrong because this
70
+ * number is off; the scan just holds slightly more or less than intended.
71
+ */
72
+ export declare const TREE_BYTES_PER_SOURCE_BYTE = 60;
73
+ /**
74
+ * Default budget: roughly a gigabyte of trees, which is about 17 MB of source
75
+ * held at once. Chosen so that every project this tool has been run against so
76
+ * far fits entirely and therefore parses each file exactly once - the redesign
77
+ * should cost nothing on the sizes people actually scan, and only start trading
78
+ * time for memory past the point where the old code died.
79
+ */
80
+ export declare const DEFAULT_TREE_BUDGET_BYTES: number;
81
+ export interface TreeStoreStats {
82
+ readonly budgetBytes: number;
83
+ /** Files whose tree is resident right now. */
84
+ readonly filesResident: number;
85
+ /** The most tree memory this scan ever estimated it was holding. */
86
+ readonly peakEstimatedBytes: number;
87
+ /** Parses performed in total, including the first one of each file. */
88
+ readonly parses: number;
89
+ /** Parses that happened only because an earlier tree had been evicted. */
90
+ readonly reparses: number;
91
+ readonly evictions: number;
92
+ /**
93
+ * True when the budget was exceeded and something had to be evicted - i.e.
94
+ * the project did not fit. Reported, because it is the difference between
95
+ * "this scan was slow" and "this scan was slow FOR A REASON".
96
+ */
97
+ readonly spilled: boolean;
98
+ /**
99
+ * Times the budget was blown because every resident tree was pinned. Not an
100
+ * error, but the one way this design can still use unbounded memory, so it is
101
+ * counted rather than assumed away.
102
+ */
103
+ readonly overBudgetWhileAllPinned: number;
104
+ }
105
+ export interface TreeStore {
106
+ /** Declare a file and hand over the tree the first pass already produced. */
107
+ admit(path: string, source: string, grammar: string, tree: Tree): void;
108
+ /** The tree for a known file, parsing it again if it was evicted. */
109
+ get(path: string): Tree;
110
+ /** Hold this file's tree until releaseAll(). Nested pins are counted. */
111
+ pin(path: string): void;
112
+ /** Drop every pin. Call only where no Node from any file is still held. */
113
+ releaseAll(): void;
114
+ /** Free everything. The store is unusable afterwards. */
115
+ disposeAll(): void;
116
+ stats(): TreeStoreStats;
117
+ }
118
+ /**
119
+ * Find the node at an exact byte range in a freshly parsed tree.
120
+ *
121
+ * This is the hinge of the whole design. The index records a function body as
122
+ * two byte offsets instead of a Node, because a Node keeps its tree alive and
123
+ * the point is to let trees go. When the tracer finally wants that body, the
124
+ * file is parsed again and the node is looked up here.
125
+ *
126
+ * Two things make it safe. The parse is deterministic - same bytes, same
127
+ * grammar, same tree - so the node is certainly there. And the result is
128
+ * CHECKED: the range and the node type must both match what was recorded, or
129
+ * this returns null and the caller declines to resolve. Tracing into a node we
130
+ * merely believe is the right one would manufacture exactly the kind of
131
+ * confident, wrong proof this project exists to avoid.
132
+ *
133
+ * The walk upward is needed because descendantForIndex returns the SMALLEST
134
+ * node covering a range, and a node can share its exact extent with its parent
135
+ * (a block that is its own statement, an expression that is its own body). The
136
+ * recorded type disambiguates them.
137
+ */
138
+ export declare function nodeAtRange(root: Node, startIndex: number, endIndex: number, type: string): Node | null;
139
+ export declare function createTreeStore(budgetBytes?: number): TreeStore;