circle-ir 4.17.0 → 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/propagator.d.ts +4 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +99 -0
- package/dist/analysis/constant-propagation/propagator.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 +81 -1
- 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 +1057 -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 +66 -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 +133 -10
- 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/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 +225 -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 +2936 -291
- package/dist/core/circle-ir-core.cjs +634 -203
- package/dist/core/circle-ir-core.js +634 -203
- 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/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 +51 -1
- package/dist/navigation/navigation-index.d.ts.map +1 -1
- package/dist/navigation/navigation-index.js +72 -0
- 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 +9 -0
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +60 -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 +35 -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,716 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path sets of one weakness class, and their difference between two versions
|
|
3
|
+
* of a tree.
|
|
4
|
+
*
|
|
5
|
+
* `pathSetOf` collects every source-to-sink path whose sink carries the given
|
|
6
|
+
* CWE from an already analysed project (per-file `taint.flows` and project
|
|
7
|
+
* `taint_paths`) and gives each a **pin-independent key**: no line number
|
|
8
|
+
* takes part, so an edit that only shifts lines keeps every key. The key is
|
|
9
|
+
* built from the declaration that holds each end (a stable id where the
|
|
10
|
+
* caller supplies `ids@0.1` for the file, else a qualified name built from the
|
|
11
|
+
* IR), the call shape of the end, and its ordinal among same-shape ends in
|
|
12
|
+
* that declaration.
|
|
13
|
+
*
|
|
14
|
+
* `diffPathSets` compares two sets as a set operation and counts, per version,
|
|
15
|
+
* the unresolved edges and the analysis budgets hit, so an empty set on a
|
|
16
|
+
* partly analysed tree is never read as clean. Neither function judges a
|
|
17
|
+
* change: they report which paths exist where.
|
|
18
|
+
*
|
|
19
|
+
* Pure: IR in, values out. Browser and Node compatible.
|
|
20
|
+
*/
|
|
21
|
+
import { DEFAULT_SINKS } from '../analysis/config-loader.js';
|
|
22
|
+
import { DominatorGraph } from '../graph/dominator-graph.js';
|
|
23
|
+
import { digest128 } from '../stable-ids/hash.js';
|
|
24
|
+
import { sideOf } from './branches.js';
|
|
25
|
+
import { PATH_KEY_SCHEME, } from './types.js';
|
|
26
|
+
/** `CWE-089`, `cwe-89` and `89` all normalise to `CWE-89`. */
|
|
27
|
+
export function normalizeCwe(cwe) {
|
|
28
|
+
const m = /^(?:cwe-)?0*(\d+)$/i.exec(String(cwe).trim());
|
|
29
|
+
if (!m)
|
|
30
|
+
throw new Error(`path-set: not a CWE id: ${JSON.stringify(cwe)}`);
|
|
31
|
+
return `CWE-${m[1]}`;
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Declarations
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
/** `package.Type$Inner` — the qualified@0.1 name of a type. */
|
|
37
|
+
export function typePath(t) {
|
|
38
|
+
const name = t.enclosing_type ? `${t.enclosing_type}$${t.name}` : t.name;
|
|
39
|
+
return t.package ? `${t.package}.${name}` : name;
|
|
40
|
+
}
|
|
41
|
+
/** `package.Type$Inner.method(T1,T2)` — the qualified@0.1 name of a method. */
|
|
42
|
+
export function qualifiedMethodName(type, method) {
|
|
43
|
+
return `${typePath(type)}.${method.name}(${method.parameters.map(p => p.type ?? '?').join(',')})`;
|
|
44
|
+
}
|
|
45
|
+
function span(start, end) {
|
|
46
|
+
return end - start;
|
|
47
|
+
}
|
|
48
|
+
/** The declaration that holds `line` in `file`. */
|
|
49
|
+
export function declAt(file, ir, line, ids) {
|
|
50
|
+
if (ids) {
|
|
51
|
+
let best;
|
|
52
|
+
for (const d of ids.decls) {
|
|
53
|
+
if (d.kind !== 'function' && d.kind !== 'method')
|
|
54
|
+
continue;
|
|
55
|
+
if (line < d.start_line || line > d.end_line)
|
|
56
|
+
continue;
|
|
57
|
+
if (!best || span(d.start_line, d.end_line) < span(best.start_line, best.end_line))
|
|
58
|
+
best = d;
|
|
59
|
+
}
|
|
60
|
+
best ??= ids.decls[0];
|
|
61
|
+
if (best)
|
|
62
|
+
return { scheme: 'ids@0.1', id: best.id, name: best.qualified_name };
|
|
63
|
+
}
|
|
64
|
+
let type;
|
|
65
|
+
for (const t of ir.types) {
|
|
66
|
+
if (line < t.start_line || line > t.end_line)
|
|
67
|
+
continue;
|
|
68
|
+
if (!type || span(t.start_line, t.end_line) < span(type.start_line, type.end_line))
|
|
69
|
+
type = t;
|
|
70
|
+
}
|
|
71
|
+
let qualified;
|
|
72
|
+
if (type) {
|
|
73
|
+
const method = type.methods
|
|
74
|
+
.filter(m => line >= m.start_line && line <= m.end_line)
|
|
75
|
+
.sort((a, b) => span(a.start_line, a.end_line) - span(b.start_line, b.end_line))[0];
|
|
76
|
+
qualified = method ? qualifiedMethodName(type, method) : `${typePath(type)}.<init>`;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// No type holds the line (top-level function in a language whose IR keeps
|
|
80
|
+
// functions outside types): fall back to the enclosing method name the
|
|
81
|
+
// call extractor recorded, else the module.
|
|
82
|
+
const inMethod = ir.calls.find(c => c.location.line === line && c.in_method)?.in_method;
|
|
83
|
+
qualified = inMethod ? `<module>.${inMethod}` : '<module>';
|
|
84
|
+
}
|
|
85
|
+
const id = `${file}#${qualified}`;
|
|
86
|
+
return { scheme: 'qualified@0.1', id, name: qualified };
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Anchors
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
/** `req.getParameter() in find` → `getParameter`. */
|
|
92
|
+
function callNameFromLocation(location) {
|
|
93
|
+
const head = location.split(' in ')[0].trim().replace(/\(\)$/, '');
|
|
94
|
+
const dot = head.lastIndexOf('.');
|
|
95
|
+
return dot >= 0 ? head.slice(dot + 1) : head;
|
|
96
|
+
}
|
|
97
|
+
function sourceShape(s) {
|
|
98
|
+
const what = s.method ?? (s.annotation ? `@${s.annotation}` : callNameFromLocation(s.location));
|
|
99
|
+
return `${s.type}:${what}`;
|
|
100
|
+
}
|
|
101
|
+
function sinkShape(s) {
|
|
102
|
+
return `${s.type}:${s.class ?? '?'}.${s.method ?? '?'}(${(s.argPositions ?? []).join(',')})`;
|
|
103
|
+
}
|
|
104
|
+
/** Ordinal of `item` among items of the same shape in the same declaration, by position. */
|
|
105
|
+
function ordinalOf(item, all, shape, declId, column) {
|
|
106
|
+
const myShape = shape(item);
|
|
107
|
+
const myDecl = declId(item);
|
|
108
|
+
const peers = all
|
|
109
|
+
.filter(x => shape(x) === myShape && declId(x) === myDecl)
|
|
110
|
+
.sort((a, b) => a.line - b.line || column(a) - column(b));
|
|
111
|
+
return peers.indexOf(item);
|
|
112
|
+
}
|
|
113
|
+
function sourceEnd(ctx, line, type) {
|
|
114
|
+
const src = ctx.ir.taint.sources.find(s => s.line === line && s.type === type);
|
|
115
|
+
if (!src)
|
|
116
|
+
return undefined;
|
|
117
|
+
const decl = declAt(ctx.file, ctx.ir, line, ctx.ids);
|
|
118
|
+
const declIdOf = (s) => declAt(ctx.file, ctx.ir, s.line, ctx.ids).id;
|
|
119
|
+
const ordinal = ordinalOf(src, ctx.ir.taint.sources, sourceShape, declIdOf, () => 0);
|
|
120
|
+
const end = {
|
|
121
|
+
anchor: `${decl.scheme}:${decl.id}|${sourceShape(src)}|#${ordinal}`,
|
|
122
|
+
decl,
|
|
123
|
+
file: ctx.file,
|
|
124
|
+
line,
|
|
125
|
+
type: src.type,
|
|
126
|
+
};
|
|
127
|
+
if (src.code !== undefined)
|
|
128
|
+
end.code = src.code;
|
|
129
|
+
return end;
|
|
130
|
+
}
|
|
131
|
+
/** Simple-name class of a call's receiver, or of a static receiver written as a type. */
|
|
132
|
+
function callClass(c) {
|
|
133
|
+
if (c.receiver_type)
|
|
134
|
+
return c.receiver_type.slice(c.receiver_type.lastIndexOf('.') + 1);
|
|
135
|
+
const r = c.receiver;
|
|
136
|
+
if (!r || r.includes('('))
|
|
137
|
+
return null;
|
|
138
|
+
const last = r.slice(r.lastIndexOf('.') + 1);
|
|
139
|
+
return /^[A-Z]/.test(last) ? last : null;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Ordinal of a sink call among the calls of the same shape (class, method) in
|
|
143
|
+
* its declaration, by position. Counted over calls, not over the engine's
|
|
144
|
+
* sink records, so a call the engine stops flagging at one version does not
|
|
145
|
+
* shift the ordinals of the others.
|
|
146
|
+
*/
|
|
147
|
+
function callOrdinal(ctx, declId, cls, method, line, column) {
|
|
148
|
+
if (!method)
|
|
149
|
+
return null;
|
|
150
|
+
const same = (c) => c.method_name === method && (cls === undefined || callClass(c) === cls);
|
|
151
|
+
const peers = ctx.ir.calls
|
|
152
|
+
.filter(c => same(c) && declAt(ctx.file, ctx.ir, c.location.line, ctx.ids).id === declId)
|
|
153
|
+
.sort((a, b) => a.location.line - b.location.line || a.location.column - b.location.column);
|
|
154
|
+
const onLine = peers.filter(c => c.location.line === line);
|
|
155
|
+
if (onLine.length === 0)
|
|
156
|
+
return null;
|
|
157
|
+
const mine = column === undefined
|
|
158
|
+
? onLine[0]
|
|
159
|
+
: onLine.reduce((best, c) => (Math.abs(c.location.column - column) < Math.abs(best.location.column - column) ? c : best));
|
|
160
|
+
return peers.indexOf(mine);
|
|
161
|
+
}
|
|
162
|
+
function sinkEnd(ctx, line, type) {
|
|
163
|
+
const candidates = ctx.ir.taint.sinks
|
|
164
|
+
.filter(s => s.line === line && s.type === type)
|
|
165
|
+
.sort((a, b) => (a.callColumn ?? 0) - (b.callColumn ?? 0));
|
|
166
|
+
const sink = candidates[0];
|
|
167
|
+
if (!sink)
|
|
168
|
+
return undefined;
|
|
169
|
+
const decl = declAt(ctx.file, ctx.ir, line, ctx.ids);
|
|
170
|
+
const declIdOf = (s) => declAt(ctx.file, ctx.ir, s.line, ctx.ids).id;
|
|
171
|
+
const ordinal = callOrdinal(ctx, decl.id, sink.class, sink.method, line, sink.callColumn)
|
|
172
|
+
?? ordinalOf(sink, ctx.ir.taint.sinks, sinkShape, declIdOf, s => s.callColumn ?? 0);
|
|
173
|
+
const call = {};
|
|
174
|
+
if (sink.class !== undefined)
|
|
175
|
+
call.class = sink.class;
|
|
176
|
+
if (sink.method !== undefined)
|
|
177
|
+
call.method = sink.method;
|
|
178
|
+
if (sink.argPositions !== undefined)
|
|
179
|
+
call.argPositions = sink.argPositions;
|
|
180
|
+
const end = {
|
|
181
|
+
anchor: `${decl.scheme}:${decl.id}|${sinkShape(sink)}|#${ordinal}`,
|
|
182
|
+
decl,
|
|
183
|
+
file: ctx.file,
|
|
184
|
+
line,
|
|
185
|
+
type: sink.type,
|
|
186
|
+
cwe: normalizeCwe(sink.cwe),
|
|
187
|
+
call,
|
|
188
|
+
};
|
|
189
|
+
if (sink.code !== undefined)
|
|
190
|
+
end.code = sink.code;
|
|
191
|
+
return end;
|
|
192
|
+
}
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Endpoints: a sink whose result feeds another sink of the class
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
const IDENT = /[A-Za-z_$][\w$]*/g;
|
|
197
|
+
const STRING_LITERAL = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g;
|
|
198
|
+
function identifiers(expr) {
|
|
199
|
+
return new Set((expr ?? '').replace(STRING_LITERAL, '""').match(IDENT) ?? []);
|
|
200
|
+
}
|
|
201
|
+
function operands(c) {
|
|
202
|
+
return [c.receiver ?? '', ...c.arguments.map(a => a.expression)].join(' ');
|
|
203
|
+
}
|
|
204
|
+
/** The engine's own registered sink this call has the shape of, for the CWE. */
|
|
205
|
+
function registeredSink(c, cwe, language) {
|
|
206
|
+
const cls = callClass(c);
|
|
207
|
+
return DEFAULT_SINKS.find(p => normalizeCwe(p.cwe) === cwe &&
|
|
208
|
+
p.method === c.method_name &&
|
|
209
|
+
(p.languages === undefined || p.languages.includes(language)) &&
|
|
210
|
+
!p.exclude_languages?.includes(language) &&
|
|
211
|
+
(p.class === undefined || p.class === cls || (cls === null && p.allow_unresolved_receiver === true)));
|
|
212
|
+
}
|
|
213
|
+
function sinkCallOf(ctx, sink) {
|
|
214
|
+
return ctx.ir.calls
|
|
215
|
+
.filter(c => c.location.line === sink.line && (!sink.call.method || c.method_name === sink.call.method))
|
|
216
|
+
.sort((a, b) => a.location.column - b.location.column)[0];
|
|
217
|
+
}
|
|
218
|
+
/** Def ids reached (via chains) from the defs written on the call's line. */
|
|
219
|
+
function reachedFrom(ctx, line) {
|
|
220
|
+
const next = new Map();
|
|
221
|
+
for (const ch of ctx.ir.dfg.chains ?? []) {
|
|
222
|
+
const l = next.get(ch.from_def);
|
|
223
|
+
if (l)
|
|
224
|
+
l.push(ch.to_def);
|
|
225
|
+
else
|
|
226
|
+
next.set(ch.from_def, [ch.to_def]);
|
|
227
|
+
}
|
|
228
|
+
const seen = new Set();
|
|
229
|
+
const stack = ctx.ir.dfg.defs.filter(d => d.line === line && d.kind !== 'param').map(d => d.id);
|
|
230
|
+
while (stack.length) {
|
|
231
|
+
const d = stack.pop();
|
|
232
|
+
if (seen.has(d))
|
|
233
|
+
continue;
|
|
234
|
+
seen.add(d);
|
|
235
|
+
stack.push(...(next.get(d) ?? []));
|
|
236
|
+
}
|
|
237
|
+
return seen;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Follow a flagged sink to the endpoint its result feeds: an enclosing call on
|
|
241
|
+
* the same line, or a later call in the same declaration that uses a value
|
|
242
|
+
* derived from it, when that call is a registered sink of the class
|
|
243
|
+
* (`Path.resolve(name)` → `Files.readAllBytes(real)`). Returns the sink as is
|
|
244
|
+
* when nothing further is reached.
|
|
245
|
+
*/
|
|
246
|
+
function endpointOf(ctx, sink) {
|
|
247
|
+
const language = ctx.ir.meta.language;
|
|
248
|
+
let cur = sinkCallOf(ctx, sink);
|
|
249
|
+
if (!cur)
|
|
250
|
+
return sink;
|
|
251
|
+
const first = cur;
|
|
252
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
253
|
+
const from = cur;
|
|
254
|
+
const declId = declAt(ctx.file, ctx.ir, from.location.line, ctx.ids).id;
|
|
255
|
+
const onLine = ctx.ir.calls.filter(c => c.location.line === from.location.line).filter(c => c !== from && registeredSink(c, sink.cwe, language));
|
|
256
|
+
const enclosing = onLine.find(c => operands(c).includes(`${from.method_name}(`));
|
|
257
|
+
let next = enclosing;
|
|
258
|
+
if (!next) {
|
|
259
|
+
const reached = reachedFrom(ctx, from.location.line);
|
|
260
|
+
const names = new Set(ctx.ir.dfg.defs.filter(d => reached.has(d.id)).map(d => d.variable));
|
|
261
|
+
next = ctx.ir.calls
|
|
262
|
+
.filter(c => c.location.line > from.location.line && registeredSink(c, sink.cwe, language))
|
|
263
|
+
.filter(c => declAt(ctx.file, ctx.ir, c.location.line, ctx.ids).id === declId)
|
|
264
|
+
.filter(c => {
|
|
265
|
+
const ids = identifiers(operands(c));
|
|
266
|
+
return ctx.ir.dfg.uses.some(u => u.line === c.location.line && u.def_id !== null && reached.has(u.def_id) && ids.has(u.variable))
|
|
267
|
+
|| [...names].some(n => ids.has(n) && ctx.ir.dfg.uses.some(u => u.line === c.location.line && u.variable === n));
|
|
268
|
+
})
|
|
269
|
+
.sort((a, b) => a.location.line - b.location.line || a.location.column - b.location.column)[0];
|
|
270
|
+
}
|
|
271
|
+
if (!next)
|
|
272
|
+
break;
|
|
273
|
+
cur = next;
|
|
274
|
+
}
|
|
275
|
+
if (cur === first)
|
|
276
|
+
return sink;
|
|
277
|
+
const reg = registeredSink(cur, sink.cwe, language);
|
|
278
|
+
/* c8 ignore next */
|
|
279
|
+
if (!reg)
|
|
280
|
+
return sink;
|
|
281
|
+
const decl = declAt(ctx.file, ctx.ir, cur.location.line, ctx.ids);
|
|
282
|
+
const cls = reg.class ?? callClass(cur) ?? undefined;
|
|
283
|
+
const ordinal = callOrdinal(ctx, decl.id, reg.class, cur.method_name, cur.location.line, cur.location.column) ?? 0;
|
|
284
|
+
const shape = `${reg.type}:${cls ?? '?'}.${cur.method_name}(${reg.arg_positions.join(',')})`;
|
|
285
|
+
const endpoint = {
|
|
286
|
+
anchor: `${decl.scheme}:${decl.id}|${shape}|#${ordinal}`,
|
|
287
|
+
decl,
|
|
288
|
+
file: ctx.file,
|
|
289
|
+
line: cur.location.line,
|
|
290
|
+
type: reg.type,
|
|
291
|
+
cwe: sink.cwe,
|
|
292
|
+
call: { ...(cls !== undefined ? { class: cls } : {}), method: cur.method_name, argPositions: reg.arg_positions },
|
|
293
|
+
intermediate: { line: sink.line, call: sink.call, ...(sink.code !== undefined ? { code: sink.code } : {}) },
|
|
294
|
+
};
|
|
295
|
+
return endpoint;
|
|
296
|
+
}
|
|
297
|
+
/** Names a sink call hands on as string literals (`addAttribute("q", q)` → `q`). */
|
|
298
|
+
export function handedNames(c) {
|
|
299
|
+
if (!c)
|
|
300
|
+
return [];
|
|
301
|
+
return c.arguments.map(a => a.literal).filter((x) => typeof x === 'string' && /^[A-Za-z_][\w]*$/.test(x));
|
|
302
|
+
}
|
|
303
|
+
function templateUsesOf(ctx, sink, facts) {
|
|
304
|
+
if (facts.length === 0)
|
|
305
|
+
return [];
|
|
306
|
+
const names = handedNames(sinkCallOf(ctx, sink));
|
|
307
|
+
if (names.length === 0)
|
|
308
|
+
return [];
|
|
309
|
+
return facts
|
|
310
|
+
.filter(f => f.names.some(n => names.includes(n)))
|
|
311
|
+
.map(f => {
|
|
312
|
+
const u = { file: f.file, line: f.line, engine: f.engine, construct: f.construct, attributes: f.attributes, names: f.names.filter(n => names.includes(n)) };
|
|
313
|
+
if (f.element !== undefined)
|
|
314
|
+
u.element = f.element;
|
|
315
|
+
if (f.inAttribute !== undefined)
|
|
316
|
+
u.inAttribute = f.inAttribute;
|
|
317
|
+
return u;
|
|
318
|
+
})
|
|
319
|
+
.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line || a.construct.localeCompare(b.construct)));
|
|
320
|
+
}
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// Guards: the endpoint runs on one side of a check of the path's own data
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
function methodRange(ir, line) {
|
|
325
|
+
let best;
|
|
326
|
+
for (const t of ir.types) {
|
|
327
|
+
for (const m of t.methods) {
|
|
328
|
+
if (line < m.start_line || line > m.end_line)
|
|
329
|
+
continue;
|
|
330
|
+
if (!best || m.end_line - m.start_line < best.end - best.start)
|
|
331
|
+
best = { start: m.start_line, end: m.end_line };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return best;
|
|
335
|
+
}
|
|
336
|
+
/** Does the CFG block holding line `a` dominate the block holding line `b` (same method)? */
|
|
337
|
+
export function lineDominates(ir, a, b) {
|
|
338
|
+
const range = methodRange(ir, b);
|
|
339
|
+
const blocks = ir.cfg.blocks.filter(x => !range || (x.start_line >= range.start && x.end_line <= range.end));
|
|
340
|
+
const holding = (line) => blocks.find(x => x.start_line <= line && line <= x.end_line);
|
|
341
|
+
const ba = holding(a);
|
|
342
|
+
const bb = holding(b);
|
|
343
|
+
if (!ba || !bb)
|
|
344
|
+
return false;
|
|
345
|
+
const entry = blocks.find(x => x.type === 'entry') ?? blocks.slice().sort((x, y) => x.id - y.id)[0];
|
|
346
|
+
return new DominatorGraph(ir.cfg, entry.id).dominates(ba.id, bb.id);
|
|
347
|
+
}
|
|
348
|
+
function guardOf(ctx, sink, pathVars) {
|
|
349
|
+
if (!ctx.branches)
|
|
350
|
+
return undefined;
|
|
351
|
+
const call = sinkCallOf(ctx, sink);
|
|
352
|
+
const vars = new Set([...pathVars, ...identifiers(call ? operands(call) : sink.code)]);
|
|
353
|
+
const declId = sink.decl.id;
|
|
354
|
+
for (const fact of ctx.branches) {
|
|
355
|
+
if (fact.line >= sink.line)
|
|
356
|
+
continue;
|
|
357
|
+
if (declAt(ctx.file, ctx.ir, fact.line, ctx.ids).id !== declId)
|
|
358
|
+
continue;
|
|
359
|
+
const side = sideOf(fact, sink.line);
|
|
360
|
+
if (side === 'both')
|
|
361
|
+
continue;
|
|
362
|
+
for (const bc of fact.calls) {
|
|
363
|
+
const ir = ctx.ir.calls.find(c => c.location.line === bc.line && c.method_name === bc.method);
|
|
364
|
+
if (!ir || ![...identifiers(operands(ir))].some(n => vars.has(n)))
|
|
365
|
+
continue;
|
|
366
|
+
if (!lineDominates(ctx.ir, fact.line, sink.line))
|
|
367
|
+
continue;
|
|
368
|
+
return { file: ctx.file, line: fact.line, method: bc.method, negated: bc.negated, sinkSide: side, callValue: side !== bc.negated };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
/** The pin-independent key of a path. */
|
|
374
|
+
export function pathKey(cwe, sourceAnchor, sinkAnchor) {
|
|
375
|
+
return digest128([PATH_KEY_SCHEME, cwe, sourceAnchor, sinkAnchor]);
|
|
376
|
+
}
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// Unresolved edges on a path
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
function methodSpan(ir, line) {
|
|
381
|
+
let best;
|
|
382
|
+
for (const t of ir.types) {
|
|
383
|
+
for (const m of t.methods) {
|
|
384
|
+
if (line < m.start_line || line > m.end_line)
|
|
385
|
+
continue;
|
|
386
|
+
if (!best || m.end_line - m.start_line < best.end - best.start)
|
|
387
|
+
best = { start: m.start_line, end: m.end_line };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return best;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* The lines a path occupies, per file: source, sink (and intermediate sink)
|
|
394
|
+
* and hop lines, plus the lines in the source's and sink's declarations that
|
|
395
|
+
* use one of the path's variables.
|
|
396
|
+
*/
|
|
397
|
+
function pathLines(ctxByFile, v) {
|
|
398
|
+
const out = new Map();
|
|
399
|
+
const add = (file, line) => {
|
|
400
|
+
let s = out.get(file);
|
|
401
|
+
if (!s)
|
|
402
|
+
out.set(file, (s = new Set()));
|
|
403
|
+
s.add(line);
|
|
404
|
+
};
|
|
405
|
+
add(v.source.file, v.source.line);
|
|
406
|
+
add(v.sink.file, v.sink.line);
|
|
407
|
+
if (v.sink.intermediate)
|
|
408
|
+
add(v.sink.file, v.sink.intermediate.line);
|
|
409
|
+
for (const h of v.hops)
|
|
410
|
+
add(h.file, h.line);
|
|
411
|
+
const vars = new Set(v.hops.map(h => h.variable).filter((x) => !!x));
|
|
412
|
+
for (const end of [v.source, v.sink]) {
|
|
413
|
+
const ctx = ctxByFile.get(end.file);
|
|
414
|
+
if (!ctx || vars.size === 0)
|
|
415
|
+
continue;
|
|
416
|
+
const span = methodSpan(ctx.ir, end.line);
|
|
417
|
+
if (!span)
|
|
418
|
+
continue;
|
|
419
|
+
for (const u of ctx.ir.dfg.uses) {
|
|
420
|
+
if (u.line >= span.start && u.line <= span.end && vars.has(u.variable))
|
|
421
|
+
add(end.file, u.line);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
function unresolvedOn(ctxByFile, projectTypes, crossFile, lines, sink) {
|
|
427
|
+
const sites = [];
|
|
428
|
+
const inProject = (cands) => (cands ?? []).some(c => projectTypes.has(c.split('.').slice(0, -1).pop() ?? c));
|
|
429
|
+
for (const [file, set] of lines) {
|
|
430
|
+
const ctx = ctxByFile.get(file);
|
|
431
|
+
if (!ctx)
|
|
432
|
+
continue;
|
|
433
|
+
for (const u of ctx.ir.unresolved) {
|
|
434
|
+
if (!set.has(u.context.line))
|
|
435
|
+
continue;
|
|
436
|
+
// the unresolved call is the sink call itself: same line, and its candidate (or its text) names the sink's method
|
|
437
|
+
const methods = (u.context.candidates ?? []).map(c => c.split('.').pop() ?? c);
|
|
438
|
+
const atSink = file === sink.file && u.context.line === sink.line && !!sink.call.method &&
|
|
439
|
+
(methods.length > 0 ? methods.includes(sink.call.method) : u.context.code.includes(`${sink.call.method}(`));
|
|
440
|
+
sites.push({ file, line: u.context.line, type: u.type, reason: u.reason, code: u.context.code, atSink, inProject: inProject(u.context.candidates) });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
for (const c of crossFile ?? []) {
|
|
444
|
+
if (c.resolved || !lines.get(c.from.file)?.has(c.from.line))
|
|
445
|
+
continue;
|
|
446
|
+
sites.push({
|
|
447
|
+
file: c.from.file, line: c.from.line, type: 'cross_file_call', reason: 'unresolved_cross_file_call',
|
|
448
|
+
code: `${c.from.method} -> ${c.to.file}#${c.to.method}`, atSink: false, inProject: true,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
sites.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line || a.type.localeCompare(b.type) || a.code.localeCompare(b.code)));
|
|
452
|
+
return { count: sites.length, sites };
|
|
453
|
+
}
|
|
454
|
+
const variantId = (v) => `${v.source.file}:${v.source.line}->${v.sink.file}:${v.sink.line}`;
|
|
455
|
+
/** Every path of class `cwe` in one analysed tree, keyed and sorted by key. */
|
|
456
|
+
export function pathSetOf(project, options) {
|
|
457
|
+
const cwe = normalizeCwe(options.cwe);
|
|
458
|
+
const ctxByFile = new Map();
|
|
459
|
+
for (const f of project.files) {
|
|
460
|
+
const ctx = { file: f.file, ir: f.analysis };
|
|
461
|
+
const ids = options.stableIds?.[f.file];
|
|
462
|
+
if (ids)
|
|
463
|
+
ctx.ids = ids;
|
|
464
|
+
const br = options.branches?.[f.file];
|
|
465
|
+
if (br)
|
|
466
|
+
ctx.branches = br;
|
|
467
|
+
ctxByFile.set(f.file, ctx);
|
|
468
|
+
}
|
|
469
|
+
const variants = [];
|
|
470
|
+
for (const ctx of ctxByFile.values()) {
|
|
471
|
+
for (const flow of ctx.ir.taint.flows ?? []) {
|
|
472
|
+
const sink = sinkEnd(ctx, flow.sink_line, flow.sink_type);
|
|
473
|
+
if (!sink || sink.cwe !== cwe)
|
|
474
|
+
continue;
|
|
475
|
+
const source = sourceEnd(ctx, flow.source_line, flow.source_type);
|
|
476
|
+
if (!source)
|
|
477
|
+
continue;
|
|
478
|
+
variants.push({
|
|
479
|
+
source,
|
|
480
|
+
sink,
|
|
481
|
+
sanitized: flow.sanitized,
|
|
482
|
+
hops: flow.path.map(s => ({ file: ctx.file, line: s.line, variable: s.variable })),
|
|
483
|
+
confidence: flow.confidence,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
for (const p of project.taint_paths ?? []) {
|
|
488
|
+
if (!p.path_exists || normalizeCwe(p.sink.cwe) !== cwe)
|
|
489
|
+
continue;
|
|
490
|
+
const sctx = ctxByFile.get(p.source.file);
|
|
491
|
+
const kctx = ctxByFile.get(p.sink.file);
|
|
492
|
+
if (!sctx || !kctx)
|
|
493
|
+
continue;
|
|
494
|
+
const source = sourceEnd(sctx, p.source.line, p.source.type);
|
|
495
|
+
const sink = sinkEnd(kctx, p.sink.line, p.sink.type);
|
|
496
|
+
if (!source || !sink)
|
|
497
|
+
continue;
|
|
498
|
+
variants.push({
|
|
499
|
+
source,
|
|
500
|
+
sink,
|
|
501
|
+
sanitized: p.sanitizers_in_path.length > 0,
|
|
502
|
+
hops: p.hops.map(h => {
|
|
503
|
+
const hop = { file: h.file, line: h.line };
|
|
504
|
+
if (h.method)
|
|
505
|
+
hop.method = h.method;
|
|
506
|
+
if (h.variable)
|
|
507
|
+
hop.variable = h.variable;
|
|
508
|
+
return hop;
|
|
509
|
+
}),
|
|
510
|
+
confidence: p.confidence,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
// Endpoints and guards (see endpointOf / guardOf), and template hand-offs.
|
|
514
|
+
const templateFacts = Object.values(options.templates ?? {}).flat();
|
|
515
|
+
for (const v of variants) {
|
|
516
|
+
const kctx = ctxByFile.get(v.sink.file);
|
|
517
|
+
v.sink = endpointOf(kctx, v.sink);
|
|
518
|
+
const uses = templateUsesOf(kctx, v.sink, templateFacts);
|
|
519
|
+
if (uses.length > 0)
|
|
520
|
+
v.sink = { ...v.sink, templates: uses };
|
|
521
|
+
if (v.sanitized)
|
|
522
|
+
continue;
|
|
523
|
+
const pathVars = new Set(v.hops.map(h => h.variable).filter((x) => !!x));
|
|
524
|
+
const g = guardOf(kctx, v.sink, pathVars);
|
|
525
|
+
if (g)
|
|
526
|
+
v.guard = g;
|
|
527
|
+
}
|
|
528
|
+
// Group by key; a variant seen both per-file and cross-file counts once.
|
|
529
|
+
const byKey = new Map();
|
|
530
|
+
for (const v of variants) {
|
|
531
|
+
const key = pathKey(cwe, v.source.anchor, v.sink.anchor);
|
|
532
|
+
let group = byKey.get(key);
|
|
533
|
+
if (!group)
|
|
534
|
+
byKey.set(key, (group = new Map()));
|
|
535
|
+
const id = variantId(v);
|
|
536
|
+
const list = group.get(id);
|
|
537
|
+
if (list)
|
|
538
|
+
list.push(v);
|
|
539
|
+
else
|
|
540
|
+
group.set(id, [v]);
|
|
541
|
+
}
|
|
542
|
+
const projectTypes = new Set(project.files.flatMap(f => f.analysis.types.map(t => t.name)));
|
|
543
|
+
const paths = [];
|
|
544
|
+
const files = new Set();
|
|
545
|
+
const schemes = new Set();
|
|
546
|
+
for (const [key, group] of byKey) {
|
|
547
|
+
const ids = [...group.keys()].sort();
|
|
548
|
+
const all = ids.flatMap(id => group.get(id));
|
|
549
|
+
const first = group.get(ids[0])[0];
|
|
550
|
+
// a variant is open if any of its observations is neither sanitized nor guarded
|
|
551
|
+
const open = ids.some(id => group.get(id).some(v => !v.sanitized && !v.guard));
|
|
552
|
+
const guard = all.find(v => !v.sanitized && v.guard)?.guard;
|
|
553
|
+
const state = open ? 'present' : guard ? 'guarded' : 'sanitized';
|
|
554
|
+
const entry = {
|
|
555
|
+
key,
|
|
556
|
+
cwe,
|
|
557
|
+
source: first.source,
|
|
558
|
+
sink: first.sink,
|
|
559
|
+
state,
|
|
560
|
+
variants: ids.length,
|
|
561
|
+
hops: first.hops,
|
|
562
|
+
confidence: Math.max(...all.map(v => v.confidence)),
|
|
563
|
+
unresolved: { count: 0, sites: [] },
|
|
564
|
+
};
|
|
565
|
+
if (state === 'guarded' && guard)
|
|
566
|
+
entry.guard = guard;
|
|
567
|
+
// unresolved edges over the lines of every variant of this path
|
|
568
|
+
const lines = new Map();
|
|
569
|
+
for (const v of all) {
|
|
570
|
+
for (const [f, set] of pathLines(ctxByFile, v)) {
|
|
571
|
+
const acc = lines.get(f);
|
|
572
|
+
if (acc)
|
|
573
|
+
for (const l of set)
|
|
574
|
+
acc.add(l);
|
|
575
|
+
else
|
|
576
|
+
lines.set(f, new Set(set));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
entry.unresolved = unresolvedOn(ctxByFile, projectTypes, project.cross_file_calls, lines, first.sink);
|
|
580
|
+
paths.push(entry);
|
|
581
|
+
for (const v of all) {
|
|
582
|
+
files.add(v.source.file);
|
|
583
|
+
files.add(v.sink.file);
|
|
584
|
+
for (const h of v.hops)
|
|
585
|
+
files.add(h.file);
|
|
586
|
+
schemes.add(v.source.decl.scheme);
|
|
587
|
+
schemes.add(v.sink.decl.scheme);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
paths.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
591
|
+
return {
|
|
592
|
+
scheme: PATH_KEY_SCHEME,
|
|
593
|
+
cwe,
|
|
594
|
+
paths,
|
|
595
|
+
declKeySchemes: [...schemes].sort(),
|
|
596
|
+
files: [...files].sort(),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
// ---------------------------------------------------------------------------
|
|
600
|
+
// Two trees
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
602
|
+
/** The set change between two states. */
|
|
603
|
+
export function changeOf(before, after) {
|
|
604
|
+
if (before === 'present') {
|
|
605
|
+
if (after === 'absent')
|
|
606
|
+
return 'removed';
|
|
607
|
+
if (after === 'sanitized')
|
|
608
|
+
return 'sanitized';
|
|
609
|
+
if (after === 'guarded')
|
|
610
|
+
return 'guarded';
|
|
611
|
+
return 'unchanged';
|
|
612
|
+
}
|
|
613
|
+
return after === 'present' ? 'added' : 'other';
|
|
614
|
+
}
|
|
615
|
+
/** Unresolved edges, budget flags and parse health of one analysed tree. */
|
|
616
|
+
export function pinFacts(project, pathFiles, pathCount) {
|
|
617
|
+
const byType = {
|
|
618
|
+
virtual_dispatch: 0,
|
|
619
|
+
taint_propagation: 0,
|
|
620
|
+
reflection: 0,
|
|
621
|
+
dynamic_call: 0,
|
|
622
|
+
};
|
|
623
|
+
let total = 0;
|
|
624
|
+
let onPathFiles = 0;
|
|
625
|
+
const flags = [];
|
|
626
|
+
for (const f of project.files) {
|
|
627
|
+
for (const u of f.analysis.unresolved) {
|
|
628
|
+
total++;
|
|
629
|
+
byType[u.type]++;
|
|
630
|
+
if (pathFiles.has(f.file))
|
|
631
|
+
onPathFiles++;
|
|
632
|
+
}
|
|
633
|
+
const ps = f.analysis.parse_status;
|
|
634
|
+
if (ps && !ps.success && pathFiles.has(f.file))
|
|
635
|
+
flags.push(`parse-error:${f.file}`);
|
|
636
|
+
}
|
|
637
|
+
const crossFileCallsUnresolved = project.cross_file_calls.filter(c => !c.resolved && (pathFiles.has(c.from.file) || pathFiles.has(c.to.file))).length;
|
|
638
|
+
if (project.cross_file_budget_exceeded)
|
|
639
|
+
flags.push('cross_file_budget_exceeded');
|
|
640
|
+
if (project.per_file_budget_exceeded)
|
|
641
|
+
flags.push('per_file_budget_exceeded');
|
|
642
|
+
if (project.project_size_budget_exceeded)
|
|
643
|
+
flags.push('project_size_budget_exceeded');
|
|
644
|
+
flags.sort();
|
|
645
|
+
return {
|
|
646
|
+
pathCount,
|
|
647
|
+
unresolved: { total, byType, onPathFiles },
|
|
648
|
+
crossFileCallsUnresolved,
|
|
649
|
+
complete: flags.length === 0,
|
|
650
|
+
flags,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
/** Compare two path sets of the same class. */
|
|
654
|
+
export function diffPathSets(before, after) {
|
|
655
|
+
if (before.cwe !== after.cwe)
|
|
656
|
+
throw new Error(`path-set: cannot diff ${before.cwe} against ${after.cwe}`);
|
|
657
|
+
const b = new Map(before.paths.map(p => [p.key, p]));
|
|
658
|
+
const a = new Map(after.paths.map(p => [p.key, p]));
|
|
659
|
+
const keys = [...new Set([...b.keys(), ...a.keys()])].sort();
|
|
660
|
+
const counts = { removed: 0, sanitized: 0, guarded: 0, unchanged: 0, added: 0, other: 0 };
|
|
661
|
+
const entries = keys.map(key => {
|
|
662
|
+
const pb = b.get(key);
|
|
663
|
+
const pa = a.get(key);
|
|
664
|
+
const state = { before: pb?.state ?? 'absent', after: pa?.state ?? 'absent' };
|
|
665
|
+
const change = changeOf(state.before, state.after);
|
|
666
|
+
counts[change]++;
|
|
667
|
+
const entry = { key, state, change };
|
|
668
|
+
if (pb)
|
|
669
|
+
entry.before = pb;
|
|
670
|
+
if (pa)
|
|
671
|
+
entry.after = pa;
|
|
672
|
+
// the view this path hands to rendered it differently at the two versions
|
|
673
|
+
if (pb?.sink.templates || pa?.sink.templates) {
|
|
674
|
+
const shape = (p) => JSON.stringify((p?.sink.templates ?? []).map(t => [t.file, t.engine, t.construct, t.attributes, t.inAttribute ?? null]));
|
|
675
|
+
if (shape(pb) !== shape(pa))
|
|
676
|
+
entry.templateChanged = true;
|
|
677
|
+
}
|
|
678
|
+
return entry;
|
|
679
|
+
});
|
|
680
|
+
return { entries, counts };
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Every path of class `cwe` at two versions of a tree, the change per key,
|
|
684
|
+
* and per version the unresolved edges and budget flags. Path files are the
|
|
685
|
+
* union over both versions, so both pins are counted over the same region.
|
|
686
|
+
*/
|
|
687
|
+
export function pathSetDiff(before, after, options) {
|
|
688
|
+
const at = (pin) => {
|
|
689
|
+
const o = { cwe: options.cwe };
|
|
690
|
+
const ids = options.stableIds?.[pin];
|
|
691
|
+
if (ids)
|
|
692
|
+
o.stableIds = ids;
|
|
693
|
+
const br = options.branches?.[pin];
|
|
694
|
+
if (br)
|
|
695
|
+
o.branches = br;
|
|
696
|
+
const tp = options.templates?.[pin];
|
|
697
|
+
if (tp)
|
|
698
|
+
o.templates = tp;
|
|
699
|
+
return o;
|
|
700
|
+
};
|
|
701
|
+
const setB = pathSetOf(before, at('before'));
|
|
702
|
+
const setA = pathSetOf(after, at('after'));
|
|
703
|
+
const files = new Set([...setB.files, ...setA.files]);
|
|
704
|
+
const { entries, counts } = diffPathSets(setB, setA);
|
|
705
|
+
return {
|
|
706
|
+
scheme: PATH_KEY_SCHEME,
|
|
707
|
+
cwe: setB.cwe,
|
|
708
|
+
entries,
|
|
709
|
+
counts,
|
|
710
|
+
pins: {
|
|
711
|
+
before: pinFacts(before, files, setB.paths.length),
|
|
712
|
+
after: pinFacts(after, files, setA.paths.length),
|
|
713
|
+
},
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
//# sourceMappingURL=path-set.js.map
|