circle-ir 4.16.1 → 4.18.1
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/dist/analysis/advisories.d.ts +186 -0
- package/dist/analysis/advisories.d.ts.map +1 -0
- package/dist/analysis/advisories.js +391 -0
- package/dist/analysis/advisories.js.map +1 -0
- package/dist/analysis/comment-lines.d.ts +14 -0
- package/dist/analysis/comment-lines.d.ts.map +1 -0
- package/dist/analysis/comment-lines.js +34 -0
- package/dist/analysis/comment-lines.js.map +1 -0
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +59 -5
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/constant-caller-gate.d.ts +2 -2
- package/dist/analysis/constant-caller-gate.d.ts.map +1 -1
- package/dist/analysis/constant-caller-gate.js +205 -29
- package/dist/analysis/constant-caller-gate.js.map +1 -1
- package/dist/analysis/constant-propagation/index.d.ts +2 -0
- package/dist/analysis/constant-propagation/index.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/index.js +10 -0
- package/dist/analysis/constant-propagation/index.js.map +1 -1
- package/dist/analysis/constant-propagation/propagator.d.ts +15 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +224 -20
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/constant-propagation/types.d.ts +2 -0
- package/dist/analysis/constant-propagation/types.d.ts.map +1 -1
- package/dist/analysis/constant-propagation.d.ts +1 -1
- package/dist/analysis/constant-propagation.d.ts.map +1 -1
- package/dist/analysis/constant-propagation.js +1 -1
- package/dist/analysis/constant-propagation.js.map +1 -1
- package/dist/analysis/entry-point-detection.d.ts.map +1 -1
- package/dist/analysis/entry-point-detection.js +72 -0
- package/dist/analysis/entry-point-detection.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +75 -8
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/maven-declared.d.ts +72 -0
- package/dist/analysis/maven-declared.d.ts.map +1 -0
- package/dist/analysis/maven-declared.js +216 -0
- package/dist/analysis/maven-declared.js.map +1 -0
- package/dist/analysis/maven-version.d.ts +28 -0
- package/dist/analysis/maven-version.d.ts.map +1 -0
- package/dist/analysis/maven-version.js +172 -0
- package/dist/analysis/maven-version.js.map +1 -0
- package/dist/analysis/passes/constant-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/constant-propagation-pass.js +29 -1
- package/dist/analysis/passes/constant-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
- package/dist/analysis/passes/cross-file-pass.js +84 -2
- package/dist/analysis/passes/cross-file-pass.js.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +7 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js +44 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.d.ts +2 -0
- package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.js +25 -3
- package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.d.ts +11 -14
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +562 -0
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +18 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +1079 -14
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js +82 -3
- package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts +44 -2
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +690 -12
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +298 -19
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/tls-verify-disabled-pass.d.ts +18 -0
- package/dist/analysis/passes/tls-verify-disabled-pass.d.ts.map +1 -1
- package/dist/analysis/passes/tls-verify-disabled-pass.js +161 -0
- package/dist/analysis/passes/tls-verify-disabled-pass.js.map +1 -1
- package/dist/analysis/passes/weak-random-pass.d.ts +6 -0
- package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-random-pass.js +29 -0
- package/dist/analysis/passes/weak-random-pass.js.map +1 -1
- package/dist/analysis/promote-cross-file-flows.d.ts +17 -0
- package/dist/analysis/promote-cross-file-flows.d.ts.map +1 -0
- package/dist/analysis/promote-cross-file-flows.js +32 -0
- package/dist/analysis/promote-cross-file-flows.js.map +1 -0
- package/dist/analysis/rust-literal-guard.d.ts.map +1 -1
- package/dist/analysis/rust-literal-guard.js +100 -1
- package/dist/analysis/rust-literal-guard.js.map +1 -1
- package/dist/analysis/sbom.d.ts +100 -0
- package/dist/analysis/sbom.d.ts.map +1 -1
- package/dist/analysis/sbom.js +514 -3
- package/dist/analysis/sbom.js.map +1 -1
- package/dist/analysis/semver-version.d.ts +30 -0
- package/dist/analysis/semver-version.d.ts.map +1 -0
- package/dist/analysis/semver-version.js +76 -0
- package/dist/analysis/semver-version.js.map +1 -0
- package/dist/analysis/taint-matcher.d.ts +5 -0
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +226 -4
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.d.ts +57 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +69 -6
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +3360 -314
- package/dist/core/circle-ir-core.cjs +783 -220
- package/dist/core/circle-ir-core.js +783 -220
- package/dist/core/extractors/dfg.js +205 -2
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/core/extractors/ts-declaration-detail.d.ts +39 -0
- package/dist/core/extractors/ts-declaration-detail.d.ts.map +1 -0
- package/dist/core/extractors/ts-declaration-detail.js +266 -0
- package/dist/core/extractors/ts-declaration-detail.js.map +1 -0
- package/dist/core/extractors/types.js +53 -9
- package/dist/core/extractors/types.js.map +1 -1
- package/dist/graph/analysis-pass.d.ts +4 -1
- package/dist/graph/analysis-pass.d.ts.map +1 -1
- package/dist/graph/analysis-pass.js +4 -1
- package/dist/graph/analysis-pass.js.map +1 -1
- package/dist/graph-facts/dead-code.d.ts +62 -0
- package/dist/graph-facts/dead-code.d.ts.map +1 -0
- package/dist/graph-facts/dead-code.js +90 -0
- package/dist/graph-facts/dead-code.js.map +1 -0
- package/dist/graph-facts/dependency-calls.d.ts +86 -0
- package/dist/graph-facts/dependency-calls.d.ts.map +1 -0
- package/dist/graph-facts/dependency-calls.js +93 -0
- package/dist/graph-facts/dependency-calls.js.map +1 -0
- package/dist/graph-facts/index.d.ts +10 -0
- package/dist/graph-facts/index.d.ts.map +1 -0
- package/dist/graph-facts/index.js +10 -0
- package/dist/graph-facts/index.js.map +1 -0
- package/dist/graph-facts/maven-packages.d.ts +42 -0
- package/dist/graph-facts/maven-packages.d.ts.map +1 -0
- package/dist/graph-facts/maven-packages.js +133 -0
- package/dist/graph-facts/maven-packages.js.map +1 -0
- package/dist/graph-facts/reach.d.ts +203 -0
- package/dist/graph-facts/reach.d.ts.map +1 -0
- package/dist/graph-facts/reach.js +219 -0
- package/dist/graph-facts/reach.js.map +1 -0
- package/dist/graph-facts/removal-reach.d.ts +73 -0
- package/dist/graph-facts/removal-reach.d.ts.map +1 -0
- package/dist/graph-facts/removal-reach.js +64 -0
- package/dist/graph-facts/removal-reach.js.map +1 -0
- package/dist/index.d.ts +21 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -3
- package/dist/index.js.map +1 -1
- package/dist/navigation/go-graph.d.ts +68 -0
- package/dist/navigation/go-graph.d.ts.map +1 -0
- package/dist/navigation/go-graph.js +753 -0
- package/dist/navigation/go-graph.js.map +1 -0
- package/dist/navigation/index.d.ts +7 -1
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js +6 -0
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/java-entry-tables.d.ts +43 -0
- package/dist/navigation/java-entry-tables.d.ts.map +1 -0
- package/dist/navigation/java-entry-tables.js +56 -0
- package/dist/navigation/java-entry-tables.js.map +1 -0
- package/dist/navigation/java-graph.d.ts +64 -0
- package/dist/navigation/java-graph.d.ts.map +1 -0
- package/dist/navigation/java-graph.js +466 -0
- package/dist/navigation/java-graph.js.map +1 -0
- package/dist/navigation/navigation-index.d.ts +91 -2
- package/dist/navigation/navigation-index.d.ts.map +1 -1
- package/dist/navigation/navigation-index.js +372 -45
- package/dist/navigation/navigation-index.js.map +1 -1
- package/dist/navigation/python-graph.d.ts +73 -0
- package/dist/navigation/python-graph.d.ts.map +1 -0
- package/dist/navigation/python-graph.js +867 -0
- package/dist/navigation/python-graph.js.map +1 -0
- package/dist/navigation/ts-call-edges.d.ts +193 -0
- package/dist/navigation/ts-call-edges.d.ts.map +1 -0
- package/dist/navigation/ts-call-edges.js +2089 -0
- package/dist/navigation/ts-call-edges.js.map +1 -0
- package/dist/navigation/ts-entry-tables.d.ts +88 -0
- package/dist/navigation/ts-entry-tables.d.ts.map +1 -0
- package/dist/navigation/ts-entry-tables.js +50 -0
- package/dist/navigation/ts-entry-tables.js.map +1 -0
- package/dist/path-set/branches.d.ts +52 -0
- package/dist/path-set/branches.d.ts.map +1 -0
- package/dist/path-set/branches.js +124 -0
- package/dist/path-set/branches.js.map +1 -0
- package/dist/path-set/finding-diff.d.ts +63 -0
- package/dist/path-set/finding-diff.d.ts.map +1 -0
- package/dist/path-set/finding-diff.js +150 -0
- package/dist/path-set/finding-diff.js.map +1 -0
- package/dist/path-set/index.d.ts +18 -0
- package/dist/path-set/index.d.ts.map +1 -0
- package/dist/path-set/index.js +12 -0
- package/dist/path-set/index.js.map +1 -0
- package/dist/path-set/line-diff.d.ts +19 -0
- package/dist/path-set/line-diff.d.ts.map +1 -0
- package/dist/path-set/line-diff.js +98 -0
- package/dist/path-set/line-diff.js.map +1 -0
- package/dist/path-set/path-set.d.ts +88 -0
- package/dist/path-set/path-set.d.ts.map +1 -0
- package/dist/path-set/path-set.js +716 -0
- package/dist/path-set/path-set.js.map +1 -0
- package/dist/path-set/templates.d.ts +42 -0
- package/dist/path-set/templates.d.ts.map +1 -0
- package/dist/path-set/templates.js +119 -0
- package/dist/path-set/templates.js.map +1 -0
- package/dist/path-set/types.d.ts +195 -0
- package/dist/path-set/types.d.ts.map +1 -0
- package/dist/path-set/types.js +8 -0
- package/dist/path-set/types.js.map +1 -0
- package/dist/remedy-class/index.d.ts +12 -0
- package/dist/remedy-class/index.d.ts.map +1 -0
- package/dist/remedy-class/index.js +9 -0
- package/dist/remedy-class/index.js.map +1 -0
- package/dist/remedy-class/match.d.ts +165 -0
- package/dist/remedy-class/match.d.ts.map +1 -0
- package/dist/remedy-class/match.js +705 -0
- package/dist/remedy-class/match.js.map +1 -0
- package/dist/remedy-class/output-context.d.ts +25 -0
- package/dist/remedy-class/output-context.d.ts.map +1 -0
- package/dist/remedy-class/output-context.js +150 -0
- package/dist/remedy-class/output-context.js.map +1 -0
- package/dist/remedy-class/sha256.d.ts +18 -0
- package/dist/remedy-class/sha256.d.ts.map +1 -0
- package/dist/remedy-class/sha256.js +89 -0
- package/dist/remedy-class/sha256.js.map +1 -0
- package/dist/remedy-class/table.d.ts +130 -0
- package/dist/remedy-class/table.d.ts.map +1 -0
- package/dist/remedy-class/table.js +457 -0
- package/dist/remedy-class/table.js.map +1 -0
- package/dist/resolution/cross-file.d.ts +18 -0
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +194 -5
- package/dist/resolution/cross-file.js.map +1 -1
- package/dist/stable-ids/builder.d.ts +20 -0
- package/dist/stable-ids/builder.d.ts.map +1 -0
- package/dist/stable-ids/builder.js +65 -0
- package/dist/stable-ids/builder.js.map +1 -0
- package/dist/stable-ids/diff.d.ts +18 -0
- package/dist/stable-ids/diff.d.ts.map +1 -0
- package/dist/stable-ids/diff.js +37 -0
- package/dist/stable-ids/diff.js.map +1 -0
- package/dist/stable-ids/go.d.ts +28 -0
- package/dist/stable-ids/go.d.ts.map +1 -0
- package/dist/stable-ids/go.js +160 -0
- package/dist/stable-ids/go.js.map +1 -0
- package/dist/stable-ids/hash.d.ts +22 -0
- package/dist/stable-ids/hash.d.ts.map +1 -0
- package/dist/stable-ids/hash.js +167 -0
- package/dist/stable-ids/hash.js.map +1 -0
- package/dist/stable-ids/index.d.ts +30 -0
- package/dist/stable-ids/index.d.ts.map +1 -0
- package/dist/stable-ids/index.js +59 -0
- package/dist/stable-ids/index.js.map +1 -0
- package/dist/stable-ids/java.d.ts +28 -0
- package/dist/stable-ids/java.d.ts.map +1 -0
- package/dist/stable-ids/java.js +224 -0
- package/dist/stable-ids/java.js.map +1 -0
- package/dist/stable-ids/moves.d.ts +87 -0
- package/dist/stable-ids/moves.d.ts.map +1 -0
- package/dist/stable-ids/moves.js +222 -0
- package/dist/stable-ids/moves.js.map +1 -0
- package/dist/stable-ids/nodes.d.ts +32 -0
- package/dist/stable-ids/nodes.d.ts.map +1 -0
- package/dist/stable-ids/nodes.js +92 -0
- package/dist/stable-ids/nodes.js.map +1 -0
- package/dist/stable-ids/normalize.d.ts +35 -0
- package/dist/stable-ids/normalize.d.ts.map +1 -0
- package/dist/stable-ids/normalize.js +174 -0
- package/dist/stable-ids/normalize.js.map +1 -0
- package/dist/stable-ids/parse-errors.d.ts +24 -0
- package/dist/stable-ids/parse-errors.d.ts.map +1 -0
- package/dist/stable-ids/parse-errors.js +46 -0
- package/dist/stable-ids/parse-errors.js.map +1 -0
- package/dist/stable-ids/python.d.ts +27 -0
- package/dist/stable-ids/python.d.ts.map +1 -0
- package/dist/stable-ids/python.js +212 -0
- package/dist/stable-ids/python.js.map +1 -0
- package/dist/stable-ids/ts-parse-errors.d.ts +27 -0
- package/dist/stable-ids/ts-parse-errors.d.ts.map +1 -0
- package/dist/stable-ids/ts-parse-errors.js +75 -0
- package/dist/stable-ids/ts-parse-errors.js.map +1 -0
- package/dist/stable-ids/types.d.ts +106 -0
- package/dist/stable-ids/types.d.ts.map +1 -0
- package/dist/stable-ids/types.js +16 -0
- package/dist/stable-ids/types.js.map +1 -0
- package/dist/stable-ids/typescript.d.ts +30 -0
- package/dist/stable-ids/typescript.d.ts.map +1 -0
- package/dist/stable-ids/typescript.js +362 -0
- package/dist/stable-ids/typescript.js.map +1 -0
- package/dist/types/config.d.ts +9 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +53 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remedy-class matcher: for every sink of a path that a version pair closed,
|
|
3
|
+
* report which row of a caller-supplied `safe-construct-table@0.1` the fixed
|
|
4
|
+
* code matches, or that none does, or that the matcher could not observe a
|
|
5
|
+
* row. Facts only: `class` and `remedyClass` are copied from the table, and
|
|
6
|
+
* nothing here judges whether a fix is adequate.
|
|
7
|
+
*
|
|
8
|
+
* Steps are looked for in the same declaration at the fix version as the
|
|
9
|
+
* baseline sink, on the IR circle-ir already produces: calls (receiver,
|
|
10
|
+
* receiver type, arguments, imports), DFG defs, uses and def-to-def chains,
|
|
11
|
+
* and the fix version's taint sources.
|
|
12
|
+
*
|
|
13
|
+
* Observed: `call` (type, methods, arg, ctor, static), `arg-is: tainted |
|
|
14
|
+
* untainted | from-allowlist`, `arg-literal-in`, `role: sink-replacement |
|
|
15
|
+
* binder | sanitizer | guard | normalizer`, `on`, `name` / `order`, `any`,
|
|
16
|
+
* `all`, `missing`, `sink-removed`. A guard needs branch facts
|
|
17
|
+
* (`deriveBranchFacts`) for the fix version's files. Not observed:
|
|
18
|
+
* `template` and `receiver-from` on a step. A row that needs a step it
|
|
19
|
+
* cannot observe **abstains**; it is never reported as unmatched.
|
|
20
|
+
*
|
|
21
|
+
* Pure: IR in, values out. Browser and Node compatible.
|
|
22
|
+
*/
|
|
23
|
+
import { sideOf } from '../path-set/branches.js';
|
|
24
|
+
import { handedNames, lineDominates, qualifiedMethodName, typePath } from '../path-set/path-set.js';
|
|
25
|
+
import { htmlContextAt, javaStringLiterals, markupBefore } from './output-context.js';
|
|
26
|
+
/** Thrown when the table was refused by the reader: no observation exists on a refused table. */
|
|
27
|
+
export class TableRefused extends Error {
|
|
28
|
+
problems;
|
|
29
|
+
digest;
|
|
30
|
+
constructor(digest, problems) {
|
|
31
|
+
super(`safe-construct table ${digest} refused: ${problems.length} problem(s); first: ${problems[0]?.path} ${problems[0]?.message}`);
|
|
32
|
+
this.name = 'TableRefused';
|
|
33
|
+
this.problems = problems;
|
|
34
|
+
this.digest = digest;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function inRange(line, r) {
|
|
38
|
+
return line >= r.start && line <= r.end;
|
|
39
|
+
}
|
|
40
|
+
/** The line range of a declaration at one version, or undefined when it is gone. */
|
|
41
|
+
function declRange(file, ir, decl, ids) {
|
|
42
|
+
if (decl.scheme === 'ids@0.1') {
|
|
43
|
+
const d = ids?.decls.find(x => x.id === decl.id);
|
|
44
|
+
return d ? { start: d.start_line, end: d.end_line } : undefined;
|
|
45
|
+
}
|
|
46
|
+
const prefix = `${file}#`;
|
|
47
|
+
if (!decl.id.startsWith(prefix))
|
|
48
|
+
return undefined;
|
|
49
|
+
const qualified = decl.id.slice(prefix.length);
|
|
50
|
+
for (const t of ir.types) {
|
|
51
|
+
for (const m of t.methods) {
|
|
52
|
+
if (qualifiedMethodName(t, m) === qualified)
|
|
53
|
+
return { start: m.start_line, end: m.end_line };
|
|
54
|
+
}
|
|
55
|
+
if (qualified === `${typePath(t)}.<init>`)
|
|
56
|
+
return { start: t.start_line, end: t.end_line };
|
|
57
|
+
}
|
|
58
|
+
if (qualified.startsWith('<module>'))
|
|
59
|
+
return 'by-method';
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const IDENT = /[A-Za-z_$][\w$]*/g;
|
|
63
|
+
const STRING_LITERAL = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g;
|
|
64
|
+
/** Identifiers in an expression, string literals removed. */
|
|
65
|
+
function identifiers(expr) {
|
|
66
|
+
return new Set((expr ?? '').replace(STRING_LITERAL, '""').match(IDENT) ?? []);
|
|
67
|
+
}
|
|
68
|
+
function buildCtx(file, ir, decl, ids, seedParameters) {
|
|
69
|
+
const range = declRange(file, ir, decl, ids);
|
|
70
|
+
if (!range)
|
|
71
|
+
return undefined;
|
|
72
|
+
let calls;
|
|
73
|
+
let lineOk;
|
|
74
|
+
if (range === 'by-method') {
|
|
75
|
+
const method = decl.name.startsWith('<module>.') ? decl.name.slice('<module>.'.length) : null;
|
|
76
|
+
calls = ir.calls.filter(c => (c.in_method ?? null) === method);
|
|
77
|
+
const lines = new Set(calls.map(c => c.location.line));
|
|
78
|
+
lineOk = l => lines.has(l);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
calls = ir.calls.filter(c => inRange(c.location.line, range));
|
|
82
|
+
lineOk = l => inRange(l, range);
|
|
83
|
+
}
|
|
84
|
+
const defs = ir.dfg.defs.filter(d => lineOk(d.line));
|
|
85
|
+
const defIds = new Set(defs.map(d => d.id));
|
|
86
|
+
const next = new Map();
|
|
87
|
+
for (const ch of ir.dfg.chains ?? []) {
|
|
88
|
+
if (!defIds.has(ch.from_def) || !defIds.has(ch.to_def))
|
|
89
|
+
continue;
|
|
90
|
+
const list = next.get(ch.from_def);
|
|
91
|
+
if (list)
|
|
92
|
+
list.push(ch.to_def);
|
|
93
|
+
else
|
|
94
|
+
next.set(ch.from_def, [ch.to_def]);
|
|
95
|
+
}
|
|
96
|
+
const reachCache = new Map();
|
|
97
|
+
const reach = (from) => {
|
|
98
|
+
const hit = reachCache.get(from);
|
|
99
|
+
if (hit)
|
|
100
|
+
return hit;
|
|
101
|
+
const seen = new Set([from]);
|
|
102
|
+
const stack = [from];
|
|
103
|
+
while (stack.length) {
|
|
104
|
+
const d = stack.pop();
|
|
105
|
+
for (const n of next.get(d) ?? []) {
|
|
106
|
+
if (!seen.has(n)) {
|
|
107
|
+
seen.add(n);
|
|
108
|
+
stack.push(n);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
reachCache.set(from, seen);
|
|
113
|
+
return seen;
|
|
114
|
+
};
|
|
115
|
+
const seeds = [];
|
|
116
|
+
for (const s of ir.taint.sources) {
|
|
117
|
+
if (!lineOk(s.line))
|
|
118
|
+
continue;
|
|
119
|
+
for (const d of defs) {
|
|
120
|
+
if (d.line === s.line && (s.variable === undefined || d.variable === s.variable))
|
|
121
|
+
seeds.push(d.id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (seedParameters)
|
|
125
|
+
for (const d of defs)
|
|
126
|
+
if (d.kind === 'param')
|
|
127
|
+
seeds.push(d.id);
|
|
128
|
+
const tainted = new Set();
|
|
129
|
+
for (const s of seeds)
|
|
130
|
+
for (const d of reach(s))
|
|
131
|
+
tainted.add(d);
|
|
132
|
+
return { file, ir, calls, defs, tainted, reach };
|
|
133
|
+
}
|
|
134
|
+
/** Uses at `line` whose variable appears in `expr`, as their reaching def ids. */
|
|
135
|
+
function defsUsedIn(ctx, line, expr) {
|
|
136
|
+
const names = identifiers(expr);
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const u of ctx.ir.dfg.uses) {
|
|
139
|
+
if (u.line === line && u.def_id !== null && names.has(u.variable))
|
|
140
|
+
out.push(u.def_id);
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
/** Does a source call appear inline in the expression at this line (`f(req.getParameter("x"))`)? */
|
|
145
|
+
function inlineSource(ctx, line, expr) {
|
|
146
|
+
return ctx.ir.taint.sources.some(s => {
|
|
147
|
+
if (s.line !== line)
|
|
148
|
+
return false;
|
|
149
|
+
const name = (s.method ?? s.location.split(' in ')[0].replace(/\(\)$/, '').split('.').pop() ?? '').trim();
|
|
150
|
+
return name.length > 0 && expr.includes(`${name}(`);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function argTainted(ctx, call, expr) {
|
|
154
|
+
if (inlineSource(ctx, call.location.line, expr))
|
|
155
|
+
return true;
|
|
156
|
+
return defsUsedIn(ctx, call.location.line, expr).some(d => ctx.tainted.has(d));
|
|
157
|
+
}
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Call shapes
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
const tail = (fqn) => fqn.slice(fqn.lastIndexOf('.') + 1);
|
|
162
|
+
const pkg = (fqn) => fqn.slice(0, Math.max(0, fqn.lastIndexOf('.')));
|
|
163
|
+
const RANK = { fqn: 0, import: 1, any: 2, 'simple-name': 3, unresolved: 4, template: 5 };
|
|
164
|
+
function isCtor(c) {
|
|
165
|
+
return c.is_constructor === true || (c.receiver === null && !!c.receiver_type && c.receiver_type === c.method_name);
|
|
166
|
+
}
|
|
167
|
+
/** A receiver written as a type (`Runtime`, `org.owasp.encoder.Encode`): a static call. */
|
|
168
|
+
function staticReceiver(c) {
|
|
169
|
+
const r = c.receiver;
|
|
170
|
+
if (!r || r.includes('('))
|
|
171
|
+
return null;
|
|
172
|
+
const last = tail(r);
|
|
173
|
+
return /^[A-Z]/.test(last) ? r : null;
|
|
174
|
+
}
|
|
175
|
+
function typeMatch(want, c, ir) {
|
|
176
|
+
if (want === 'java.lang.Object')
|
|
177
|
+
return 'any';
|
|
178
|
+
const fqn = c.receiver_type_fqn ?? (staticReceiver(c)?.includes('.') ? staticReceiver(c) : null);
|
|
179
|
+
if (fqn)
|
|
180
|
+
return fqn === want ? 'fqn' : null;
|
|
181
|
+
const simple = c.receiver_type ?? staticReceiver(c);
|
|
182
|
+
if (!simple) {
|
|
183
|
+
// receiver is a call expression or an untyped variable; an unqualified call is the enclosing class
|
|
184
|
+
return c.receiver === null && !isCtor(c) ? null : 'unresolved';
|
|
185
|
+
}
|
|
186
|
+
if (simple.includes('.'))
|
|
187
|
+
return simple === want ? 'fqn' : null;
|
|
188
|
+
if (simple !== tail(want))
|
|
189
|
+
return null;
|
|
190
|
+
const explicit = ir.imports.find(i => !i.is_wildcard && i.imported_name === simple);
|
|
191
|
+
if (explicit)
|
|
192
|
+
return `${explicit.from_package}.${simple}` === want ? 'import' : null;
|
|
193
|
+
if (ir.imports.some(i => i.is_wildcard && i.from_package === pkg(want)))
|
|
194
|
+
return 'import';
|
|
195
|
+
if (pkg(want) === 'java.lang')
|
|
196
|
+
return 'import';
|
|
197
|
+
return 'simple-name';
|
|
198
|
+
}
|
|
199
|
+
function methodMatches(patterns, name) {
|
|
200
|
+
return patterns.some(p => p === '*' || p === name || (p.endsWith('*') && name.startsWith(p.slice(0, -1))));
|
|
201
|
+
}
|
|
202
|
+
function shapeMatch(want, c, ir) {
|
|
203
|
+
if (want.ctor) {
|
|
204
|
+
if (!isCtor(c))
|
|
205
|
+
return null;
|
|
206
|
+
if (want.type === 'java.lang.Object')
|
|
207
|
+
return 'any';
|
|
208
|
+
const simple = c.receiver_type ?? c.method_name;
|
|
209
|
+
return typeMatch(want.type, { ...c, receiver_type: simple, receiver: simple }, ir);
|
|
210
|
+
}
|
|
211
|
+
if (isCtor(c)) {
|
|
212
|
+
// `methods: ["*"]` on `java.lang.Object` is any call at the sink, constructors included
|
|
213
|
+
return want.type === 'java.lang.Object' && want.methods.includes('*') ? 'any' : null;
|
|
214
|
+
}
|
|
215
|
+
if (!methodMatches(want.methods, c.method_name))
|
|
216
|
+
return null;
|
|
217
|
+
return typeMatch(want.type, c, ir);
|
|
218
|
+
}
|
|
219
|
+
function argsAt(c, arg) {
|
|
220
|
+
if (arg === undefined)
|
|
221
|
+
return null;
|
|
222
|
+
if (arg === 'all')
|
|
223
|
+
return c.arguments.map(a => a.expression);
|
|
224
|
+
const a = c.arguments.find(x => x.position === arg);
|
|
225
|
+
return a ? [a.expression] : [];
|
|
226
|
+
}
|
|
227
|
+
const VECTOR_LITERAL = /^\s*(?:new\s+[\w.]+\s*\[\s*\]\s*\{|[\w.]+\.(?:of|asList)\s*\()/;
|
|
228
|
+
/** Element 0 of an array or list literal (`new String[]{a, b}`, `List.of(a, b)`), as text; else null. */
|
|
229
|
+
function leadingElement(expr) {
|
|
230
|
+
const m = VECTOR_LITERAL.exec(expr);
|
|
231
|
+
if (!m)
|
|
232
|
+
return null;
|
|
233
|
+
const rest = expr.slice(m[0].length);
|
|
234
|
+
let depth = 0;
|
|
235
|
+
let quote = null;
|
|
236
|
+
for (let i = 0; i < rest.length; i++) {
|
|
237
|
+
const ch = rest[i];
|
|
238
|
+
if (quote) {
|
|
239
|
+
if (ch === '\\')
|
|
240
|
+
i++;
|
|
241
|
+
else if (ch === quote)
|
|
242
|
+
quote = null;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (ch === '"' || ch === "'")
|
|
246
|
+
quote = ch;
|
|
247
|
+
else if (ch === '(' || ch === '[' || ch === '{')
|
|
248
|
+
depth++;
|
|
249
|
+
else if ((ch === ')' || ch === ']' || ch === '}') && depth > 0)
|
|
250
|
+
depth--;
|
|
251
|
+
else if ((ch === ',' || ch === ')' || ch === '}') && depth === 0)
|
|
252
|
+
return rest.slice(0, i).trim();
|
|
253
|
+
}
|
|
254
|
+
return rest.trim() || null;
|
|
255
|
+
}
|
|
256
|
+
/** The literal at an argument, or element 0 of an array or list literal there. */
|
|
257
|
+
function leadingLiteral(c, arg) {
|
|
258
|
+
const a = c.arguments.find(x => x.position === (arg === 'all' || arg === undefined ? 0 : arg));
|
|
259
|
+
if (!a)
|
|
260
|
+
return null;
|
|
261
|
+
if (a.literal !== null && a.literal !== undefined)
|
|
262
|
+
return a.literal.trim();
|
|
263
|
+
const first = leadingElement(a.expression);
|
|
264
|
+
const m = first === null ? null : /^"((?:[^"\\]|\\.)*)"$/.exec(first);
|
|
265
|
+
return m ? m[1].trim() : null;
|
|
266
|
+
}
|
|
267
|
+
/** Def produced by a call: a def on its line when the call is the outermost call there. */
|
|
268
|
+
function resultDef(ctx, c) {
|
|
269
|
+
const line = c.location.line;
|
|
270
|
+
const others = ctx.calls.filter(x => x !== c && x.location.line === line);
|
|
271
|
+
const inner = others.some(o => (o.receiver ?? '').includes(`${c.method_name}(`) || o.arguments.some(a => a.expression.includes(`${c.method_name}(`)));
|
|
272
|
+
if (inner)
|
|
273
|
+
return undefined;
|
|
274
|
+
// several defs on one line (`try (Connection c = ..; PreparedStatement ps = ..)`): the def written
|
|
275
|
+
// nearest before the call, by column; without columns, the only def on the line
|
|
276
|
+
const onLine = ctx.defs.filter(d => d.line === line && d.kind !== 'param');
|
|
277
|
+
if (onLine.length <= 1)
|
|
278
|
+
return onLine[0];
|
|
279
|
+
const before = onLine
|
|
280
|
+
.filter(d => d.column !== undefined && d.column <= c.location.column)
|
|
281
|
+
.sort((a, b) => b.column - a.column);
|
|
282
|
+
return before[0];
|
|
283
|
+
}
|
|
284
|
+
/** The call on the same line whose receiver or an argument holds `c` (`c` is nested in it). */
|
|
285
|
+
function enclosingCall(ctx, c) {
|
|
286
|
+
return ctx.calls.find(o => o !== c && o.location.line === c.location.line &&
|
|
287
|
+
((o.receiver ?? '').includes(`${c.method_name}(`) || o.arguments.some(a => a.expression.includes(`${c.method_name}(`))));
|
|
288
|
+
}
|
|
289
|
+
/** Does `a`'s result (or receiver object) reach `b`'s receiver or an argument? */
|
|
290
|
+
function reaches(ctx, a, b, depth = 0) {
|
|
291
|
+
const texts = [b.receiver ?? '', ...b.arguments.map(x => x.expression)];
|
|
292
|
+
if (texts.some(t => t.includes(`${a.method_name}(`)) && a.location.line === b.location.line)
|
|
293
|
+
return true;
|
|
294
|
+
// a nested call reaches whatever the call holding it reaches (`base.resolve(n).normalize()`)
|
|
295
|
+
const outer = enclosingCall(ctx, a);
|
|
296
|
+
if (outer && depth < 8)
|
|
297
|
+
return outer === b || reaches(ctx, outer, b, depth + 1);
|
|
298
|
+
const def = resultDef(ctx, a);
|
|
299
|
+
if (def) {
|
|
300
|
+
const reach = ctx.reach(def.id);
|
|
301
|
+
return texts.some(t => defsUsedIn(ctx, b.location.line, t).some(d => reach.has(d)));
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
function unobservableReason(step, env) {
|
|
306
|
+
if (step.template && env.templateFacts === null)
|
|
307
|
+
return 'template-not-read';
|
|
308
|
+
if (step.call?.receiverFrom)
|
|
309
|
+
return 'step-not-observed:receiver-from';
|
|
310
|
+
if (step.argLiteralIn && !step.call)
|
|
311
|
+
return 'step-not-observed:arg-literal-in-without-call';
|
|
312
|
+
if ((step.role === 'guard' || step.on !== undefined) && env.ctx && !env.ctx.branches)
|
|
313
|
+
return 'step-not-observed:guard-without-branch-facts';
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
function evalStep(step, path, env) {
|
|
317
|
+
const why = unobservableReason(step, env);
|
|
318
|
+
if (why) {
|
|
319
|
+
env.observed.push({ path, holds: false });
|
|
320
|
+
return { holds: false, unobservable: why, hits: [] };
|
|
321
|
+
}
|
|
322
|
+
let result;
|
|
323
|
+
if (step.any || step.all) {
|
|
324
|
+
const children = (step.any ?? step.all);
|
|
325
|
+
const q = step.any ? 'any' : 'all';
|
|
326
|
+
const rs = children.filter(c => !(c.order && Object.keys(c).length === 1))
|
|
327
|
+
.map((c) => evalStep(c, `${path}/${q}/${children.indexOf(c)}`, env));
|
|
328
|
+
if (step.any) {
|
|
329
|
+
const holds = rs.some(r => r.holds);
|
|
330
|
+
result = { holds, hits: rs.flatMap(r => r.hits) };
|
|
331
|
+
if (!holds) {
|
|
332
|
+
const u = rs.find(r => r.unobservable)?.unobservable;
|
|
333
|
+
if (u)
|
|
334
|
+
result.unobservable = u;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
const failed = rs.filter(r => !r.holds);
|
|
339
|
+
const u = failed.find(r => r.unobservable)?.unobservable;
|
|
340
|
+
result = { holds: failed.length === 0, hits: rs.flatMap(r => r.hits) };
|
|
341
|
+
if (u && failed.every(r => r.unobservable))
|
|
342
|
+
result.unobservable = u;
|
|
343
|
+
// order steps after their siblings have bound their names
|
|
344
|
+
children.forEach((c, i) => {
|
|
345
|
+
if (!(c.order && Object.keys(c).length === 1))
|
|
346
|
+
return;
|
|
347
|
+
const holds = result.holds && orderHolds(c.order, env);
|
|
348
|
+
env.observed.push({ path: `${path}/${q}/${i}`, holds });
|
|
349
|
+
if (!holds)
|
|
350
|
+
result.holds = false;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
else if (step.missing) {
|
|
355
|
+
const inner = evalStep(step.missing, `${path}/missing`, env);
|
|
356
|
+
result = inner.unobservable ? { holds: false, unobservable: inner.unobservable, hits: [] } : { holds: !inner.holds, hits: [] };
|
|
357
|
+
env.observed.push({ path, holds: result.holds });
|
|
358
|
+
}
|
|
359
|
+
else if (step.sinkRemoved) {
|
|
360
|
+
const holds = env.pathRemoved && env.sinkCalls.length === 0;
|
|
361
|
+
result = { holds, hits: [] };
|
|
362
|
+
env.observed.push({ path, holds });
|
|
363
|
+
}
|
|
364
|
+
else if (step.template) {
|
|
365
|
+
const want = step.template;
|
|
366
|
+
const hit = (env.templateFacts ?? []).find(f => templateMatches(want, f));
|
|
367
|
+
result = { holds: hit !== undefined, hits: [] };
|
|
368
|
+
const obs = { path, holds: result.holds };
|
|
369
|
+
if (hit) {
|
|
370
|
+
obs.at = { file: hit.file, line: hit.line };
|
|
371
|
+
obs.typeMatch = 'template';
|
|
372
|
+
}
|
|
373
|
+
env.observed.push(obs);
|
|
374
|
+
}
|
|
375
|
+
else if (step.call) {
|
|
376
|
+
const hits = callHits(step, env);
|
|
377
|
+
result = { holds: hits.length > 0, hits };
|
|
378
|
+
const obs = { path, holds: result.holds };
|
|
379
|
+
if (hits[0]) {
|
|
380
|
+
obs.at = { file: env.ctx?.file ?? '', line: hits[0].call.location.line };
|
|
381
|
+
obs.typeMatch = hits[0].typeMatch;
|
|
382
|
+
}
|
|
383
|
+
env.observed.push(obs);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
// a step with only `order` outside an `all`, or `name`/`role` alone: nothing to observe
|
|
387
|
+
result = { holds: false, unobservable: 'step-not-observed:empty', hits: [] };
|
|
388
|
+
env.observed.push({ path, holds: false });
|
|
389
|
+
}
|
|
390
|
+
if (step.name)
|
|
391
|
+
env.names.set(step.name, result.hits);
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
function callHits(step, env) {
|
|
395
|
+
const ctx = env.ctx;
|
|
396
|
+
if (!ctx || !step.call)
|
|
397
|
+
return [];
|
|
398
|
+
const want = step.call;
|
|
399
|
+
const out = [];
|
|
400
|
+
for (const c of ctx.calls) {
|
|
401
|
+
const tm = shapeMatch(want, c, ctx.ir);
|
|
402
|
+
if (!tm)
|
|
403
|
+
continue;
|
|
404
|
+
// a sink-replacement is a call at the sink: it must have the shape of one of the row's sinks
|
|
405
|
+
if (step.role === 'sink-replacement' && !env.sinkCalls.includes(c))
|
|
406
|
+
continue;
|
|
407
|
+
if (step.argIs) {
|
|
408
|
+
let exprs = argsAt(c, want.arg ?? 0) ?? [];
|
|
409
|
+
if (exprs.length === 0)
|
|
410
|
+
continue;
|
|
411
|
+
if (step.argIs === 'from-allowlist') {
|
|
412
|
+
if (!allowlisted(ctx, c, exprs))
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
// `untainted` on an argument vector (array or list literal) reads its program element, element 0
|
|
417
|
+
if (step.argIs === 'untainted')
|
|
418
|
+
exprs = exprs.map(e => leadingElement(e) ?? e);
|
|
419
|
+
const flags = exprs.map(e => argTainted(ctx, c, e));
|
|
420
|
+
if (step.argIs === 'tainted' && !flags.some(Boolean))
|
|
421
|
+
continue;
|
|
422
|
+
if (step.argIs === 'untainted' && flags.some(Boolean))
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if ((step.role === 'guard' || step.on !== undefined) && !guards(ctx, c, step.on ?? true, env))
|
|
427
|
+
continue;
|
|
428
|
+
if (step.argLiteralIn) {
|
|
429
|
+
const lit = leadingLiteral(c, want.arg);
|
|
430
|
+
if (lit === null || !step.argLiteralIn.includes(lit))
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (step.role === 'sanitizer' && !env.sinkCalls.some(s => s !== c && reaches(ctx, c, s)))
|
|
434
|
+
continue;
|
|
435
|
+
out.push({ call: c, typeMatch: tm });
|
|
436
|
+
}
|
|
437
|
+
out.sort((a, b) => a.call.location.line - b.call.location.line || a.call.location.column - b.call.location.column);
|
|
438
|
+
return out;
|
|
439
|
+
}
|
|
440
|
+
/** Identifiers of an expression that name values, not called methods. */
|
|
441
|
+
function valueNames(expr) {
|
|
442
|
+
return identifiers(expr.replace(/[A-Za-z_$][\w$]*\s*\(/g, '('));
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* A local variable chosen from a finite set of literals: at least two defs in
|
|
446
|
+
* the declaration, and every def written with no call and no other variable on
|
|
447
|
+
* its line (a `switch` or `if` chain of `v = "literal";`, or a bare declaration).
|
|
448
|
+
*/
|
|
449
|
+
function literalChoice(ctx, name) {
|
|
450
|
+
const defs = ctx.defs.filter(d => d.variable === name);
|
|
451
|
+
if (defs.length < 2 || defs.some(d => d.kind !== 'local'))
|
|
452
|
+
return false;
|
|
453
|
+
return defs.every(d => !ctx.calls.some(c => c.location.line === d.line) &&
|
|
454
|
+
!ctx.ir.dfg.uses.some(u => u.line === d.line && u.variable !== name));
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* `arg-is: from-allowlist`: no argument is tainted, and at least one names a
|
|
458
|
+
* variable chosen from literals (`literalChoice`); the rest are literals or
|
|
459
|
+
* untainted values. A `contains` guard over a literal set is not read in this
|
|
460
|
+
* version (the switch and if-chain forms are).
|
|
461
|
+
*/
|
|
462
|
+
function allowlisted(ctx, c, exprs) {
|
|
463
|
+
if (exprs.some(e => argTainted(ctx, c, e)))
|
|
464
|
+
return false;
|
|
465
|
+
return exprs.some(e => [...valueNames(e)].some(n => literalChoice(ctx, n)));
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* `role: guard` with `on`: a branch whose condition makes this call decides
|
|
469
|
+
* which side the endpoint sinks run on, and on that side the call returned
|
|
470
|
+
* `on`; the condition's block dominates each of them in the CFG. The targets
|
|
471
|
+
* are the endpoint sinks with a tainted argument, else all endpoint sinks.
|
|
472
|
+
*/
|
|
473
|
+
function guards(ctx, c, on, env) {
|
|
474
|
+
const fact = ctx.branches?.find(f => f.calls.some(bc => bc.line === c.location.line && bc.method === c.method_name));
|
|
475
|
+
if (!fact)
|
|
476
|
+
return false;
|
|
477
|
+
const bc = fact.calls.find(x => x.line === c.location.line && x.method === c.method_name);
|
|
478
|
+
const tainted = env.endpointCalls.filter(t => t.arguments.some(a => argTainted(ctx, t, a.expression)));
|
|
479
|
+
const targets = tainted.length > 0 ? tainted : env.endpointCalls;
|
|
480
|
+
if (targets.length === 0)
|
|
481
|
+
return false;
|
|
482
|
+
return targets.every(t => {
|
|
483
|
+
if (t.location.line <= fact.line)
|
|
484
|
+
return false;
|
|
485
|
+
const side = sideOf(fact, t.location.line);
|
|
486
|
+
if (side === 'both')
|
|
487
|
+
return false;
|
|
488
|
+
return (side !== bc.negated) === on && lineDominates(ctx.ir, fact.line, t.location.line);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
function orderHolds(order, env) {
|
|
492
|
+
const ctx = env.ctx;
|
|
493
|
+
const a = env.names.get(order[0]) ?? [];
|
|
494
|
+
const b = env.names.get(order[1]) ?? [];
|
|
495
|
+
if (!ctx)
|
|
496
|
+
return false;
|
|
497
|
+
return a.some(x => b.some(y => x.call !== y.call && reaches(ctx, x.call, y.call)));
|
|
498
|
+
}
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
// Table sinks
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
function tableSinkOf(table, sink, ir) {
|
|
503
|
+
const atLine = ir.calls.filter(c => c.location.line === sink.line);
|
|
504
|
+
const direct = atLine.filter(c => !sink.call.method || c.method_name === sink.call.method);
|
|
505
|
+
// the call that made the sink's receiver (`new ProcessBuilder(..)` for `pb.start()`)
|
|
506
|
+
const viaReceiver = [];
|
|
507
|
+
for (const c of direct) {
|
|
508
|
+
if (!c.receiver || c.receiver.includes('('))
|
|
509
|
+
continue;
|
|
510
|
+
const use = ir.dfg.uses.find(u => u.line === c.location.line && u.variable === c.receiver && u.def_id !== null);
|
|
511
|
+
const def = use ? ir.dfg.defs.find(d => d.id === use.def_id) : undefined;
|
|
512
|
+
if (def)
|
|
513
|
+
viaReceiver.push(...ir.calls.filter(x => x.location.line === def.line && isCtor(x)));
|
|
514
|
+
}
|
|
515
|
+
let best;
|
|
516
|
+
for (const ts of table.sinks) {
|
|
517
|
+
if (!ts.call)
|
|
518
|
+
continue;
|
|
519
|
+
for (const [via, calls] of [['call', direct], ['receiver-def', viaReceiver]]) {
|
|
520
|
+
for (const c of calls) {
|
|
521
|
+
const tm = shapeMatch(ts.call, c, ir);
|
|
522
|
+
if (!tm)
|
|
523
|
+
continue;
|
|
524
|
+
if (!best || RANK[tm] < RANK[best.typeMatch])
|
|
525
|
+
best = { id: ts.id, typeMatch: tm, via };
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return best;
|
|
530
|
+
}
|
|
531
|
+
/** Does a template fact have the construct (and attribute value) a table entry names? */
|
|
532
|
+
function templateMatches(want, f) {
|
|
533
|
+
if (want.engine !== f.engine || want.construct !== f.construct)
|
|
534
|
+
return false;
|
|
535
|
+
if (want.attribute === undefined)
|
|
536
|
+
return true;
|
|
537
|
+
const actual = f.attributes[want.attribute];
|
|
538
|
+
if (want.value === undefined)
|
|
539
|
+
return actual !== undefined;
|
|
540
|
+
return want.value.includes(actual === undefined ? 'absent' : actual);
|
|
541
|
+
}
|
|
542
|
+
/** The template facts that render a name the sink call hands on. */
|
|
543
|
+
function linkedFacts(facts, names) {
|
|
544
|
+
if (!facts)
|
|
545
|
+
return null;
|
|
546
|
+
return Object.values(facts).flat().filter(f => f.names.some(n => names.includes(n)));
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* A sink that hands its value to a view takes its label from the table's
|
|
550
|
+
* template sinks: the construct that renders it at the baseline, else at the fix.
|
|
551
|
+
*/
|
|
552
|
+
function templateSinkOf(table, before, after) {
|
|
553
|
+
for (const facts of [before, after]) {
|
|
554
|
+
for (const ts of table.sinks) {
|
|
555
|
+
if (ts.template && (facts ?? []).some(f => templateMatches(ts.template, f)))
|
|
556
|
+
return ts.id;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Observe, for every sink of a path `diff` reports `removed` or `sanitized`,
|
|
563
|
+
* which row of `table` the fix version matches.
|
|
564
|
+
*
|
|
565
|
+
* `table` is the reader's result; a refused table throws `TableRefused`.
|
|
566
|
+
*/
|
|
567
|
+
export function remedyClassOf(diff, before, after, loaded, options = {}) {
|
|
568
|
+
if (!loaded.ok)
|
|
569
|
+
throw new TableRefused(loaded.digest, loaded.problems);
|
|
570
|
+
const table = loaded.table;
|
|
571
|
+
if (`CWE-${table.cwe}` !== diff.cwe)
|
|
572
|
+
throw new Error(`remedy-class: table is CWE-${table.cwe}, path set is ${diff.cwe}`);
|
|
573
|
+
const tableInfo = { format: table.format, cwe: table.cwe, language: table.language, version: table.version, digest: loaded.digest };
|
|
574
|
+
const irBefore = new Map(before.files.map(f => [f.file, f.analysis]));
|
|
575
|
+
const irAfter = new Map(after.files.map(f => [f.file, f.analysis]));
|
|
576
|
+
const rows = table.constructs.filter(r => r.retired === undefined);
|
|
577
|
+
const sinks = [];
|
|
578
|
+
for (const e of diff.entries) {
|
|
579
|
+
// a path still open in code whose view now renders it differently was fixed in the view:
|
|
580
|
+
// observed in both scopes, with `templateChanged`
|
|
581
|
+
const observed = e.change === 'removed' || e.change === 'sanitized' || e.change === 'guarded'
|
|
582
|
+
|| (e.change === 'unchanged' && (options.scope === 'all' || e.templateChanged === true));
|
|
583
|
+
if (!observed || !e.before)
|
|
584
|
+
continue;
|
|
585
|
+
const base = e.before;
|
|
586
|
+
const seedParameters = base.source.decl.id !== base.sink.decl.id;
|
|
587
|
+
const obs = {
|
|
588
|
+
anchor: `${e.key}:sink`,
|
|
589
|
+
pathKey: e.key,
|
|
590
|
+
change: e.change,
|
|
591
|
+
...(e.templateChanged ? { templateChanged: true } : {}),
|
|
592
|
+
sink: { tableSink: 'unlisted', file: base.sink.file, line: base.sink.line, decl: base.sink.decl },
|
|
593
|
+
match: 'unrecognized',
|
|
594
|
+
rows: [],
|
|
595
|
+
abstainedRows: [],
|
|
596
|
+
taintSeeds: seedParameters ? 'sources+parameters' : 'sources',
|
|
597
|
+
table: tableInfo,
|
|
598
|
+
};
|
|
599
|
+
sinks.push(obs);
|
|
600
|
+
const beforeIr = irBefore.get(base.sink.file);
|
|
601
|
+
const sinkCall = beforeIr?.calls.find(c => c.location.line === base.sink.line && (!base.sink.call.method || c.method_name === base.sink.call.method));
|
|
602
|
+
const handed = handedNames(sinkCall);
|
|
603
|
+
const factsBefore = handed.length > 0 ? linkedFacts(options.templates?.before, handed) : null;
|
|
604
|
+
const factsAfter = handed.length > 0 ? linkedFacts(options.templates?.after, handed) : null;
|
|
605
|
+
let label = beforeIr ? tableSinkOf(table, base.sink, beforeIr) : undefined;
|
|
606
|
+
if (!label) {
|
|
607
|
+
const id = templateSinkOf(table, factsBefore, factsAfter);
|
|
608
|
+
if (id)
|
|
609
|
+
label = { id, typeMatch: 'template', via: 'template' };
|
|
610
|
+
}
|
|
611
|
+
if (!label) {
|
|
612
|
+
obs.reason = 'sink-not-in-table';
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
obs.sink.tableSink = label.id;
|
|
616
|
+
obs.sink.tableSinkMatch = label.typeMatch;
|
|
617
|
+
obs.sink.via = label.via;
|
|
618
|
+
const afterIr = irAfter.get(base.sink.file);
|
|
619
|
+
const ids = options.stableIds?.after?.[base.sink.file];
|
|
620
|
+
const ctx = afterIr ? buildCtx(base.sink.file, afterIr, base.sink.decl, ids, seedParameters) : undefined;
|
|
621
|
+
const br = options.branches?.after?.[base.sink.file];
|
|
622
|
+
if (ctx && br)
|
|
623
|
+
ctx.branches = br;
|
|
624
|
+
for (const row of rows) {
|
|
625
|
+
if (!row.appliesTo.includes(label.id))
|
|
626
|
+
continue;
|
|
627
|
+
const rowSinks = table.sinks.filter(s => row.appliesTo.includes(s.id) && s.call);
|
|
628
|
+
const sinkCalls = ctx ? ctx.calls.filter(c => rowSinks.some(s => shapeMatch(s.call, c, ctx.ir))) : [];
|
|
629
|
+
const endpointCalls = ctx ? sinkCalls.filter(c => !sinkCalls.some(o => o !== c && reaches(ctx, c, o))) : [];
|
|
630
|
+
const env = { ctx, sinkCalls, endpointCalls, pathRemoved: e.change === 'removed', names: new Map(), observed: [], templateFacts: options.templates ? (factsAfter ?? []) : null };
|
|
631
|
+
const r = evalStep(row.match, '/match', env);
|
|
632
|
+
if (r.holds)
|
|
633
|
+
obs.rows.push(matchedRow(row, env.observed));
|
|
634
|
+
else if (r.unobservable)
|
|
635
|
+
obs.abstainedRows.push({ id: row.id, reason: r.unobservable });
|
|
636
|
+
}
|
|
637
|
+
if (options.outputContext && ctx) {
|
|
638
|
+
const labelled = table.sinks.find(x => x.id === label?.id);
|
|
639
|
+
const oc = labelled?.call ? outputContextOf(ctx, labelled.call) : 'unknown';
|
|
640
|
+
obs.sink.outputContext = oc;
|
|
641
|
+
obs.contextExcludedRows = [];
|
|
642
|
+
if (oc !== 'unknown') {
|
|
643
|
+
const keep = [];
|
|
644
|
+
for (const r of obs.rows) {
|
|
645
|
+
if (r.context !== undefined && r.context !== oc)
|
|
646
|
+
obs.contextExcludedRows.push({ id: r.id, context: r.context });
|
|
647
|
+
else
|
|
648
|
+
keep.push(r);
|
|
649
|
+
}
|
|
650
|
+
obs.rows = keep;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (obs.rows.length === 1) {
|
|
654
|
+
obs.match = obs.rows[0].id;
|
|
655
|
+
obs.class = obs.rows[0].class;
|
|
656
|
+
}
|
|
657
|
+
else if (obs.rows.length > 1) {
|
|
658
|
+
obs.match = 'multiple';
|
|
659
|
+
}
|
|
660
|
+
else if (obs.abstainedRows.length > 0) {
|
|
661
|
+
obs.match = 'abstained';
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
obs.reason = ctx ? 'no-row-holds' : 'declaration-not-found';
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
sinks.sort((a, b) => (a.anchor < b.anchor ? -1 : a.anchor > b.anchor ? 1 : 0));
|
|
668
|
+
return { table: tableInfo, sinks };
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* The output context at the fix version's writer sinks of this shape: the
|
|
672
|
+
* markup written before the first tainted value in each call's argument,
|
|
673
|
+
* after any literal-only writes to the same writer earlier in the declaration.
|
|
674
|
+
* One context across the calls, else `unknown`.
|
|
675
|
+
*/
|
|
676
|
+
function outputContextOf(ctx, sinkShape) {
|
|
677
|
+
const names = new Set(ctx.defs.filter(d => ctx.tainted.has(d.id)).map(d => d.variable));
|
|
678
|
+
const calls = ctx.calls
|
|
679
|
+
.filter(c => shapeMatch(sinkShape, c, ctx.ir) !== null)
|
|
680
|
+
.sort((a, b) => a.location.line - b.location.line || a.location.column - b.location.column);
|
|
681
|
+
const seen = new Set();
|
|
682
|
+
for (const c of calls) {
|
|
683
|
+
const expr = c.arguments[0]?.expression;
|
|
684
|
+
if (!expr)
|
|
685
|
+
continue;
|
|
686
|
+
const local = markupBefore(expr, names);
|
|
687
|
+
if (local === null)
|
|
688
|
+
continue;
|
|
689
|
+
const earlier = calls
|
|
690
|
+
.filter(o => o !== c && o.receiver === c.receiver &&
|
|
691
|
+
(o.location.line < c.location.line || (o.location.line === c.location.line && o.location.column < c.location.column)))
|
|
692
|
+
.map(o => o.arguments[0]?.expression ?? '')
|
|
693
|
+
.filter(x => x.trim().startsWith('"') && markupBefore(x, names) === null && !/[A-Za-z_$]\s*\(/.test(x.replace(/"(?:[^"\\]|\\.)*"/g, '')))
|
|
694
|
+
.map(x => javaStringLiterals(x).join(''));
|
|
695
|
+
seen.add(htmlContextAt(earlier.join('') + local));
|
|
696
|
+
}
|
|
697
|
+
return seen.size === 1 ? [...seen][0] : 'unknown';
|
|
698
|
+
}
|
|
699
|
+
function matchedRow(row, observed) {
|
|
700
|
+
const m = { id: row.id, class: row.class, remedyClass: row.remedyClass, steps: [...observed].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) };
|
|
701
|
+
if (row.context !== undefined)
|
|
702
|
+
m.context = row.context;
|
|
703
|
+
return m;
|
|
704
|
+
}
|
|
705
|
+
//# sourceMappingURL=match.js.map
|