@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.
- package/LICENSE +235 -0
- package/README.md +561 -2
- package/dist/src/cli.d.ts +20 -0
- package/dist/src/cli.js +619 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/core/analyze.d.ts +178 -0
- package/dist/src/core/analyze.js +834 -0
- package/dist/src/core/analyze.js.map +1 -0
- package/dist/src/core/coverage.d.ts +87 -0
- package/dist/src/core/coverage.js +181 -0
- package/dist/src/core/coverage.js.map +1 -0
- package/dist/src/core/finding.d.ts +196 -0
- package/dist/src/core/finding.js +178 -0
- package/dist/src/core/finding.js.map +1 -0
- package/dist/src/core/licence.d.ts +50 -0
- package/dist/src/core/licence.js +56 -0
- package/dist/src/core/licence.js.map +1 -0
- package/dist/src/core/paths.d.ts +21 -0
- package/dist/src/core/paths.js +58 -0
- package/dist/src/core/paths.js.map +1 -0
- package/dist/src/core/score.d.ts +137 -0
- package/dist/src/core/score.js +232 -0
- package/dist/src/core/score.js.map +1 -0
- package/dist/src/dast/targets.d.ts +30 -0
- package/dist/src/dast/targets.js +79 -0
- package/dist/src/dast/targets.js.map +1 -0
- package/dist/src/dast/zap.d.ts +42 -0
- package/dist/src/dast/zap.js +184 -0
- package/dist/src/dast/zap.js.map +1 -0
- package/dist/src/engine/scan.d.ts +80 -0
- package/dist/src/engine/scan.js +129 -0
- package/dist/src/engine/scan.js.map +1 -0
- package/dist/src/engine/shapes.d.ts +47 -0
- package/dist/src/engine/shapes.js +328 -0
- package/dist/src/engine/shapes.js.map +1 -0
- package/dist/src/engine/walk.d.ts +60 -0
- package/dist/src/engine/walk.js +156 -0
- package/dist/src/engine/walk.js.map +1 -0
- package/dist/src/parse/astDump.d.ts +31 -0
- package/dist/src/parse/astDump.js +66 -0
- package/dist/src/parse/astDump.js.map +1 -0
- package/dist/src/parse/languages.d.ts +70 -0
- package/dist/src/parse/languages.js +174 -0
- package/dist/src/parse/languages.js.map +1 -0
- package/dist/src/parse/location.d.ts +17 -0
- package/dist/src/parse/location.js +38 -0
- package/dist/src/parse/location.js.map +1 -0
- package/dist/src/parse/node-grammars.d.ts +12 -0
- package/dist/src/parse/node-grammars.js +35 -0
- package/dist/src/parse/node-grammars.js.map +1 -0
- package/dist/src/parse/parser.d.ts +126 -0
- package/dist/src/parse/parser.js +202 -0
- package/dist/src/parse/parser.js.map +1 -0
- package/dist/src/parse/tree-store.d.ts +139 -0
- package/dist/src/parse/tree-store.js +236 -0
- package/dist/src/parse/tree-store.js.map +1 -0
- package/dist/src/platform/dependency-rule.d.ts +27 -0
- package/dist/src/platform/dependency-rule.js +21 -0
- package/dist/src/platform/dependency-rule.js.map +1 -0
- package/dist/src/platform/deps.d.ts +86 -0
- package/dist/src/platform/deps.js +304 -0
- package/dist/src/platform/deps.js.map +1 -0
- package/dist/src/platform/php-facts.d.ts +49 -0
- package/dist/src/platform/php-facts.js +67 -0
- package/dist/src/platform/php-facts.js.map +1 -0
- package/dist/src/platform/runtime-version.d.ts +78 -0
- package/dist/src/platform/runtime-version.js +573 -0
- package/dist/src/platform/runtime-version.js.map +1 -0
- package/dist/src/report/colors.d.ts +50 -0
- package/dist/src/report/colors.js +97 -0
- package/dist/src/report/colors.js.map +1 -0
- package/dist/src/report/coverage-table.d.ts +90 -0
- package/dist/src/report/coverage-table.js +291 -0
- package/dist/src/report/coverage-table.js.map +1 -0
- package/dist/src/report/html-report.d.ts +66 -0
- package/dist/src/report/html-report.js +489 -0
- package/dist/src/report/html-report.js.map +1 -0
- package/dist/src/report/human.d.ts +44 -0
- package/dist/src/report/human.js +575 -0
- package/dist/src/report/human.js.map +1 -0
- package/dist/src/report/json.d.ts +24 -0
- package/dist/src/report/json.js +178 -0
- package/dist/src/report/json.js.map +1 -0
- package/dist/src/report/mark.d.ts +17 -0
- package/dist/src/report/mark.js +39 -0
- package/dist/src/report/mark.js.map +1 -0
- package/dist/src/report/pricing.d.ts +33 -0
- package/dist/src/report/pricing.js +343 -0
- package/dist/src/report/pricing.js.map +1 -0
- package/dist/src/report/sarif.d.ts +39 -0
- package/dist/src/report/sarif.js +219 -0
- package/dist/src/report/sarif.js.map +1 -0
- package/dist/src/report/site.d.ts +87 -0
- package/dist/src/report/site.js +742 -0
- package/dist/src/report/site.js.map +1 -0
- package/dist/src/rules/code-injection.d.ts +47 -0
- package/dist/src/rules/code-injection.js +326 -0
- package/dist/src/rules/code-injection.js.map +1 -0
- package/dist/src/rules/command-injection.d.ts +30 -0
- package/dist/src/rules/command-injection.js +286 -0
- package/dist/src/rules/command-injection.js.map +1 -0
- package/dist/src/rules/contract.d.ts +198 -0
- package/dist/src/rules/contract.js +39 -0
- package/dist/src/rules/contract.js.map +1 -0
- package/dist/src/rules/format-string.d.ts +46 -0
- package/dist/src/rules/format-string.js +146 -0
- package/dist/src/rules/format-string.js.map +1 -0
- package/dist/src/rules/hardcoded-secret.d.ts +38 -0
- package/dist/src/rules/hardcoded-secret.js +440 -0
- package/dist/src/rules/hardcoded-secret.js.map +1 -0
- package/dist/src/rules/lib/guards.d.ts +168 -0
- package/dist/src/rules/lib/guards.js +1179 -0
- package/dist/src/rules/lib/guards.js.map +1 -0
- package/dist/src/rules/lib/html-safety.d.ts +75 -0
- package/dist/src/rules/lib/html-safety.js +755 -0
- package/dist/src/rules/lib/html-safety.js.map +1 -0
- package/dist/src/rules/lib/local-build.d.ts +59 -0
- package/dist/src/rules/lib/local-build.js +320 -0
- package/dist/src/rules/lib/local-build.js.map +1 -0
- package/dist/src/rules/lib/strings.d.ts +208 -0
- package/dist/src/rules/lib/strings.js +1970 -0
- package/dist/src/rules/lib/strings.js.map +1 -0
- package/dist/src/rules/path-traversal.d.ts +33 -0
- package/dist/src/rules/path-traversal.js +96 -0
- package/dist/src/rules/path-traversal.js.map +1 -0
- package/dist/src/rules/registry.d.ts +19 -0
- package/dist/src/rules/registry.js +77 -0
- package/dist/src/rules/registry.js.map +1 -0
- package/dist/src/rules/sql-injection.d.ts +36 -0
- package/dist/src/rules/sql-injection.js +299 -0
- package/dist/src/rules/sql-injection.js.map +1 -0
- package/dist/src/rules/ssrf.d.ts +39 -0
- package/dist/src/rules/ssrf.js +111 -0
- package/dist/src/rules/ssrf.js.map +1 -0
- package/dist/src/rules/unbounded-copy.d.ts +37 -0
- package/dist/src/rules/unbounded-copy.js +147 -0
- package/dist/src/rules/unbounded-copy.js.map +1 -0
- package/dist/src/rules/unsafe-deserialization.d.ts +43 -0
- package/dist/src/rules/unsafe-deserialization.js +276 -0
- package/dist/src/rules/unsafe-deserialization.js.map +1 -0
- package/dist/src/rules/weak-hash.d.ts +37 -0
- package/dist/src/rules/weak-hash.js +197 -0
- package/dist/src/rules/weak-hash.js.map +1 -0
- package/dist/src/rules/xss.d.ts +38 -0
- package/dist/src/rules/xss.js +527 -0
- package/dist/src/rules/xss.js.map +1 -0
- package/dist/src/taint/content-type.d.ts +19 -0
- package/dist/src/taint/content-type.js +20 -0
- package/dist/src/taint/content-type.js.map +1 -0
- package/dist/src/taint/dictionaries.d.ts +23 -0
- package/dist/src/taint/dictionaries.js +1778 -0
- package/dist/src/taint/dictionaries.js.map +1 -0
- package/dist/src/taint/fold.d.ts +59 -0
- package/dist/src/taint/fold.js +1177 -0
- package/dist/src/taint/fold.js.map +1 -0
- package/dist/src/taint/java-scope.d.ts +61 -0
- package/dist/src/taint/java-scope.js +212 -0
- package/dist/src/taint/java-scope.js.map +1 -0
- package/dist/src/taint/local-map.d.ts +79 -0
- package/dist/src/taint/local-map.js +603 -0
- package/dist/src/taint/local-map.js.map +1 -0
- package/dist/src/taint/project.d.ts +172 -0
- package/dist/src/taint/project.js +617 -0
- package/dist/src/taint/project.js.map +1 -0
- package/dist/src/taint/tracer.d.ts +265 -0
- package/dist/src/taint/tracer.js +3817 -0
- package/dist/src/taint/tracer.js.map +1 -0
- package/dist/src/taint/types.d.ts +484 -0
- package/dist/src/taint/types.js +47 -0
- package/dist/src/taint/types.js.map +1 -0
- package/dist/src/taint/visibility.d.ts +25 -0
- package/dist/src/taint/visibility.js +96 -0
- package/dist/src/taint/visibility.js.map +1 -0
- package/docs/examples/defuse.json +20 -0
- package/docs/examples/dvwa-urls.json +14 -0
- package/docs/examples/github-code-scanning.yml +54 -0
- package/docs/examples/tiny.js +4 -0
- package/package.json +73 -4
- package/src/cli.ts +747 -0
- package/src/core/analyze.ts +1072 -0
- package/src/core/coverage.ts +269 -0
- package/src/core/finding.ts +311 -0
- package/src/core/licence.ts +61 -0
- package/src/core/paths.ts +59 -0
- package/src/core/score.ts +353 -0
- package/src/dast/targets.ts +121 -0
- package/src/dast/zap.ts +246 -0
- package/src/engine/scan.ts +200 -0
- package/src/engine/shapes.ts +348 -0
- package/src/engine/walk.ts +196 -0
- package/src/parse/astDump.ts +85 -0
- package/src/parse/languages.ts +232 -0
- package/src/parse/location.ts +44 -0
- package/src/parse/node-grammars.ts +41 -0
- package/src/parse/parser.ts +262 -0
- package/src/parse/tree-store.ts +301 -0
- package/src/platform/dependency-rule.ts +33 -0
- package/src/platform/deps.ts +374 -0
- package/src/platform/php-facts.ts +85 -0
- package/src/platform/runtime-version.ts +710 -0
- package/src/report/colors.ts +107 -0
- package/src/report/coverage-table.ts +359 -0
- package/src/report/html-report.ts +601 -0
- package/src/report/human.ts +811 -0
- package/src/report/json.ts +189 -0
- package/src/report/mark.ts +47 -0
- package/src/report/pricing.ts +374 -0
- package/src/report/sarif.ts +235 -0
- package/src/report/site.ts +760 -0
- package/src/rules/code-injection.ts +346 -0
- package/src/rules/command-injection.ts +296 -0
- package/src/rules/contract.ts +220 -0
- package/src/rules/format-string.ts +158 -0
- package/src/rules/hardcoded-secret.ts +485 -0
- package/src/rules/lib/guards.ts +1235 -0
- package/src/rules/lib/html-safety.ts +793 -0
- package/src/rules/lib/local-build.ts +329 -0
- package/src/rules/lib/strings.ts +2034 -0
- package/src/rules/path-traversal.ts +101 -0
- package/src/rules/registry.ts +90 -0
- package/src/rules/sql-injection.ts +311 -0
- package/src/rules/ssrf.ts +118 -0
- package/src/rules/unbounded-copy.ts +162 -0
- package/src/rules/unsafe-deserialization.ts +291 -0
- package/src/rules/weak-hash.ts +226 -0
- package/src/rules/xss.ts +572 -0
- package/src/taint/content-type.ts +20 -0
- package/src/taint/dictionaries.ts +1855 -0
- package/src/taint/fold.ts +1206 -0
- package/src/taint/java-scope.ts +200 -0
- package/src/taint/local-map.ts +594 -0
- package/src/taint/project.ts +774 -0
- package/src/taint/tracer.ts +4298 -0
- package/src/taint/types.ts +511 -0
- package/src/taint/visibility.ts +100 -0
- 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;
|