@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,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TAINT ANALYSIS - THE VOCABULARY
|
|
3
|
+
* ===============================
|
|
4
|
+
*
|
|
5
|
+
* "Taint" is the oldest metaphor in program analysis and still the clearest:
|
|
6
|
+
* data that came from outside your program is DIRTY. Dirt spreads when you copy
|
|
7
|
+
* or combine the value. Washing it is possible, but only with the right soap for
|
|
8
|
+
* the right job. A vulnerability is dirt arriving somewhere dangerous unwashed.
|
|
9
|
+
*
|
|
10
|
+
* Four words, and the whole engine is built out of them:
|
|
11
|
+
*
|
|
12
|
+
* SOURCE where dirt enters. `req.query.id`, `request.form['x']`,
|
|
13
|
+
* `sys.argv`. Someone outside your program chose this value.
|
|
14
|
+
*
|
|
15
|
+
* PROPAGATION dirt spreading. `const b = a;` `"x" + a` `a.trim()` `f"{a}"`.
|
|
16
|
+
* A copy of a dirty value is dirty. So is a string containing it.
|
|
17
|
+
*
|
|
18
|
+
* SANITISER washing. `html.escape(a)` for HTML, `shlex.quote(a)` for shells,
|
|
19
|
+
* `parseInt(a)` for anything that should be a number.
|
|
20
|
+
*
|
|
21
|
+
* SINK where dirt does damage. `db.query(...)`, `exec(...)`,
|
|
22
|
+
* `el.innerHTML = ...`. These read strings as instructions.
|
|
23
|
+
*
|
|
24
|
+
* THE PART MOST TOOLS GET WRONG - soap is job-specific.
|
|
25
|
+
*
|
|
26
|
+
* `escapeHtml(x)` makes a value safe to put in a web page. It does absolutely
|
|
27
|
+
* nothing to make it safe inside a SQL query - `<` is not valid SQL escaping,
|
|
28
|
+
* and the dangerous characters for SQL (quotes) are not the dangerous characters
|
|
29
|
+
* for HTML (angle brackets). A scanner that keeps one boolean "is it clean?"
|
|
30
|
+
* will silently accept HTML-escaping in front of a database call.
|
|
31
|
+
*
|
|
32
|
+
* So taint here carries a SET of kinds it has been cleaned for, and a sink only
|
|
33
|
+
* accepts soap matching its own kind. That's the `sanitizedFor` field below, and
|
|
34
|
+
* it is a small design decision with a large correctness payoff.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import type { FlowStep } from '../core/finding.js';
|
|
38
|
+
|
|
39
|
+
/** The categories of damage a dirty value can do. */
|
|
40
|
+
export type SinkKind =
|
|
41
|
+
| 'sql'
|
|
42
|
+
| 'command'
|
|
43
|
+
| 'xss'
|
|
44
|
+
| 'deserialization'
|
|
45
|
+
| 'code'
|
|
46
|
+
| 'ssrf'
|
|
47
|
+
/**
|
|
48
|
+
* A tainted FILE PATH (CWE-22). `../` in the value walks out of the folder
|
|
49
|
+
* the code meant, so a read returns any file the server can see and a write
|
|
50
|
+
* or delete reaches any file it can touch. Separate from `ssrf` because PHP's
|
|
51
|
+
* file_get_contents is both: a URL there is a request, a path a file read.
|
|
52
|
+
*/
|
|
53
|
+
| 'path'
|
|
54
|
+
/**
|
|
55
|
+
* A tainted FORMAT STRING (CWE-134). C-family only, and the cleanest sink in
|
|
56
|
+
* this union: `printf(name)` reads the attacker's string as a format, so
|
|
57
|
+
* `%n` turns a logging call into an arbitrary memory write. It is separate
|
|
58
|
+
* from `command` because the fix is different (make the format constant) and
|
|
59
|
+
* separate from `xss` because nothing is being rendered.
|
|
60
|
+
*/
|
|
61
|
+
| 'format'
|
|
62
|
+
/**
|
|
63
|
+
* A tainted value reaching a copy with NO LENGTH BOUND (CWE-120).
|
|
64
|
+
*
|
|
65
|
+
* Read the name carefully: it is not `buffer-overflow`. Whether a copy
|
|
66
|
+
* actually overflows depends on the destination's size, which this engine
|
|
67
|
+
* does not model - it tracks values, not sizes. What is provable from data
|
|
68
|
+
* flow alone is that attacker-controlled data reached a function that writes
|
|
69
|
+
* until it meets a NUL byte, with the attacker choosing where that is. The
|
|
70
|
+
* finding says exactly that and no more.
|
|
71
|
+
*/
|
|
72
|
+
| 'overflow';
|
|
73
|
+
|
|
74
|
+
export const SINK_KIND_RULE: Record<SinkKind, string> = {
|
|
75
|
+
sql: 'sql-injection',
|
|
76
|
+
command: 'command-injection',
|
|
77
|
+
xss: 'xss',
|
|
78
|
+
deserialization: 'unsafe-deserialization',
|
|
79
|
+
code: 'code-injection',
|
|
80
|
+
ssrf: 'ssrf',
|
|
81
|
+
path: 'path-traversal',
|
|
82
|
+
format: 'format-string',
|
|
83
|
+
overflow: 'unbounded-copy',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* What we know about one value.
|
|
88
|
+
*
|
|
89
|
+
* There is no `tainted: false` state - an untainted value is simply `null`.
|
|
90
|
+
* Modelling "clean" as the absence of a record keeps the engine honest: we only
|
|
91
|
+
* ever claim dirt we can trace, never cleanliness we assumed.
|
|
92
|
+
*/
|
|
93
|
+
export interface Taint {
|
|
94
|
+
/** Every hop so far, starting with the source. Becomes the finding's flowPath. */
|
|
95
|
+
readonly steps: readonly FlowStep[];
|
|
96
|
+
/** Kinds of sink this value has been properly washed for. */
|
|
97
|
+
readonly sanitizedFor: ReadonlySet<SinkKind>;
|
|
98
|
+
/** Short label for the origin, e.g. "req.query.id". Used in messages. */
|
|
99
|
+
readonly origin: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A pattern that means "an attacker chose this value". */
|
|
103
|
+
export interface SourceSpec {
|
|
104
|
+
/** Matched against the source text of an expression. */
|
|
105
|
+
readonly pattern: RegExp;
|
|
106
|
+
/** Plain-language description for the flow path, e.g. "HTTP query string". */
|
|
107
|
+
readonly description: string;
|
|
108
|
+
/**
|
|
109
|
+
* The WHOLE expression, anchored, that `pattern` also matches but that the
|
|
110
|
+
* client does not write - Django's `request.META["CSRF_COOKIE"]`, set by its
|
|
111
|
+
* own middleware after checking the cookie is letters and digits. Such an
|
|
112
|
+
* expression is not a source, and the container it is read from does not
|
|
113
|
+
* make it dirty either. Keep these exact: anything not listed stays a source.
|
|
114
|
+
*/
|
|
115
|
+
readonly except?: RegExp;
|
|
116
|
+
/**
|
|
117
|
+
* A source that exists only in a web browser - the address bar, browser
|
|
118
|
+
* storage, document.cookie. Code that reads one runs in the user's own
|
|
119
|
+
* browser, so a request it makes is the browser's, not a server's, and
|
|
120
|
+
* cannot be server-side request forgery. See `fixedDestination` in tracer.ts.
|
|
121
|
+
*/
|
|
122
|
+
readonly browserOnly?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* A source the person RUNNING the program writes - its command line, its
|
|
125
|
+
* environment, the terminal. Opening the file `argv[1]` names is what a
|
|
126
|
+
* command-line tool is for, so a path built from one is not proven path
|
|
127
|
+
* traversal: whoever chose it can already read that file. See `localSource`
|
|
128
|
+
* in tracer.ts. Every other kind is unaffected.
|
|
129
|
+
*/
|
|
130
|
+
readonly localOnly?: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* A file name the platform has already cut down to its last part. PHP keeps
|
|
133
|
+
* only what follows the last `/` or `\` in an upload's name, so
|
|
134
|
+
* `$_FILES['f']['name']` cannot climb a folder - storing the upload under it
|
|
135
|
+
* is an unrestricted-upload question, not path traversal. Every other kind
|
|
136
|
+
* (an echoed `<script>` file name, say) is unaffected.
|
|
137
|
+
*/
|
|
138
|
+
readonly fileNameOnly?: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A call that does something dangerous with a string. */
|
|
142
|
+
export interface CallSinkSpec {
|
|
143
|
+
readonly kind: SinkKind;
|
|
144
|
+
/** Method or function names, last segment only. */
|
|
145
|
+
readonly methods: readonly string[];
|
|
146
|
+
/**
|
|
147
|
+
* WHICH arguments are dangerous. This matters enormously: in
|
|
148
|
+
* `db.query("SELECT ... ?", [userId])` argument 0 is the instruction and
|
|
149
|
+
* argument 1 is data the driver keeps separate. Treating every argument as a
|
|
150
|
+
* sink would report the CORRECT, parameterised form as a vulnerability - the
|
|
151
|
+
* single worst false positive a SQL rule can produce, because it punishes the
|
|
152
|
+
* fix.
|
|
153
|
+
*/
|
|
154
|
+
readonly argIndexes: readonly number[] | 'all';
|
|
155
|
+
/**
|
|
156
|
+
* Only a sink when the call sits on one of these receivers.
|
|
157
|
+
*
|
|
158
|
+
* `loads` and `load` are dangerous on `pickle` and meaningless on everything
|
|
159
|
+
* else, and every Python codebase has a settings loader. Matching the method
|
|
160
|
+
* name alone reported `json.loads(request.data)` - the SAFE parser - as a
|
|
161
|
+
* deserialization sink, which is the same class of mistake as flagging a
|
|
162
|
+
* parameterised query.
|
|
163
|
+
*/
|
|
164
|
+
readonly requiredReceivers?: readonly string[];
|
|
165
|
+
/**
|
|
166
|
+
* Only a sink when the call has NO receiver at all.
|
|
167
|
+
*
|
|
168
|
+
* The mirror of requiredReceivers, for built-in functions. `compile(src)` is
|
|
169
|
+
* Python's builtin; `re.compile(pattern)` is a regular expression and shares
|
|
170
|
+
* only the name. See BARE_BUILTINS in rules/code-injection.ts.
|
|
171
|
+
*/
|
|
172
|
+
readonly bareOnly?: boolean;
|
|
173
|
+
/** Receivers that keep a bareOnly sink alive, because they ARE the evaluator. */
|
|
174
|
+
readonly allowedReceivers?: readonly string[];
|
|
175
|
+
/**
|
|
176
|
+
* Only a sink when the receiver EXPRESSION matches this.
|
|
177
|
+
*
|
|
178
|
+
* The looser sibling of requiredReceivers, for receivers that are themselves
|
|
179
|
+
* calls: `response.getWriter().write(x)` has no single name to compare.
|
|
180
|
+
*
|
|
181
|
+
* This is what replaced `contentCheck: 'html'` on the Java response sinks.
|
|
182
|
+
* The content check was there to stop `sb.append(x)` reading as XSS, but it
|
|
183
|
+
* demanded that the ARGUMENT look like HTML - and the argument is usually an
|
|
184
|
+
* opaque variable, so it rejected 204 real cases to avoid one false one.
|
|
185
|
+
* Asking about the RECEIVER instead separates them exactly: a servlet writer
|
|
186
|
+
* writes to the page, a StringBuilder does not, whatever is passed to either.
|
|
187
|
+
*/
|
|
188
|
+
readonly receiverPattern?: RegExp;
|
|
189
|
+
/**
|
|
190
|
+
* For sinks whose DESTINATION is argument zero rather than the receiver.
|
|
191
|
+
*
|
|
192
|
+
* `fmt.Fprintf(w, format, args...)` writes to `w`. The receiver is `fmt`,
|
|
193
|
+
* which says nothing at all, so `receiverPattern` cannot reach this shape -
|
|
194
|
+
* and it is the one shape in the sink table that stayed wrong through a whole
|
|
195
|
+
* scoping sweep, still reporting `fmt.Fprintf(out, ...)` in a test mock as
|
|
196
|
+
* cross-site scripting.
|
|
197
|
+
*
|
|
198
|
+
* When set, argument zero's text (or its declared type) must match, AND
|
|
199
|
+
* argument zero is excluded from the tainted-value check - it is where the
|
|
200
|
+
* output goes, not what is written.
|
|
201
|
+
*/
|
|
202
|
+
readonly writerArgPattern?: RegExp;
|
|
203
|
+
/** Only a sink when the call text also shows a shell being used. */
|
|
204
|
+
readonly requiresShellOption?: RegExp;
|
|
205
|
+
/**
|
|
206
|
+
* NOT a sink when the call text shows the dangerous behaviour turned off.
|
|
207
|
+
*
|
|
208
|
+
* The mirror image of requiresShellOption, and it exists because the safe
|
|
209
|
+
* form of a call is often the dangerous call plus an argument:
|
|
210
|
+
*
|
|
211
|
+
* unserialize($data, ['allowed_classes' => false]) // PHP
|
|
212
|
+
* yaml.load(text, Loader=yaml.SafeLoader) // Python
|
|
213
|
+
* HttpResponse(body, content_type="text/plain") // Django: not a page
|
|
214
|
+
*
|
|
215
|
+
* Both are still a call to the risky function. Reporting them would punish
|
|
216
|
+
* the fix, which is the worst thing an injection rule can do - it teaches
|
|
217
|
+
* people that hardening does not help.
|
|
218
|
+
*
|
|
219
|
+
* This reads the VISIBLE TEXT of the call, so an options object assembled in
|
|
220
|
+
* another variable is missed and the finding still fires. That errs toward
|
|
221
|
+
* reporting, which is the safe direction, and it is stated in the rule's
|
|
222
|
+
* limitations rather than left for someone to discover.
|
|
223
|
+
*/
|
|
224
|
+
readonly unlessOption?: RegExp;
|
|
225
|
+
/**
|
|
226
|
+
* Only a sink when the argument's literal text actually looks like this kind
|
|
227
|
+
* of content. Needed because method names collide across libraries: `exec` is
|
|
228
|
+
* both a database call and a shell call, and `append` is both a jQuery HTML
|
|
229
|
+
* insertion and an ordinary array method. Without this, `exec("ping " + host)`
|
|
230
|
+
* gets reported as SQL injection - which it is not.
|
|
231
|
+
*/
|
|
232
|
+
readonly contentCheck?: 'sql' | 'html';
|
|
233
|
+
readonly description: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** An assignment target that does something dangerous with a string. */
|
|
237
|
+
export interface AssignSinkSpec {
|
|
238
|
+
readonly kind: SinkKind;
|
|
239
|
+
/** Property names, last segment only: innerHTML, __html, srcdoc. */
|
|
240
|
+
readonly properties: readonly string[];
|
|
241
|
+
/**
|
|
242
|
+
* Require the assigned text to LOOK like the thing this sink parses.
|
|
243
|
+
*
|
|
244
|
+
* `innerHTML` needs no such check - the property name is the proof, because
|
|
245
|
+
* the browser will parse whatever lands there. A PHP page buffer is the
|
|
246
|
+
* opposite: `$html`, `$output` and `$content` are conventions, and
|
|
247
|
+
* `$sql .= $where` accumulates a query rather than a page. Requiring visible
|
|
248
|
+
* markup is what separates the two without inventing a type system.
|
|
249
|
+
*/
|
|
250
|
+
readonly contentCheck?: 'sql' | 'html';
|
|
251
|
+
readonly description: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** A function that makes a value safe - for SOME kinds of sink, not all. */
|
|
255
|
+
export interface SanitizerSpec {
|
|
256
|
+
readonly names: readonly string[];
|
|
257
|
+
/** Which sinks this soap actually works for. */
|
|
258
|
+
readonly kinds: readonly SinkKind[];
|
|
259
|
+
/**
|
|
260
|
+
* Kinds this soap protects ONLY when its output lands inside a quoted string.
|
|
261
|
+
*
|
|
262
|
+
* `mysqli_real_escape_string()` puts a backslash in front of quote characters
|
|
263
|
+
* and nothing else. Inside `'...'` in a query that is the whole defence - a
|
|
264
|
+
* quote is the only way out. In an unquoted slot (`WHERE id = $id`) there is
|
|
265
|
+
* nothing to break out of, and `1 OR 1=1` passes through it untouched. So the
|
|
266
|
+
* value is only provisionally clean until the tracer sees where it is spliced
|
|
267
|
+
* into the query. See `quotePending` in tracer.ts.
|
|
268
|
+
*/
|
|
269
|
+
readonly onlyInsideQuotes?: readonly SinkKind[];
|
|
270
|
+
/**
|
|
271
|
+
* The entry holds only for calls with at most this many arguments.
|
|
272
|
+
*
|
|
273
|
+
* `md5($x)` returns 32 hex characters, which cannot carry a quote or a tag.
|
|
274
|
+
* `md5($x, true)` returns 16 RAW bytes, which can - `'ffifdyop'` hashes to a
|
|
275
|
+
* string containing `'or'6`, the well-known login bypass. Same name, one
|
|
276
|
+
* extra argument, opposite answer. A call with more arguments than this is
|
|
277
|
+
* not matched by the entry and is treated as a function we do not model.
|
|
278
|
+
*/
|
|
279
|
+
readonly maxArgs?: number;
|
|
280
|
+
/**
|
|
281
|
+
* The entry holds only when the FIRST argument's text matches. Node's
|
|
282
|
+
* `hash.digest('hex')` returns hex digits; `hash.digest()` returns raw bytes,
|
|
283
|
+
* which can hold any character at all. Same name, the argument decides.
|
|
284
|
+
*/
|
|
285
|
+
readonly argumentPattern?: RegExp;
|
|
286
|
+
/**
|
|
287
|
+
* The value being washed is the RECEIVER and the arguments are options:
|
|
288
|
+
* `crypto.createHash(...).update(dirty).digest('hex')`.
|
|
289
|
+
*/
|
|
290
|
+
readonly fromReceiver?: boolean;
|
|
291
|
+
readonly description: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Functions whose RESULT is new data rather than their argument.
|
|
296
|
+
*
|
|
297
|
+
* The default for a function this engine does not know is that it hands its
|
|
298
|
+
* argument back - right for `stripslashes()`, and how the tool finds anything
|
|
299
|
+
* at all in real code. It is wrong for a read: `mysqli_query($conn, $sql)`
|
|
300
|
+
* returns rows stored in the database, not the text of `$sql`, and a proof
|
|
301
|
+
* that runs from the request through the query into the printed row is a
|
|
302
|
+
* false statement about the code. An AI review of DVWA's published proofs
|
|
303
|
+
* marked every one of those paths wrong.
|
|
304
|
+
*
|
|
305
|
+
* The trace stops at these calls. That is NOT a claim the result is safe -
|
|
306
|
+
* stored data can be just as dangerous, it is just not THIS request's input -
|
|
307
|
+
* so nothing is cleared: a sink further on is still reported by its signature
|
|
308
|
+
* rule, unproven, and every stop is listed in diagnostics.tracesStoppedAtNewData.
|
|
309
|
+
*
|
|
310
|
+
* Consulted only for a call this engine cannot read: a function of the same
|
|
311
|
+
* name defined in the project is followed instead.
|
|
312
|
+
*/
|
|
313
|
+
export interface FreshResultSpec {
|
|
314
|
+
readonly names: readonly string[];
|
|
315
|
+
/** What comes back instead of the argument. Printed in the stop record. */
|
|
316
|
+
readonly description: string;
|
|
317
|
+
/**
|
|
318
|
+
* A database read, where an INJECTABLE query hands the attacker the rows.
|
|
319
|
+
*
|
|
320
|
+
* DVWA's brute/source/low.php puts the raw username into the query, then
|
|
321
|
+
* prints the avatar column of the row it returns. The row is not the
|
|
322
|
+
* username - but `' UNION SELECT ...` makes it whatever the attacker likes,
|
|
323
|
+
* and an AI review called that proof real for exactly that reason. The two
|
|
324
|
+
* DVWA levels that escape the username inside quotes return stored rows
|
|
325
|
+
* only, and the same review called those proofs wrong.
|
|
326
|
+
*
|
|
327
|
+
* So for these entries an ARGUMENT that is still dirty for SQL is carried
|
|
328
|
+
* into the result, with a step saying why. The query functions watch the
|
|
329
|
+
* same argument positions as their SQL sink entry; the row readers watch
|
|
330
|
+
* their arguments - a result handle - and never the receiver, because a
|
|
331
|
+
* statement dirtied by bindParam() holds bound parameters, which cannot
|
|
332
|
+
* inject anything.
|
|
333
|
+
*/
|
|
334
|
+
readonly rowsFollowInjection?: boolean;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** A function whose output is still dirty if its input was. */
|
|
338
|
+
export interface PropagatorSpec {
|
|
339
|
+
readonly names: readonly string[];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export interface TaintDictionary {
|
|
343
|
+
readonly sources: readonly SourceSpec[];
|
|
344
|
+
/**
|
|
345
|
+
* Sources that are PARAMETER DECLARATIONS rather than expressions.
|
|
346
|
+
*
|
|
347
|
+
* Every other source in this file is recognised by the text of an expression:
|
|
348
|
+
* `req.query.id` looks like a query parameter wherever it appears. A Spring
|
|
349
|
+
* handler has no such expression - the framework binds the request value to a
|
|
350
|
+
* parameter NAME before the method body starts, and the only evidence in the
|
|
351
|
+
* source is an annotation on the declaration:
|
|
352
|
+
*
|
|
353
|
+
* public String lookup(@RequestParam String account) { ... }
|
|
354
|
+
*
|
|
355
|
+
* `account` is attacker-controlled from its first use and nothing in the body
|
|
356
|
+
* says so. Matching the declaration is the only way to see it.
|
|
357
|
+
*/
|
|
358
|
+
readonly parameterSources?: readonly SourceSpec[];
|
|
359
|
+
readonly callSinks: readonly CallSinkSpec[];
|
|
360
|
+
readonly assignSinks: readonly AssignSinkSpec[];
|
|
361
|
+
readonly sanitizers: readonly SanitizerSpec[];
|
|
362
|
+
readonly propagators: PropagatorSpec;
|
|
363
|
+
/**
|
|
364
|
+
* Methods where a tainted ARGUMENT dirties the RECEIVER.
|
|
365
|
+
*
|
|
366
|
+
* sb.append(param); // the taint lands on `sb`, not on a return
|
|
367
|
+
* argList.add(param); // `add` returns a boolean
|
|
368
|
+
*
|
|
369
|
+
* Propagators are value-in / value-out: they carry taint through a return
|
|
370
|
+
* value that someone assigns. These do not. `append` and `format` were both
|
|
371
|
+
* already in the Java propagator list and still missed every case, because
|
|
372
|
+
* the return is thrown away and the taint has to attach to the object.
|
|
373
|
+
*
|
|
374
|
+
* Measured on OWASP BenchmarkJava, this one shape is about 60% of all false
|
|
375
|
+
* negatives - 49 StringBuilder cases plus 151 that push a request value into
|
|
376
|
+
* a List or Map before the sink. It is the single largest recall gap, and it
|
|
377
|
+
* is language-agnostic.
|
|
378
|
+
*/
|
|
379
|
+
readonly mutators?: readonly string[];
|
|
380
|
+
/**
|
|
381
|
+
* Functions where a tainted argument dirties argument ZERO.
|
|
382
|
+
*
|
|
383
|
+
* `mutators` above binds taint to the receiver, which is where the filled
|
|
384
|
+
* object sits in every language that has receivers. C has none: the
|
|
385
|
+
* destination is the first argument.
|
|
386
|
+
*
|
|
387
|
+
* sprintf(cmd, "ping %s", argv[1]); -> `cmd` is now tainted
|
|
388
|
+
* strcpy(buf, argv[1]); -> `buf` is now tainted
|
|
389
|
+
*
|
|
390
|
+
* Without this, C coverage is close to zero - the build-then-run pattern is
|
|
391
|
+
* how every command injection in the language is actually written, and the
|
|
392
|
+
* receiver rule cannot see it. See checkOutParamMutation in tracer.ts.
|
|
393
|
+
*/
|
|
394
|
+
readonly outParamMutators?: readonly string[];
|
|
395
|
+
/**
|
|
396
|
+
* Functions that ARE a source and deliver their bytes through a parameter.
|
|
397
|
+
*
|
|
398
|
+
* fgets(line, sizeof(line), stdin); -> `line` is now attacker input
|
|
399
|
+
* read(fd, buf, n); -> `buf` is
|
|
400
|
+
* scanf("%s", name); -> `name` is
|
|
401
|
+
*
|
|
402
|
+
* The ordinary source list matches the TEXT of an expression, which works
|
|
403
|
+
* when the value is the call's return - `getenv("X")` reads as a source
|
|
404
|
+
* wherever it appears. These functions return a count or a status; the data
|
|
405
|
+
* goes into an argument, and the call is usually a statement whose result
|
|
406
|
+
* nobody keeps. So there is no expression to match and nothing to bind,
|
|
407
|
+
* unless the destination index is known - and it differs per function, which
|
|
408
|
+
* is why this carries one rather than assuming zero.
|
|
409
|
+
*/
|
|
410
|
+
readonly outParamSources?: readonly {
|
|
411
|
+
readonly names: readonly string[];
|
|
412
|
+
/** Which argument receives the bytes. */
|
|
413
|
+
readonly destination: number;
|
|
414
|
+
readonly description: string;
|
|
415
|
+
}[];
|
|
416
|
+
/**
|
|
417
|
+
* KEYED mutators - a write into a named compartment of an object that is
|
|
418
|
+
* primarily something else.
|
|
419
|
+
*
|
|
420
|
+
* req.setAttribute("loginForm", dirty);
|
|
421
|
+
*
|
|
422
|
+
* These are mutators too, and they appear in `mutators` as well; this list
|
|
423
|
+
* says something extra about a subset of them. The plain kind ARE the value
|
|
424
|
+
* they were handed: after `sb.append(dirty)`, `sb` is the dirty string and
|
|
425
|
+
* every read of `sb` is a read of it. A servlet request is not its attribute
|
|
426
|
+
* map. It also has a method, a URI, a session and a context path, and none
|
|
427
|
+
* of those changed because something was filed in a side compartment.
|
|
428
|
+
*
|
|
429
|
+
* Scanning Jenkins produced a flow-verified finding on `req.getContextPath()`
|
|
430
|
+
* for exactly this reason: an unmodelled call carries its receiver's taint,
|
|
431
|
+
* the receiver had been dirtied by setAttribute, so a container-configured
|
|
432
|
+
* constant was reported as attacker-controlled. Worse, the tracer then
|
|
433
|
+
* attributed the flow to the one argument the code did NOT escape, so the
|
|
434
|
+
* escaper retraction added a version earlier never fired.
|
|
435
|
+
*
|
|
436
|
+
* The rule this enables is narrow: taint written through a keyed mutator
|
|
437
|
+
* comes back out through `keyedReaders`, and not through anything else.
|
|
438
|
+
* Reads out of the store stay deliberately KEY-INSENSITIVE - `setAttribute`
|
|
439
|
+
* under one name and `getAttribute` under another is still treated as
|
|
440
|
+
* tainted, because matching key literals would be a second guess layered on
|
|
441
|
+
* a first. The imprecision is aimed, not removed.
|
|
442
|
+
*/
|
|
443
|
+
readonly keyedMutators?: readonly string[];
|
|
444
|
+
/**
|
|
445
|
+
* ELEMENT mutators - the subset of `mutators` that add one SEPARABLE ITEM to
|
|
446
|
+
* a collection, as opposed to accumulating into a single value.
|
|
447
|
+
*
|
|
448
|
+
* list.add(dirty) one element among several. A later read picks
|
|
449
|
+
* ONE of them, and we do not model which.
|
|
450
|
+
* sb.append(dirty) not separable. Every appended piece is in the
|
|
451
|
+
* result, so any read of `sb` reads the dirty one.
|
|
452
|
+
*
|
|
453
|
+
* This list exists only to decide a CONFIDENCE LABEL, never to drop taint.
|
|
454
|
+
* When a collection took more than one element write, a read of it might be
|
|
455
|
+
* any of them, so the tracer marks the flow a guess and the finding is
|
|
456
|
+
* emitted signature-based. One write means the value read is the value
|
|
457
|
+
* written, and the proof stands.
|
|
458
|
+
*
|
|
459
|
+
* Measured on OWASP BenchmarkJava, flow-verified findings only: paths
|
|
460
|
+
* collecting via `add()` scored 49.1% precision and via `put()` 53.2%, next
|
|
461
|
+
* to 71.5% for paths with no collect step at all. A coin flip cannot call
|
|
462
|
+
* itself a proven trace.
|
|
463
|
+
*
|
|
464
|
+
* THE NAMES ARE NOT PORTABLE, which is the whole reason this is per-language
|
|
465
|
+
* rather than one shared list. Java's `append` is StringBuilder concatenation
|
|
466
|
+
* and must stay OUT; Python's `append` is a list element and must be IN. A
|
|
467
|
+
* copied list would silently downgrade real findings in one language and
|
|
468
|
+
* overclaim in another.
|
|
469
|
+
*/
|
|
470
|
+
readonly elementMutators?: readonly string[];
|
|
471
|
+
/**
|
|
472
|
+
* The methods that legitimately read a keyed store back out. Only consulted
|
|
473
|
+
* for taint that arrived via `keyedMutators`; every other method on such an
|
|
474
|
+
* object is treated as untouched by the write.
|
|
475
|
+
*/
|
|
476
|
+
readonly keyedReaders?: readonly string[];
|
|
477
|
+
/**
|
|
478
|
+
* Sinks fired by a tainted RECEIVER rather than a tainted argument.
|
|
479
|
+
*
|
|
480
|
+
* pb.command(argList); // argList is dirty, so pb is now dirty
|
|
481
|
+
* pb.start(); // <- the dangerous act, and it takes NO arguments
|
|
482
|
+
*
|
|
483
|
+
* Every other sink check in this engine reads the argument list. This shape
|
|
484
|
+
* has an empty one: the payload was loaded into the object on an earlier
|
|
485
|
+
* line, and the call that executes it carries nothing. It is the standard
|
|
486
|
+
* ProcessBuilder idiom and it is all over OWASP BenchmarkJava.
|
|
487
|
+
*/
|
|
488
|
+
readonly receiverSinks?: readonly {
|
|
489
|
+
readonly methods: readonly string[];
|
|
490
|
+
readonly kind: SinkKind;
|
|
491
|
+
readonly description: string;
|
|
492
|
+
}[];
|
|
493
|
+
/**
|
|
494
|
+
* `return "<h1>" + name + "</h1>"` is a sink in this language.
|
|
495
|
+
*
|
|
496
|
+
* In Flask and Django a view that returns a string HAS returned the response
|
|
497
|
+
* body - there is no `res.send()` to match on, so the dangerous call this
|
|
498
|
+
* engine looks for simply is not written anywhere. A tester's fixture caught
|
|
499
|
+
* this: an obvious reflected XSS produced nothing, and - worse - the ESCAPED
|
|
500
|
+
* version of the same code "passed" for the wrong reason, because with no
|
|
501
|
+
* sink recognised there was nothing for `html.escape` to be credited against.
|
|
502
|
+
*
|
|
503
|
+
* Set only where returning a string plausibly means returning a page. Express
|
|
504
|
+
* is deliberately excluded: it answers with `res.send`, which is already a
|
|
505
|
+
* modelled call sink, so enabling this there would invent a second sink for
|
|
506
|
+
* the same thing.
|
|
507
|
+
*/
|
|
508
|
+
readonly htmlReturnIsSink?: boolean;
|
|
509
|
+
/** Reads whose result is new data, not the argument - see FreshResultSpec. */
|
|
510
|
+
readonly freshResults?: readonly FreshResultSpec[];
|
|
511
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHERE A DECLARATION CAN BE SEEN, AND WHAT THE PARSER CANNOT SEE.
|
|
3
|
+
* ===============================================================
|
|
4
|
+
*
|
|
5
|
+
* The constant proof (rules/lib/strings.ts) collects every declaration of a
|
|
6
|
+
* name in the scopes around a read. A collected declaration is only the one the
|
|
7
|
+
* read means if the read is INSIDE where that declaration is visible - and
|
|
8
|
+
* every language here scopes more tightly than "somewhere in this function".
|
|
9
|
+
* The second break-it round (tests/probes/g05, s02, s03, s05, s06) was a
|
|
10
|
+
* literal declared in a block, a `for` header, a class static block or a
|
|
11
|
+
* namespace, standing in for the import or package variable the read really
|
|
12
|
+
* meant.
|
|
13
|
+
*
|
|
14
|
+
* Every answer here may be NARROWER than the language's real rule - then the
|
|
15
|
+
* proof gives up and the finding stays - but never wider.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Node } from 'web-tree-sitter';
|
|
19
|
+
import type { LanguageId } from '../parse/languages.js';
|
|
20
|
+
import { declarationScope } from './java-scope.js';
|
|
21
|
+
|
|
22
|
+
/** From this offset to the end of `region`, the declaration is the one a read of its name means. */
|
|
23
|
+
export interface Visibility {
|
|
24
|
+
readonly region: Node;
|
|
25
|
+
readonly from: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const JS_FUNCTIONS = new Set([
|
|
29
|
+
'function_declaration', 'function_expression', 'arrow_function', 'method_definition',
|
|
30
|
+
'generator_function_declaration', 'generator_function', 'class_static_block', 'program',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export function declarationVisibility(declarator: Node, language: LanguageId): Visibility | null {
|
|
34
|
+
if (language === 'java') return declarationScope(declarator);
|
|
35
|
+
|
|
36
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
37
|
+
const declaration = declarator.parent;
|
|
38
|
+
if (!declaration) return null;
|
|
39
|
+
let holder = declaration.parent;
|
|
40
|
+
while (holder?.type === 'export_statement') holder = holder.parent;
|
|
41
|
+
if (!holder) return null;
|
|
42
|
+
// `let` and `const`: the enclosing block (a `for` header's: the loop),
|
|
43
|
+
// from the declaration on - before it is the temporal dead zone.
|
|
44
|
+
if (declaration.type === 'lexical_declaration') return { region: holder, from: declarator.endIndex };
|
|
45
|
+
// `var` is hoisted to the top of its function.
|
|
46
|
+
if (declaration.type === 'variable_declaration') {
|
|
47
|
+
let fn: Node | null = holder;
|
|
48
|
+
while (fn && !JS_FUNCTIONS.has(fn.type)) fn = fn.parent;
|
|
49
|
+
return fn ? { region: fn, from: fn.startIndex } : null;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (language === 'go') {
|
|
55
|
+
if (declarator.type === 'short_var_declaration') {
|
|
56
|
+
// `x := v` is visible to the end of its block; in an `if`, `for` or
|
|
57
|
+
// `switch` header, to the end of that statement.
|
|
58
|
+
let holder = declarator.parent;
|
|
59
|
+
if (holder?.type === 'for_clause') holder = holder.parent;
|
|
60
|
+
return holder ? { region: holder, from: declarator.endIndex } : null;
|
|
61
|
+
}
|
|
62
|
+
if (declarator.type === 'var_spec') {
|
|
63
|
+
let declaration = declarator.parent;
|
|
64
|
+
if (declaration?.type === 'var_spec_list') declaration = declaration.parent;
|
|
65
|
+
const holder = declaration?.parent;
|
|
66
|
+
if (!holder) return null;
|
|
67
|
+
// A package-level `var` is visible in the whole package, in any order.
|
|
68
|
+
if (holder.type === 'source_file') return { region: holder, from: holder.startIndex };
|
|
69
|
+
return { region: holder, from: declarator.endIndex };
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
* TEXT THE COMPILER READS AND THE PARSER DOES NOT.
|
|
79
|
+
*
|
|
80
|
+
* A lone carriage return ends a `//` comment for javac, for JavaScript
|
|
81
|
+
* engines and for Python's tokenizer; U+2028 and U+2029 end one in
|
|
82
|
+
* JavaScript; `\u000a` is a newline to javac before it parses anything.
|
|
83
|
+
* Tree-sitter reads all of them as more comment - so `// note<CR> x = f();`
|
|
84
|
+
* hides a real write (tests/probes/j15, s10, p31, p32). A file holding any of
|
|
85
|
+
* them is a file the constant proof does not try to read.
|
|
86
|
+
*/
|
|
87
|
+
const hidden = new WeakMap<object, boolean>();
|
|
88
|
+
export function hidesCodeFromParser(n: Node, language: LanguageId): boolean {
|
|
89
|
+
const key = n.tree as object;
|
|
90
|
+
let found = hidden.get(key);
|
|
91
|
+
if (found === undefined) {
|
|
92
|
+
const text = n.tree.rootNode.text ?? '';
|
|
93
|
+
found =
|
|
94
|
+
/\r(?!\n)/.test(text) ||
|
|
95
|
+
((language === 'javascript' || language === 'typescript') && new RegExp("[\\u2028\\u2029]").test(text)) ||
|
|
96
|
+
(language === 'java' && /\\u+[0-9a-fA-F]{4}/.test(text));
|
|
97
|
+
hidden.set(key, found);
|
|
98
|
+
}
|
|
99
|
+
return found;
|
|
100
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// TypeScript compiler settings for Defuse.
|
|
4
|
+
// Every option below is here for a reason; the comments say what that reason is.
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
// "What JavaScript features may the output use?"
|
|
8
|
+
// Node 20+ supports everything in ES2022, so we don't need TypeScript to
|
|
9
|
+
// rewrite modern syntax into older syntax. Less rewriting = output that
|
|
10
|
+
// looks like the code you wrote, which matters when you're learning.
|
|
11
|
+
"target": "ES2022",
|
|
12
|
+
"lib": ["ES2022"],
|
|
13
|
+
|
|
14
|
+
// "How do imports work?" NodeNext tells TypeScript to use Node's real,
|
|
15
|
+
// modern module rules (import/export, file extensions in paths). This is
|
|
16
|
+
// the setting that matches "type": "module" in package.json.
|
|
17
|
+
"module": "NodeNext",
|
|
18
|
+
"moduleResolution": "NodeNext",
|
|
19
|
+
|
|
20
|
+
// Where the .ts files are and where the compiled .js files go.
|
|
21
|
+
// rootDir "." means both src/ and tests/ are compiled, preserving structure:
|
|
22
|
+
// src/cli.ts -> dist/src/cli.js
|
|
23
|
+
// tests/run-tests.ts -> dist/tests/run-tests.js
|
|
24
|
+
"rootDir": ".",
|
|
25
|
+
"outDir": "dist",
|
|
26
|
+
|
|
27
|
+
// STRICT MODE. This is the single most valuable setting for a beginner.
|
|
28
|
+
// It makes TypeScript refuse to compile code where a value might be
|
|
29
|
+
// undefined/null and you forgot to handle it. It turns whole categories of
|
|
30
|
+
// "why is this crashing at 2am" into "why is this red squiggle here".
|
|
31
|
+
"strict": true,
|
|
32
|
+
|
|
33
|
+
// Extra strictness that strict:true does NOT include but we want:
|
|
34
|
+
// - noUncheckedIndexedAccess: reading arr[5] gives you "T | undefined",
|
|
35
|
+
// forcing you to handle the case where index 5 doesn't exist. Essential
|
|
36
|
+
// for a scanner that indexes into AST children constantly.
|
|
37
|
+
// - exactOptionalPropertyTypes: "optional" means absent, not "maybe undefined".
|
|
38
|
+
// - noImplicitOverride / noFallthroughCasesInSwitch: catch classic slips.
|
|
39
|
+
"noUncheckedIndexedAccess": true,
|
|
40
|
+
"exactOptionalPropertyTypes": true,
|
|
41
|
+
"noImplicitOverride": true,
|
|
42
|
+
"noFallthroughCasesInSwitch": true,
|
|
43
|
+
"noUnusedLocals": true,
|
|
44
|
+
"noUnusedParameters": true,
|
|
45
|
+
|
|
46
|
+
// Emit .d.ts type declarations and source maps. Source maps mean that when
|
|
47
|
+
// the compiled JS throws an error, the stack trace points at the .ts line
|
|
48
|
+
// you actually wrote instead of the generated one.
|
|
49
|
+
"declaration": true,
|
|
50
|
+
"sourceMap": true,
|
|
51
|
+
|
|
52
|
+
// Interop + hygiene.
|
|
53
|
+
"esModuleInterop": true,
|
|
54
|
+
"forceConsistentCasingInFileNames": true,
|
|
55
|
+
"skipLibCheck": true,
|
|
56
|
+
"resolveJsonModule": true
|
|
57
|
+
},
|
|
58
|
+
"include": ["src/**/*.ts", "tests/**/*.ts"],
|
|
59
|
+
// Fixtures and probes are code for the scanner to READ, not to compile.
|
|
60
|
+
"exclude": ["node_modules", "dist", "tests/fixtures", "tests/probes"]
|
|
61
|
+
}
|