@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,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHERE WAS THIS VARIABLE BUILT?
|
|
3
|
+
* ==============================
|
|
4
|
+
*
|
|
5
|
+
* A signature rule reads the argument of a dangerous call and asks how its text
|
|
6
|
+
* was assembled. That question has an obvious blind spot, and until this file
|
|
7
|
+
* existed the command-injection rule sat squarely in it:
|
|
8
|
+
*
|
|
9
|
+
* os.system("ls " + name) <- the argument IS the build. Reported.
|
|
10
|
+
*
|
|
11
|
+
* cmd = "ls " + name
|
|
12
|
+
* os.system(cmd) <- the argument is a NAME. Silent.
|
|
13
|
+
*
|
|
14
|
+
* Same code, same risk, one line break apart - in every language. In C it was
|
|
15
|
+
* worse than a blind spot: C has no `+` for strings, so a command is ALWAYS
|
|
16
|
+
* formatted into a buffer first and run afterwards, and the signature pass saw
|
|
17
|
+
* `system(buf)` every single time.
|
|
18
|
+
*
|
|
19
|
+
* WHAT THIS DOES, AND ON PURPOSE WHAT IT DOES NOT.
|
|
20
|
+
*
|
|
21
|
+
* Given a bare name at a call site, it looks back through the SAME function for
|
|
22
|
+
* the writes that reach the call, in straight-line code:
|
|
23
|
+
*
|
|
24
|
+
* a RESET replaces the value cmd = ... char cmd[] = ... sprintf(cmd, ...)
|
|
25
|
+
* an EXTEND appends to it cmd += ... strcat(cmd, ...) cmd.append(...)
|
|
26
|
+
*
|
|
27
|
+
* The value at the call is the LAST reset plus every extend after it. That is
|
|
28
|
+
* what makes `cmd = "ls " + name; cmd = "ls -l"; system(cmd)` quiet: the build
|
|
29
|
+
* that spliced `name` in was overwritten before the call ran.
|
|
30
|
+
*
|
|
31
|
+
* It gives up - returns null, which leaves the rule exactly as quiet as it was
|
|
32
|
+
* before - whenever the answer would depend on which way the program went:
|
|
33
|
+
*
|
|
34
|
+
* - any write to the name sits under an if, a loop, a switch or a try;
|
|
35
|
+
* - there is no reset in this function at all (a parameter, a global);
|
|
36
|
+
* - the write is a shape this file has not been taught.
|
|
37
|
+
*
|
|
38
|
+
* Giving up is the safe direction for a SIGNATURE rule in the narrow sense that
|
|
39
|
+
* it cannot add a false finding. It is not free: every one of those is a guess
|
|
40
|
+
* the rule could have made and did not. The coverage notes say so, rather than
|
|
41
|
+
* letting "implemented" imply the conditional case is covered too.
|
|
42
|
+
*
|
|
43
|
+
* It never evaluates anything. It describes how the text was put together,
|
|
44
|
+
* which is all a signature rule is entitled to know.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import type { Node } from 'web-tree-sitter';
|
|
48
|
+
import type { LanguageId } from '../../parse/languages.js';
|
|
49
|
+
import { analyzeStringExpression, type StringExpression } from './strings.js';
|
|
50
|
+
|
|
51
|
+
export interface LocalBuild {
|
|
52
|
+
readonly expression: StringExpression;
|
|
53
|
+
/** 1-based line of the write the value at the call starts from. */
|
|
54
|
+
readonly line: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Where a function's own writes stop. A nested function is a different scope. */
|
|
58
|
+
const FUNCTION_NODES = new Set([
|
|
59
|
+
'function_declaration', 'function_expression', 'arrow_function', 'method_definition',
|
|
60
|
+
'function_definition', 'lambda', 'lambda_expression',
|
|
61
|
+
'method_declaration', 'constructor_declaration',
|
|
62
|
+
'func_literal', 'anonymous_function_creation_expression',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
* Control flow, matched on the node-type NAME across all eight grammars, because
|
|
67
|
+
* eight grammars spell it eight ways (if_statement, elif_clause,
|
|
68
|
+
* expression_switch_statement, enhanced_for_statement, for_range_loop,
|
|
69
|
+
* switch_block_statement_group, catch_clause, except_clause...). A type this
|
|
70
|
+
* pattern wrongly calls conditional only makes the helper give up - it cannot
|
|
71
|
+
* make it report something it would not otherwise have reported.
|
|
72
|
+
*/
|
|
73
|
+
const CONTROL_FLOW =
|
|
74
|
+
/(^|_)(if|else|elif|switch|case|for|foreach|while|do|try|catch|except|finally|match|conditional|ternary)(_|$)/;
|
|
75
|
+
|
|
76
|
+
/* C's output-parameter writers. The destination is always argument 0. */
|
|
77
|
+
const C_FORMAT_WRITERS: Record<string, number> = {
|
|
78
|
+
sprintf: 1, vsprintf: 1, snprintf: 2, vsnprintf: 2, // value: index of the format string
|
|
79
|
+
};
|
|
80
|
+
const C_COPY_RESETS = new Set(['strcpy', 'strncpy', 'strlcpy']);
|
|
81
|
+
const C_APPENDS = new Set(['strcat', 'strncat', 'strlcat']);
|
|
82
|
+
|
|
83
|
+
/* C++ std::string members that replace or extend the receiver. */
|
|
84
|
+
const CPP_MEMBER_RESETS = new Set(['assign']);
|
|
85
|
+
const CPP_MEMBER_APPENDS = new Set(['append']);
|
|
86
|
+
|
|
87
|
+
/*
|
|
88
|
+
* `.c_str()` and `.data()` do nothing to the text; they hand a std::string over
|
|
89
|
+
* in the shape a C API wants. std::system() takes a `const char*`, so EVERY C++
|
|
90
|
+
* shell call ends in one of these, and to the signature pass a method call was
|
|
91
|
+
* an opaque value - which is why even the one-line form was silent:
|
|
92
|
+
*
|
|
93
|
+
* std::system(("ls " + name).c_str());
|
|
94
|
+
*/
|
|
95
|
+
const TRANSPARENT_METHODS = new Set(['c_str', 'data']);
|
|
96
|
+
|
|
97
|
+
type Write =
|
|
98
|
+
| { kind: 'reset'; node: Node; piece: StringExpression }
|
|
99
|
+
| { kind: 'extend'; node: Node; piece: StringExpression };
|
|
100
|
+
|
|
101
|
+
const lineOf = (node: Node): number => node.startPosition.row + 1;
|
|
102
|
+
|
|
103
|
+
const same = (a: Node | null, b: Node | null): boolean =>
|
|
104
|
+
a !== null && b !== null && a.startIndex === b.startIndex && a.endIndex === b.endIndex;
|
|
105
|
+
|
|
106
|
+
/** The bare name a declarator or assignment target binds: `cmd`, `buf[256]`, `*p`, `$cmd`. */
|
|
107
|
+
function boundName(target: Node | null): string | null {
|
|
108
|
+
let current: Node | null = target;
|
|
109
|
+
for (let depth = 0; current && depth < 6; depth++) {
|
|
110
|
+
if (current.type === 'identifier' || current.type === 'variable_name') {
|
|
111
|
+
return (current.text ?? '').replace(/^\$/, '');
|
|
112
|
+
}
|
|
113
|
+
// array_declarator, pointer_declarator, reference_declarator, expression_list
|
|
114
|
+
current = current.childForFieldName('declarator') ?? current.namedChildren[0] ?? null;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Strip parentheses and `.c_str()` / `.data()` - wrappers that do not change the text. */
|
|
120
|
+
function unwrap(node: Node): Node {
|
|
121
|
+
let current = node;
|
|
122
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
123
|
+
if (current.type === 'parenthesized_expression' && current.namedChildren.length === 1) {
|
|
124
|
+
current = current.namedChildren[0] ?? current;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (current.type === 'call_expression') {
|
|
128
|
+
const fn = current.childForFieldName('function');
|
|
129
|
+
const args = current.childForFieldName('arguments');
|
|
130
|
+
const field = fn?.childForFieldName('field')?.text ?? '';
|
|
131
|
+
const receiver = fn?.childForFieldName('argument') ?? null;
|
|
132
|
+
if (fn?.type === 'field_expression' && TRANSPARENT_METHODS.has(field) && receiver &&
|
|
133
|
+
(args?.namedChildren.length ?? 0) === 0) {
|
|
134
|
+
current = receiver;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
return current;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/*
|
|
144
|
+
* THE FORMAT STRING IS A TYPE DECLARATION.
|
|
145
|
+
*
|
|
146
|
+
* `%d` can only ever emit digits and a sign, so it cannot carry a semicolon, a
|
|
147
|
+
* pipe or a backtick however hostile the integer is. `%s` and `%c` splice text.
|
|
148
|
+
* So `snprintf(buf, n, "kill -9 %d", pid)` is not a built command in any sense
|
|
149
|
+
* that matters to a shell, and `snprintf(buf, n, "ls %s", name)` is.
|
|
150
|
+
*
|
|
151
|
+
* A format string that is not itself a literal is the worst case of all - the
|
|
152
|
+
* caller controls the shape of the command - and is reported as dynamic.
|
|
153
|
+
*/
|
|
154
|
+
const SPECIFIER = /%[-+ #0]*(\*|\d+)?(?:\.(\*|\d+))?(?:hh|h|ll|l|j|z|t|L)?([diouxXeEfFgGaAcspn%])/g;
|
|
155
|
+
|
|
156
|
+
function formatPiece(call: Node, language: LanguageId, formatIndex: number, isVa: boolean): StringExpression {
|
|
157
|
+
const args = (call.childForFieldName('arguments')?.namedChildren ?? []).filter((c): c is Node => c !== null);
|
|
158
|
+
const format = args[formatIndex];
|
|
159
|
+
if (!format) return { isDynamic: false, mechanism: 'literal', literalText: '', dynamicParts: [] };
|
|
160
|
+
|
|
161
|
+
const fmt = analyzeStringExpression(format, language);
|
|
162
|
+
if (fmt.isDynamic) {
|
|
163
|
+
return { isDynamic: true, mechanism: 'format-call', literalText: '', dynamicParts: [format.text ?? ''] };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const dynamicParts: string[] = [];
|
|
167
|
+
let argCursor = formatIndex + 1;
|
|
168
|
+
for (const match of fmt.literalText.matchAll(SPECIFIER)) {
|
|
169
|
+
const [, width, precision, conversion] = match;
|
|
170
|
+
if (conversion === '%') continue;
|
|
171
|
+
if (width === '*') argCursor++;
|
|
172
|
+
if (precision === '*') argCursor++;
|
|
173
|
+
const value = args[argCursor++];
|
|
174
|
+
if (conversion !== 's' && conversion !== 'c') continue; // numeric: cannot splice text
|
|
175
|
+
if (isVa) {
|
|
176
|
+
dynamicParts.push('va_list');
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (!value) continue;
|
|
180
|
+
const piece = analyzeStringExpression(value, language);
|
|
181
|
+
if (piece.isDynamic) dynamicParts.push((value.text ?? '').trim().slice(0, 60));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
isDynamic: dynamicParts.length > 0,
|
|
186
|
+
mechanism: dynamicParts.length > 0 ? 'format-call' : 'literal',
|
|
187
|
+
literalText: fmt.literalText.replace(SPECIFIER, ''),
|
|
188
|
+
dynamicParts,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Classify one node as a write to `name`, or null if it is not one. */
|
|
193
|
+
function writeTo(node: Node, name: string, language: LanguageId): Write | null {
|
|
194
|
+
const piece = (value: Node | null): StringExpression | null =>
|
|
195
|
+
value ? analyzeStringExpression(unwrap(value), language) : null;
|
|
196
|
+
|
|
197
|
+
// Declarations with an initialiser: JS/TS/Java variable_declarator, C/C++ init_declarator, Go var_spec.
|
|
198
|
+
if (node.type === 'variable_declarator' || node.type === 'init_declarator' || node.type === 'var_spec') {
|
|
199
|
+
const target = node.childForFieldName('name') ?? node.childForFieldName('declarator');
|
|
200
|
+
const value = node.childForFieldName('value');
|
|
201
|
+
if (boundName(target) !== name || !value) return null;
|
|
202
|
+
const p = piece(value);
|
|
203
|
+
return p ? { kind: 'reset', node, piece: p } : null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Assignment, plain or compound. The operator decides reset versus extend.
|
|
207
|
+
if (
|
|
208
|
+
node.type === 'assignment' || node.type === 'assignment_expression' ||
|
|
209
|
+
node.type === 'augmented_assignment' || node.type === 'augmented_assignment_expression' ||
|
|
210
|
+
node.type === 'assignment_statement' || node.type === 'short_var_declaration'
|
|
211
|
+
) {
|
|
212
|
+
const target = node.childForFieldName('left');
|
|
213
|
+
const value = node.childForFieldName('right');
|
|
214
|
+
if (!target || !value) return null;
|
|
215
|
+
if ((target.text ?? '').trim().replace(/^\$/, '') !== name) return null;
|
|
216
|
+
const operator = (node.childForFieldName('operator')?.text ?? '=').trim();
|
|
217
|
+
const p = piece(value);
|
|
218
|
+
if (!p) return null;
|
|
219
|
+
if (operator === '=' || operator === ':=') return { kind: 'reset', node, piece: p };
|
|
220
|
+
if (operator === '+=' || operator === '.=') return { kind: 'extend', node, piece: p };
|
|
221
|
+
return null; // some other compound operator: not a string build this knows
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (node.type !== 'call_expression') return null;
|
|
225
|
+
const fn = node.childForFieldName('function');
|
|
226
|
+
const args = (node.childForFieldName('arguments')?.namedChildren ?? []).filter((c): c is Node => c !== null);
|
|
227
|
+
|
|
228
|
+
// C++ members on the string itself: cmd.append(x), cmd.assign(x).
|
|
229
|
+
if (fn?.type === 'field_expression') {
|
|
230
|
+
const receiver = fn.childForFieldName('argument');
|
|
231
|
+
const member = fn.childForFieldName('field')?.text ?? '';
|
|
232
|
+
if ((receiver?.text ?? '').trim() !== name) return null;
|
|
233
|
+
const p = piece(args[0] ?? null);
|
|
234
|
+
if (!p) return null;
|
|
235
|
+
if (CPP_MEMBER_APPENDS.has(member)) return { kind: 'extend', node, piece: p };
|
|
236
|
+
if (CPP_MEMBER_RESETS.has(member)) return { kind: 'reset', node, piece: p };
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// C's output-parameter idiom: the buffer is argument 0.
|
|
241
|
+
if (language !== 'c' && language !== 'cpp') return null;
|
|
242
|
+
const callee = (fn?.text ?? '').split('::').pop() ?? '';
|
|
243
|
+
if ((args[0]?.text ?? '').trim() !== name) return null;
|
|
244
|
+
if (callee in C_FORMAT_WRITERS) {
|
|
245
|
+
return {
|
|
246
|
+
kind: 'reset',
|
|
247
|
+
node,
|
|
248
|
+
piece: formatPiece(node, language, C_FORMAT_WRITERS[callee] ?? 1, callee.startsWith('v')),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const p = piece(args[1] ?? null);
|
|
252
|
+
if (!p) return null;
|
|
253
|
+
if (C_COPY_RESETS.has(callee)) return { kind: 'reset', node, piece: p };
|
|
254
|
+
if (C_APPENDS.has(callee)) return { kind: 'extend', node, piece: p };
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Is `node` inside a branch, loop or try between itself and the function body? */
|
|
259
|
+
function underControlFlow(node: Node, body: Node): boolean {
|
|
260
|
+
let current: Node | null = node.parent;
|
|
261
|
+
while (current && !same(current, body)) {
|
|
262
|
+
if (CONTROL_FLOW.test(current.type)) return true;
|
|
263
|
+
current = current.parent;
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Follow the argument of a dangerous call back to how its text was built.
|
|
270
|
+
* Returns null when there is nothing to follow, or when following it would
|
|
271
|
+
* need to know which way the program went.
|
|
272
|
+
*/
|
|
273
|
+
export function followLocalBuild(arg: Node, language: LanguageId): LocalBuild | null {
|
|
274
|
+
const target = unwrap(arg);
|
|
275
|
+
|
|
276
|
+
// Unwrapping alone can expose an inline build: std::system(("ls " + x).c_str()).
|
|
277
|
+
if (!same(target, arg) && target.type !== 'identifier') {
|
|
278
|
+
return { expression: analyzeStringExpression(target, language), line: lineOf(target) };
|
|
279
|
+
}
|
|
280
|
+
if (target.type !== 'identifier' && target.type !== 'variable_name') return null;
|
|
281
|
+
const name = (target.text ?? '').replace(/^\$/, '');
|
|
282
|
+
|
|
283
|
+
let body: Node | null = arg.parent;
|
|
284
|
+
while (body && !FUNCTION_NODES.has(body.type)) body = body.parent;
|
|
285
|
+
if (!body) return null;
|
|
286
|
+
|
|
287
|
+
const writes: Write[] = [];
|
|
288
|
+
let unsafe = false;
|
|
289
|
+
const visit = (node: Node): void => {
|
|
290
|
+
if (unsafe || node.startIndex >= arg.startIndex) return;
|
|
291
|
+
if (!same(node, body) && FUNCTION_NODES.has(node.type)) return; // a nested function
|
|
292
|
+
const write = writeTo(node, name, language);
|
|
293
|
+
if (write) {
|
|
294
|
+
if (underControlFlow(node, body!)) unsafe = true;
|
|
295
|
+
else writes.push(write);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
for (const child of node.namedChildren) if (child) visit(child);
|
|
299
|
+
};
|
|
300
|
+
visit(body);
|
|
301
|
+
if (unsafe) return null;
|
|
302
|
+
|
|
303
|
+
let start = -1;
|
|
304
|
+
for (let i = writes.length - 1; i >= 0; i--) {
|
|
305
|
+
if (writes[i]?.kind === 'reset') {
|
|
306
|
+
start = i;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (start < 0) return null; // a parameter or a global: nothing here says how it was built
|
|
311
|
+
|
|
312
|
+
const pieces = writes.slice(start).map((w) => w.piece);
|
|
313
|
+
const first = writes[start]!;
|
|
314
|
+
if (pieces.length === 1) return { expression: pieces[0]!, line: lineOf(first.node) };
|
|
315
|
+
|
|
316
|
+
const dynamicParts = pieces.flatMap((p) => [...p.dynamicParts]);
|
|
317
|
+
const literalText = pieces.map((p) => p.literalText).join('');
|
|
318
|
+
const mechanism: StringExpression['mechanism'] = pieces.some((p) => p.mechanism === 'interpolation')
|
|
319
|
+
? 'interpolation'
|
|
320
|
+
: pieces.some((p) => p.mechanism === 'format-call')
|
|
321
|
+
? 'format-call'
|
|
322
|
+
: dynamicParts.length > 0
|
|
323
|
+
? 'concatenation'
|
|
324
|
+
: 'literal';
|
|
325
|
+
return {
|
|
326
|
+
expression: { isDynamic: mechanism !== 'literal', mechanism, literalText, dynamicParts },
|
|
327
|
+
line: lineOf(first.node),
|
|
328
|
+
};
|
|
329
|
+
}
|