circle-ir 3.176.0 → 3.178.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +75 -0
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/constant-propagation/propagator.d.ts +1 -2
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +28 -30
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/dependency-versions.d.ts +102 -0
- package/dist/analysis/dependency-versions.d.ts.map +1 -0
- package/dist/analysis/dependency-versions.js +153 -0
- package/dist/analysis/dependency-versions.js.map +1 -0
- package/dist/analysis/dfg-walk.d.ts.map +1 -1
- package/dist/analysis/dfg-walk.js +28 -1
- package/dist/analysis/dfg-walk.js.map +1 -1
- package/dist/analysis/note-coalescer.d.ts +46 -0
- package/dist/analysis/note-coalescer.d.ts.map +1 -0
- package/dist/analysis/note-coalescer.js +106 -0
- package/dist/analysis/note-coalescer.js.map +1 -0
- package/dist/analysis/passes/deserialization-safety-gate-pass.d.ts +58 -0
- package/dist/analysis/passes/deserialization-safety-gate-pass.d.ts.map +1 -0
- package/dist/analysis/passes/deserialization-safety-gate-pass.js +122 -0
- package/dist/analysis/passes/deserialization-safety-gate-pass.js.map +1 -0
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +33 -2
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +55 -33
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.d.ts +34 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +21 -1
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +351 -47
- package/dist/core/circle-ir-core.cjs +197 -45
- package/dist/core/circle-ir-core.js +197 -45
- package/dist/core/extractors/calls.js +131 -1
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/core/extractors/cfg.d.ts.map +1 -1
- package/dist/core/extractors/cfg.js +17 -9
- package/dist/core/extractors/cfg.js.map +1 -1
- package/dist/types/index.d.ts +24 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-manifest helpers for the deserialization-safety-gate pass.
|
|
3
|
+
*
|
|
4
|
+
* cognium-dev #258 — dependency-version-aware sink gating. The engine
|
|
5
|
+
* never reads the filesystem (Pillar I / browser-safety); the caller
|
|
6
|
+
* (cognium-dev CLI or `analyzeProject`) reads the manifest and passes
|
|
7
|
+
* it as raw text via `AnalyzerOptions.dependencyContext`. Helpers here
|
|
8
|
+
* turn that raw text into the boolean predicates the gate needs.
|
|
9
|
+
*
|
|
10
|
+
* All parsers here are string-scoped: regex over the raw manifest
|
|
11
|
+
* rather than a full XML/JSON tree. That is deliberate — the gate only
|
|
12
|
+
* needs a handful of narrow signals (Fastjson version, presence of a
|
|
13
|
+
* safe classifier), and every existing runtime dep in circle-ir is
|
|
14
|
+
* either `web-tree-sitter` or `yaml`. Pulling in an XML parser purely
|
|
15
|
+
* for one Fastjson property would violate the minimal-dependencies
|
|
16
|
+
* guardrail.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Extract the effective Fastjson version from a `pom.xml`. Two sources
|
|
20
|
+
* are consulted:
|
|
21
|
+
*
|
|
22
|
+
* 1. A `<properties>` entry named exactly `<fastjson.version>` — the
|
|
23
|
+
* idiomatic way Maven projects centralise a dep version they
|
|
24
|
+
* reference in a `<dependencies>` block. This is where
|
|
25
|
+
* alibaba/Sentinel pins `1.2.83_noneautotype`.
|
|
26
|
+
* 2. A `<dependency>` block whose `<groupId>` is `com.alibaba` and
|
|
27
|
+
* `<artifactId>` is `fastjson` — read as a fallback when the
|
|
28
|
+
* version is declared inline rather than as a property.
|
|
29
|
+
*
|
|
30
|
+
* Returns `null` when no Fastjson coordinate can be resolved from the
|
|
31
|
+
* pom (no properties entry AND no matching dependency block, or the
|
|
32
|
+
* dependency uses a `${...}` reference the properties block doesn't
|
|
33
|
+
* define).
|
|
34
|
+
*/
|
|
35
|
+
export function resolveFastjsonFromPom(pomXml) {
|
|
36
|
+
if (!pomXml)
|
|
37
|
+
return null;
|
|
38
|
+
const propMatch = pomXml.match(/<fastjson\.version>\s*([^<\s]+)\s*<\/fastjson\.version>/);
|
|
39
|
+
if (propMatch) {
|
|
40
|
+
const version = propMatch[1];
|
|
41
|
+
return { version, noneAutotype: /_noneautotype/i.test(version) };
|
|
42
|
+
}
|
|
43
|
+
// Fallback: scan every <dependency>...</dependency> block for
|
|
44
|
+
// com.alibaba:fastjson (or fastjson2). Non-greedy so we don't fuse
|
|
45
|
+
// sibling blocks. Case-sensitive on artifact / group; Maven's own
|
|
46
|
+
// resolution is case-sensitive too.
|
|
47
|
+
const depRe = /<dependency>[\s\S]*?<\/dependency>/g;
|
|
48
|
+
let m;
|
|
49
|
+
while ((m = depRe.exec(pomXml)) !== null) {
|
|
50
|
+
const block = m[0];
|
|
51
|
+
const gid = block.match(/<groupId>\s*([^<\s]+)\s*<\/groupId>/)?.[1];
|
|
52
|
+
const aid = block.match(/<artifactId>\s*([^<\s]+)\s*<\/artifactId>/)?.[1];
|
|
53
|
+
if (gid !== 'com.alibaba')
|
|
54
|
+
continue;
|
|
55
|
+
if (aid !== 'fastjson' && aid !== 'fastjson2')
|
|
56
|
+
continue;
|
|
57
|
+
const ver = block.match(/<version>\s*([^<\s]+)\s*<\/version>/)?.[1];
|
|
58
|
+
if (!ver)
|
|
59
|
+
continue;
|
|
60
|
+
// `${fastjson.version}` reference the properties block did not
|
|
61
|
+
// resolve — treat as unknown so the gate defaults to fire.
|
|
62
|
+
if (/^\$\{/.test(ver))
|
|
63
|
+
return null;
|
|
64
|
+
return { version: ver, noneAutotype: /_noneautotype/i.test(ver) };
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Return true when the given source text contains an in-file call that
|
|
70
|
+
* re-enables Fastjson autotype. Even a `_noneautotype` build does not
|
|
71
|
+
* protect against code that programmatically re-enables the feature
|
|
72
|
+
* (which the hardened build documents as impossible, but the classifier
|
|
73
|
+
* is a build-time strip, not a runtime lock). Defense-in-depth for the
|
|
74
|
+
* `resolveFastjsonFromPom` gate.
|
|
75
|
+
*/
|
|
76
|
+
export function fileReenablesFastjsonAutotype(source) {
|
|
77
|
+
if (!source)
|
|
78
|
+
return false;
|
|
79
|
+
// ParserConfig.getGlobalInstance().setAutoTypeSupport(true)
|
|
80
|
+
// ParserConfig.setAutoTypeSupport(true) (static-import form)
|
|
81
|
+
return /\bsetAutoTypeSupport\s*\(\s*true\b/.test(source);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Return true when the given Java source contains a call that enables
|
|
85
|
+
* Jackson polymorphic type handling — either the legacy
|
|
86
|
+
* `enableDefaultTyping(...)` (deprecated in Jackson 2.10+ but still
|
|
87
|
+
* shipped) or the current `activateDefaultTyping(...)`.
|
|
88
|
+
*
|
|
89
|
+
* When neither is present in the file (and no `@JsonTypeInfo` is used
|
|
90
|
+
* on the target type — best-effort scan below), Jackson's default
|
|
91
|
+
* behaviour since 2.10 is safe: `ObjectMapper.readValue(json,
|
|
92
|
+
* targetType)` cannot instantiate arbitrary classes. The gate uses
|
|
93
|
+
* this to distinguish a genuine `readValue` sink from a safely-
|
|
94
|
+
* configured one.
|
|
95
|
+
*
|
|
96
|
+
* `@JsonTypeInfo` scan is intentionally file-local. A `@JsonTypeInfo`
|
|
97
|
+
* annotation on a target type in a different file would still allow
|
|
98
|
+
* polymorphic construction, but the engine treats that as unknown
|
|
99
|
+
* risk and preserves the sink (the gate only fires when the *file
|
|
100
|
+
* itself* provides positive evidence of a safe configuration).
|
|
101
|
+
*/
|
|
102
|
+
export function fileEnablesJacksonPolymorphism(source) {
|
|
103
|
+
if (!source)
|
|
104
|
+
return false;
|
|
105
|
+
if (/\benableDefaultTyping\s*\(/.test(source))
|
|
106
|
+
return true;
|
|
107
|
+
if (/\bactivateDefaultTyping\s*\(/.test(source))
|
|
108
|
+
return true;
|
|
109
|
+
// @JsonTypeInfo(use = Id.CLASS) / (use = Id.MINIMAL_CLASS) / (use = Id.NAME)
|
|
110
|
+
// enables polymorphic type handling on the annotated field or type.
|
|
111
|
+
// The exact `use` argument doesn't matter for the gate — any
|
|
112
|
+
// @JsonTypeInfo signals polymorphism is in play somewhere.
|
|
113
|
+
if (/@JsonTypeInfo\b/.test(source))
|
|
114
|
+
return true;
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Return true when the given Java source contains a
|
|
119
|
+
* `new Yaml(new SafeConstructor(...))` or an equivalent hardened
|
|
120
|
+
* SnakeYAML constructor. When any `Yaml` instance in the file is
|
|
121
|
+
* built with the safe constructor family, we assume the file's
|
|
122
|
+
* `Yaml.load(...)` calls are safely configured; the gate then drops
|
|
123
|
+
* the deserialization sink for those calls.
|
|
124
|
+
*
|
|
125
|
+
* Recognised safe constructor classes (SnakeYAML 1.x + 2.x):
|
|
126
|
+
* - SafeConstructor — canonical safe loader
|
|
127
|
+
* - SafeSchema (rare, YAML 1.2) — safe schema-based loader
|
|
128
|
+
*
|
|
129
|
+
* Recognised safe factory calls (SnakeYAML 2.x LoaderOptions API):
|
|
130
|
+
* - Yaml.load() with a Constructor that extends SafeConstructor
|
|
131
|
+
* (heuristic: any `SafeConstructor`-typed variable is safe)
|
|
132
|
+
*
|
|
133
|
+
* NOTE: A file that mixes `new Yaml(new SafeConstructor())` with
|
|
134
|
+
* `new Yaml(new Constructor(SomeClass.class))` on separate call sites
|
|
135
|
+
* would over-suppress the unsafe site. In practice this is rare (SAST
|
|
136
|
+
* teams write one wrapper per file), and the current sink-filter
|
|
137
|
+
* stage 9b handles the analogous compiled-template case with the same
|
|
138
|
+
* file-scoped heuristic. If it becomes a problem, tighten to
|
|
139
|
+
* receiver-scoped: check the specific `Yaml` receiver that carries
|
|
140
|
+
* the load() call.
|
|
141
|
+
*/
|
|
142
|
+
export function fileConfiguresSnakeYamlSafely(source) {
|
|
143
|
+
if (!source)
|
|
144
|
+
return false;
|
|
145
|
+
if (/\bnew\s+SafeConstructor\s*\(/.test(source))
|
|
146
|
+
return true;
|
|
147
|
+
// Explicit typed declaration form: `SafeConstructor sc = new SafeConstructor()`
|
|
148
|
+
// or `SafeConstructor sc = ...` used in a Yaml constructor.
|
|
149
|
+
if (/\bSafeConstructor\s+\w+\s*=/.test(source))
|
|
150
|
+
return true;
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=dependency-versions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dependency-versions.js","sourceRoot":"","sources":["../../src/analysis/dependency-versions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAcH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAc;IACnD,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEzB,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAC1F,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,OAAO,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;QAC7B,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;IACnE,CAAC;IAED,8DAA8D;IAC9D,mEAAmE;IACnE,kEAAkE;IAClE,oCAAoC;IACpC,MAAM,KAAK,GAAG,qCAAqC,CAAC;IACpD,IAAI,CAAyB,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACnB,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,qCAAqC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACpE,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,2CAA2C,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC1E,IAAI,GAAG,KAAK,aAAa;YAAE,SAAS;QACpC,IAAI,GAAG,KAAK,UAAU,IAAI,GAAG,KAAK,WAAW;YAAE,SAAS;QACxD,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,qCAAqC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACpE,IAAI,CAAC,GAAG;YAAE,SAAS;QACnB,+DAA+D;QAC/D,2DAA2D;QAC3D,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QACnC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IACpE,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,6BAA6B,CAAC,MAAc;IAC1D,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1B,4DAA4D;IAC5D,+DAA+D;IAC/D,OAAO,oCAAoC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,8BAA8B,CAAC,MAAc;IAC3D,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1B,IAAI,4BAA4B,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3D,IAAI,8BAA8B,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7D,6EAA6E;IAC7E,oEAAoE;IACpE,6DAA6D;IAC7D,2DAA2D;IAC3D,IAAI,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAChD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,6BAA6B,CAAC,MAAc;IAC1D,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1B,IAAI,8BAA8B,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7D,gFAAgF;IAChF,4DAA4D;IAC5D,IAAI,6BAA6B,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5D,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dfg-walk.d.ts","sourceRoot":"","sources":["../../src/analysis/dfg-walk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE1D,MAAM,WAAW,kBAAkB;IACjC,kDAAkD;IAClD,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7B,oDAAoD;IACpD,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3B,yEAAyE;IACzE,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;
|
|
1
|
+
{"version":3,"file":"dfg-walk.d.ts","sourceRoot":"","sources":["../../src/analysis/dfg-walk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE1D,MAAM,WAAW,kBAAkB;IACjC,kDAAkD;IAClD,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7B,oDAAoD;IACpD,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3B,yEAAyE;IACzE,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAoBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,EAC9C,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,OAAO,GAAE,mBAAwB,GAChC,kBAAkB,CA0DpB"}
|
|
@@ -15,12 +15,33 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Cycle-safe via a `visited` Set. Bounded via `maxHops` (default 32).
|
|
17
17
|
*/
|
|
18
|
+
/**
|
|
19
|
+
* Per-file memo for `walkBackwardDefs`. Keyed on `chainsByToDef` identity,
|
|
20
|
+
* so results auto-clear when the caller moves to a new file (each file's
|
|
21
|
+
* `analyze()` builds a fresh `chainsByToDef` map, releasing the previous
|
|
22
|
+
* entry when nothing else references it). Inner key is
|
|
23
|
+
* `${startDefId}|${maxHops}` — same starting def + hop cap within a file
|
|
24
|
+
* returns the cached `BackwardWalkResult` directly.
|
|
25
|
+
*
|
|
26
|
+
* cognium-dev #254 T2#10: multiple sinks in a single file frequently walk
|
|
27
|
+
* back to the same source def; without a memo the DFG chain from that def
|
|
28
|
+
* is re-traversed once per sink. Result Sets are immutable (`ReadonlySet`
|
|
29
|
+
* via the return type), so sharing them across callers is safe.
|
|
30
|
+
*/
|
|
31
|
+
const walkBackwardDefsMemo = new WeakMap();
|
|
18
32
|
/**
|
|
19
33
|
* Bounded backward BFS from `startDefId` along `chainsByToDef`. Every def
|
|
20
34
|
* reached is added to `lines` (via `defById`). Cycle-safe.
|
|
21
35
|
*/
|
|
22
36
|
export function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
|
|
23
37
|
const maxHops = options.maxHops ?? 32;
|
|
38
|
+
let perFile = walkBackwardDefsMemo.get(chainsByToDef);
|
|
39
|
+
if (perFile !== undefined) {
|
|
40
|
+
const key = `${startDefId}|${maxHops}`;
|
|
41
|
+
const hit = perFile.get(key);
|
|
42
|
+
if (hit !== undefined)
|
|
43
|
+
return hit;
|
|
44
|
+
}
|
|
24
45
|
const visited = new Set();
|
|
25
46
|
const lines = new Set();
|
|
26
47
|
let hopCapReached = false;
|
|
@@ -54,6 +75,12 @@ export function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {
|
|
|
54
75
|
queue.push(fromId);
|
|
55
76
|
}
|
|
56
77
|
}
|
|
57
|
-
|
|
78
|
+
const result = { visited, lines, hopCapReached };
|
|
79
|
+
if (perFile === undefined) {
|
|
80
|
+
perFile = new Map();
|
|
81
|
+
walkBackwardDefsMemo.set(chainsByToDef, perFile);
|
|
82
|
+
}
|
|
83
|
+
perFile.set(`${startDefId}|${maxHops}`, result);
|
|
84
|
+
return result;
|
|
58
85
|
}
|
|
59
86
|
//# sourceMappingURL=dfg-walk.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dfg-walk.js","sourceRoot":"","sources":["../../src/analysis/dfg-walk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAkBH;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,UAAkB,EAClB,aAA8C,EAC9C,OAAoC,EACpC,UAA+B,EAAE;IAEjC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"dfg-walk.js","sourceRoot":"","sources":["../../src/analysis/dfg-walk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAkBH;;;;;;;;;;;;GAYG;AACH,MAAM,oBAAoB,GAAG,IAAI,OAAO,EAGrC,CAAC;AAEJ;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,UAAkB,EAClB,aAA8C,EAC9C,OAAoC,EACpC,UAA+B,EAAE;IAEjC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;IAEtC,IAAI,OAAO,GAAG,oBAAoB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IACtD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,GAAG,UAAU,IAAI,OAAO,EAAE,CAAC;QACvC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,GAAG,CAAC;IACpC,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,IAAI,aAAa,GAAG,KAAK,CAAC;IAE1B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACzC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC;IAClD,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACxB,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAEzB,MAAM,KAAK,GAAa,CAAC,UAAU,CAAC,CAAC;IACrC,IAAI,IAAI,GAAG,CAAC,CAAC;IAEb,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,IAAI,IAAI,IAAI,OAAO,EAAE,CAAC;YACpB,aAAa,GAAG,IAAI,CAAC;YACrB,MAAM;QACR,CAAC;QACD,IAAI,EAAE,CAAC;QAEP,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,EAAY,CAAC;QAC1C,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC9C,IAAI,CAAC,QAAQ;YAAE,SAAS;QAExB,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC;YAC9B,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,SAAS;YAElC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACpB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACpC,IAAI,OAAO,EAAE,CAAC;gBACZ,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC1B,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAuB,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC;IAErE,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,GAAG,IAAI,GAAG,EAAE,CAAC;QACpB,oBAAoB,CAAC,GAAG,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,GAAG,UAAU,IAAI,OAAO,EAAE,EAAE,MAAM,CAAC,CAAC;IAEhD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Note-level finding coalescer — cognium-dev #143.
|
|
3
|
+
*
|
|
4
|
+
* Groups `SastFinding[]` by `(file, line)` and folds any group of two or
|
|
5
|
+
* more `level === 'note'` findings into a single record. The primary
|
|
6
|
+
* finding is picked deterministically (lexicographic `rule_id`) and
|
|
7
|
+
* the co-located rule_ids are attached as `labels[]`. Every other
|
|
8
|
+
* (`level === 'warning' | 'error'`) finding passes through untouched.
|
|
9
|
+
*
|
|
10
|
+
* Empirical basis (from the OWASP-Benchmark instrumentation capture on
|
|
11
|
+
* cognium-dev#145, comment 2026-07-03):
|
|
12
|
+
* - 5,481 `(file, line)` locations (31.3% of files) were hit by ≥ 2
|
|
13
|
+
* distinct advisory rules.
|
|
14
|
+
* - Most-common pairs:
|
|
15
|
+
* missing-public-doc + naming-convention × 2,740
|
|
16
|
+
* missing-csp-frame-ancestors + missing-x-frame-options × 2,740
|
|
17
|
+
* unused-variable + variable-shadowing (co-located)
|
|
18
|
+
* - HIGH-severity: 0 co-locations in the same capture.
|
|
19
|
+
*
|
|
20
|
+
* Design invariants:
|
|
21
|
+
* - Additive: consumers that key on `rule_id` continue to work
|
|
22
|
+
* unchanged. Consumers that surface every co-located rule read
|
|
23
|
+
* both `rule_id` and `labels`.
|
|
24
|
+
* - Level-gated: only fires when EVERY finding in the group has
|
|
25
|
+
* `level === 'note'`. If any group member is `warning` or `error`,
|
|
26
|
+
* the group passes through un-coalesced — visibility of higher-
|
|
27
|
+
* severity findings is never diminished.
|
|
28
|
+
* - Deterministic: sort by `rule_id` inside a group before picking
|
|
29
|
+
* the primary, so the same input always produces the same output
|
|
30
|
+
* (test-friendly, diff-friendly, cache-friendly).
|
|
31
|
+
* - Message preservation: the primary finding's `message` is kept
|
|
32
|
+
* verbatim. The `labels[]` field is the sole signal that more
|
|
33
|
+
* rules co-located there.
|
|
34
|
+
*
|
|
35
|
+
* This is the MVP of the reopen. Follow-ups on #143 include a full
|
|
36
|
+
* instrumentation rerun on multi-severity data + a broader
|
|
37
|
+
* medium-severity coalesce policy — both deferred until the data
|
|
38
|
+
* capture is redone.
|
|
39
|
+
*/
|
|
40
|
+
import type { SastFinding } from '../types/index.js';
|
|
41
|
+
/**
|
|
42
|
+
* Coalesce note-level findings at the same `(file, line)` location.
|
|
43
|
+
* Returns a new array; the input is not mutated.
|
|
44
|
+
*/
|
|
45
|
+
export declare function coalesceNoteLevelFindings(findings: readonly SastFinding[]): SastFinding[];
|
|
46
|
+
//# sourceMappingURL=note-coalescer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"note-coalescer.d.ts","sourceRoot":"","sources":["../../src/analysis/note-coalescer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,SAAS,WAAW,EAAE,GAC/B,WAAW,EAAE,CAkEf"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Note-level finding coalescer — cognium-dev #143.
|
|
3
|
+
*
|
|
4
|
+
* Groups `SastFinding[]` by `(file, line)` and folds any group of two or
|
|
5
|
+
* more `level === 'note'` findings into a single record. The primary
|
|
6
|
+
* finding is picked deterministically (lexicographic `rule_id`) and
|
|
7
|
+
* the co-located rule_ids are attached as `labels[]`. Every other
|
|
8
|
+
* (`level === 'warning' | 'error'`) finding passes through untouched.
|
|
9
|
+
*
|
|
10
|
+
* Empirical basis (from the OWASP-Benchmark instrumentation capture on
|
|
11
|
+
* cognium-dev#145, comment 2026-07-03):
|
|
12
|
+
* - 5,481 `(file, line)` locations (31.3% of files) were hit by ≥ 2
|
|
13
|
+
* distinct advisory rules.
|
|
14
|
+
* - Most-common pairs:
|
|
15
|
+
* missing-public-doc + naming-convention × 2,740
|
|
16
|
+
* missing-csp-frame-ancestors + missing-x-frame-options × 2,740
|
|
17
|
+
* unused-variable + variable-shadowing (co-located)
|
|
18
|
+
* - HIGH-severity: 0 co-locations in the same capture.
|
|
19
|
+
*
|
|
20
|
+
* Design invariants:
|
|
21
|
+
* - Additive: consumers that key on `rule_id` continue to work
|
|
22
|
+
* unchanged. Consumers that surface every co-located rule read
|
|
23
|
+
* both `rule_id` and `labels`.
|
|
24
|
+
* - Level-gated: only fires when EVERY finding in the group has
|
|
25
|
+
* `level === 'note'`. If any group member is `warning` or `error`,
|
|
26
|
+
* the group passes through un-coalesced — visibility of higher-
|
|
27
|
+
* severity findings is never diminished.
|
|
28
|
+
* - Deterministic: sort by `rule_id` inside a group before picking
|
|
29
|
+
* the primary, so the same input always produces the same output
|
|
30
|
+
* (test-friendly, diff-friendly, cache-friendly).
|
|
31
|
+
* - Message preservation: the primary finding's `message` is kept
|
|
32
|
+
* verbatim. The `labels[]` field is the sole signal that more
|
|
33
|
+
* rules co-located there.
|
|
34
|
+
*
|
|
35
|
+
* This is the MVP of the reopen. Follow-ups on #143 include a full
|
|
36
|
+
* instrumentation rerun on multi-severity data + a broader
|
|
37
|
+
* medium-severity coalesce policy — both deferred until the data
|
|
38
|
+
* capture is redone.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Coalesce note-level findings at the same `(file, line)` location.
|
|
42
|
+
* Returns a new array; the input is not mutated.
|
|
43
|
+
*/
|
|
44
|
+
export function coalesceNoteLevelFindings(findings) {
|
|
45
|
+
if (findings.length < 2)
|
|
46
|
+
return [...findings];
|
|
47
|
+
// Bucket by (file, line). We keep original insertion order so
|
|
48
|
+
// higher-level findings interleaved with note-level findings are
|
|
49
|
+
// preserved in-place; only the note-level subset is folded.
|
|
50
|
+
const groups = new Map();
|
|
51
|
+
const order = [];
|
|
52
|
+
for (const f of findings) {
|
|
53
|
+
const key = `${f.file}\0${f.line}`;
|
|
54
|
+
const bucket = groups.get(key);
|
|
55
|
+
if (bucket) {
|
|
56
|
+
bucket.push(f);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
groups.set(key, [f]);
|
|
60
|
+
order.push(key);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const key of order) {
|
|
65
|
+
const bucket = groups.get(key);
|
|
66
|
+
if (bucket.length === 1) {
|
|
67
|
+
out.push(bucket[0]);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// Split by level. Only same-key finding groups where EVERY entry
|
|
71
|
+
// is `note` get coalesced; mixed-level groups pass through
|
|
72
|
+
// un-coalesced (visibility of warnings / errors preserved).
|
|
73
|
+
const allNote = bucket.every((f) => f.level === 'note');
|
|
74
|
+
if (!allNote) {
|
|
75
|
+
for (const f of bucket)
|
|
76
|
+
out.push(f);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// Also skip when every entry has the SAME rule_id — those are
|
|
80
|
+
// duplicate emissions, not multi-rule collisions. Passing them
|
|
81
|
+
// through preserves the existing dedup behaviour handled elsewhere
|
|
82
|
+
// (e.g. taint-matcher's sinkMap dedup). Coalescer's job is only
|
|
83
|
+
// multi-rule folding.
|
|
84
|
+
const uniqueRuleIds = new Set(bucket.map((f) => f.rule_id));
|
|
85
|
+
if (uniqueRuleIds.size < 2) {
|
|
86
|
+
for (const f of bucket)
|
|
87
|
+
out.push(f);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// Deterministic primary pick: lexicographic rule_id.
|
|
91
|
+
const sorted = [...bucket].sort((a, b) => a.rule_id.localeCompare(b.rule_id));
|
|
92
|
+
const primary = sorted[0];
|
|
93
|
+
const additional = sorted
|
|
94
|
+
.slice(1)
|
|
95
|
+
.map((f) => f.rule_id)
|
|
96
|
+
// Preserve `labels[]` carried on the primary or others from a
|
|
97
|
+
// prior coalesce (idempotent when re-run).
|
|
98
|
+
.concat(...sorted.map((f) => f.labels ?? []));
|
|
99
|
+
// Dedup labels; drop the primary's rule_id if it accidentally
|
|
100
|
+
// appears in a prior labels[] entry.
|
|
101
|
+
const uniqueLabels = Array.from(new Set(additional)).filter((l) => l !== primary.rule_id);
|
|
102
|
+
out.push({ ...primary, labels: uniqueLabels });
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=note-coalescer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"note-coalescer.js","sourceRoot":"","sources":["../../src/analysis/note-coalescer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeserializationSafetyGatePass — cognium-dev #258
|
|
3
|
+
*
|
|
4
|
+
* Drops `deserialization` sinks whose surrounding library configuration
|
|
5
|
+
* renders them non-exploitable. Java-only in the MVP scope; runs after
|
|
6
|
+
* `SinkSemanticsPass` (so upstream registry gates have already fired)
|
|
7
|
+
* and before `TaintPropagationPass` (so flow generators never see the
|
|
8
|
+
* dropped sinks).
|
|
9
|
+
*
|
|
10
|
+
* Three sub-gates:
|
|
11
|
+
*
|
|
12
|
+
* Gate A — Fastjson `*_noneautotype` build (manifest-based).
|
|
13
|
+
* Reads `AnalyzerOptions.dependencyContext.java.pomXml`, resolves
|
|
14
|
+
* the effective Fastjson coordinate, and drops
|
|
15
|
+
* `JSON.parseObject` / `JSON.parse` deserialization sinks when the
|
|
16
|
+
* version literally matches the hardened classifier — UNLESS the
|
|
17
|
+
* file itself re-enables autotype via `setAutoTypeSupport(true)`
|
|
18
|
+
* (in which case the pinned build's protection is defeated and the
|
|
19
|
+
* sink continues to fire).
|
|
20
|
+
*
|
|
21
|
+
* Gate B — Jackson polymorphism not enabled (in-file scan).
|
|
22
|
+
* `ObjectMapper.readValue(json, targetType)` is safe on Jackson
|
|
23
|
+
* ≥ 2.10 unless the file enables polymorphic type handling via
|
|
24
|
+
* `enableDefaultTyping` / `activateDefaultTyping` or applies
|
|
25
|
+
* `@JsonTypeInfo` somewhere. When none of those signals appear in
|
|
26
|
+
* the file, drop `ObjectMapper.readValue` deserialization sinks.
|
|
27
|
+
*
|
|
28
|
+
* Gate C — SnakeYAML `SafeConstructor` (in-file scan).
|
|
29
|
+
* `new Yaml(new SafeConstructor())` gives a Yaml instance whose
|
|
30
|
+
* `.load(...)` cannot instantiate arbitrary classes. When the file
|
|
31
|
+
* builds any Yaml with SafeConstructor, drop `Yaml.load` /
|
|
32
|
+
* `Yaml.loadAs` / `Yaml.loadAll` deserialization sinks.
|
|
33
|
+
*
|
|
34
|
+
* Each sub-gate is defensive: on missing signal it defaults to *do
|
|
35
|
+
* not drop* (the sink continues to fire). So a resolver bug can only
|
|
36
|
+
* ever regress toward the current over-firing behaviour, never toward
|
|
37
|
+
* a false negative.
|
|
38
|
+
*
|
|
39
|
+
* See `docs/PASSES.md` for the canonical pass registry entry.
|
|
40
|
+
*/
|
|
41
|
+
import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
|
|
42
|
+
import type { DependencyContext } from '../../analyzer.js';
|
|
43
|
+
export interface DeserializationSafetyGateResult {
|
|
44
|
+
/** Sinks dropped by Gate A (Fastjson noneautotype). */
|
|
45
|
+
droppedFastjson: number;
|
|
46
|
+
/** Sinks dropped by Gate B (Jackson polymorphism not enabled). */
|
|
47
|
+
droppedJackson: number;
|
|
48
|
+
/** Sinks dropped by Gate C (SnakeYAML SafeConstructor). */
|
|
49
|
+
droppedSnakeYaml: number;
|
|
50
|
+
}
|
|
51
|
+
export declare class DeserializationSafetyGatePass implements AnalysisPass<DeserializationSafetyGateResult> {
|
|
52
|
+
private readonly dependencyContext?;
|
|
53
|
+
readonly name = "deserialization-safety-gate";
|
|
54
|
+
readonly category: "security";
|
|
55
|
+
constructor(dependencyContext?: DependencyContext | undefined);
|
|
56
|
+
run(ctx: PassContext): DeserializationSafetyGateResult;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=deserialization-safety-gate-pass.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deserialization-safety-gate-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/deserialization-safety-gate-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAG9E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAQ3D,MAAM,WAAW,+BAA+B;IAC9C,uDAAuD;IACvD,eAAe,EAAE,MAAM,CAAC;IACxB,kEAAkE;IAClE,cAAc,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAWD,qBAAa,6BACX,YAAW,YAAY,CAAC,+BAA+B,CAAC;IAK5C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC;IAH/C,QAAQ,CAAC,IAAI,iCAAiC;IAC9C,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;gBAEX,iBAAiB,CAAC,EAAE,iBAAiB,YAAA;IAElE,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,+BAA+B;CAoFvD"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeserializationSafetyGatePass — cognium-dev #258
|
|
3
|
+
*
|
|
4
|
+
* Drops `deserialization` sinks whose surrounding library configuration
|
|
5
|
+
* renders them non-exploitable. Java-only in the MVP scope; runs after
|
|
6
|
+
* `SinkSemanticsPass` (so upstream registry gates have already fired)
|
|
7
|
+
* and before `TaintPropagationPass` (so flow generators never see the
|
|
8
|
+
* dropped sinks).
|
|
9
|
+
*
|
|
10
|
+
* Three sub-gates:
|
|
11
|
+
*
|
|
12
|
+
* Gate A — Fastjson `*_noneautotype` build (manifest-based).
|
|
13
|
+
* Reads `AnalyzerOptions.dependencyContext.java.pomXml`, resolves
|
|
14
|
+
* the effective Fastjson coordinate, and drops
|
|
15
|
+
* `JSON.parseObject` / `JSON.parse` deserialization sinks when the
|
|
16
|
+
* version literally matches the hardened classifier — UNLESS the
|
|
17
|
+
* file itself re-enables autotype via `setAutoTypeSupport(true)`
|
|
18
|
+
* (in which case the pinned build's protection is defeated and the
|
|
19
|
+
* sink continues to fire).
|
|
20
|
+
*
|
|
21
|
+
* Gate B — Jackson polymorphism not enabled (in-file scan).
|
|
22
|
+
* `ObjectMapper.readValue(json, targetType)` is safe on Jackson
|
|
23
|
+
* ≥ 2.10 unless the file enables polymorphic type handling via
|
|
24
|
+
* `enableDefaultTyping` / `activateDefaultTyping` or applies
|
|
25
|
+
* `@JsonTypeInfo` somewhere. When none of those signals appear in
|
|
26
|
+
* the file, drop `ObjectMapper.readValue` deserialization sinks.
|
|
27
|
+
*
|
|
28
|
+
* Gate C — SnakeYAML `SafeConstructor` (in-file scan).
|
|
29
|
+
* `new Yaml(new SafeConstructor())` gives a Yaml instance whose
|
|
30
|
+
* `.load(...)` cannot instantiate arbitrary classes. When the file
|
|
31
|
+
* builds any Yaml with SafeConstructor, drop `Yaml.load` /
|
|
32
|
+
* `Yaml.loadAs` / `Yaml.loadAll` deserialization sinks.
|
|
33
|
+
*
|
|
34
|
+
* Each sub-gate is defensive: on missing signal it defaults to *do
|
|
35
|
+
* not drop* (the sink continues to fire). So a resolver bug can only
|
|
36
|
+
* ever regress toward the current over-firing behaviour, never toward
|
|
37
|
+
* a false negative.
|
|
38
|
+
*
|
|
39
|
+
* See `docs/PASSES.md` for the canonical pass registry entry.
|
|
40
|
+
*/
|
|
41
|
+
import { resolveFastjsonFromPom, fileReenablesFastjsonAutotype, fileEnablesJacksonPolymorphism, fileConfiguresSnakeYamlSafely, } from '../dependency-versions.js';
|
|
42
|
+
const FASTJSON_METHODS = new Set(['parseObject', 'parse']);
|
|
43
|
+
const FASTJSON_CLASSES = new Set(['JSON', 'JSONObject']);
|
|
44
|
+
const JACKSON_METHODS = new Set(['readValue', 'convertValue', 'treeToValue']);
|
|
45
|
+
const JACKSON_CLASSES = new Set(['ObjectMapper', 'ObjectReader']);
|
|
46
|
+
const SNAKEYAML_METHODS = new Set(['load', 'loadAs', 'loadAll']);
|
|
47
|
+
const SNAKEYAML_CLASSES = new Set(['Yaml']);
|
|
48
|
+
export class DeserializationSafetyGatePass {
|
|
49
|
+
dependencyContext;
|
|
50
|
+
name = 'deserialization-safety-gate';
|
|
51
|
+
category = 'security';
|
|
52
|
+
constructor(dependencyContext) {
|
|
53
|
+
this.dependencyContext = dependencyContext;
|
|
54
|
+
}
|
|
55
|
+
run(ctx) {
|
|
56
|
+
const { graph, language, code } = ctx;
|
|
57
|
+
// Java-only for the MVP. Other languages (Python pyyaml, JS
|
|
58
|
+
// Deserialize, Rust bincode) can extend the gate on their own
|
|
59
|
+
// manifests in follow-up scopes.
|
|
60
|
+
if (language !== 'java') {
|
|
61
|
+
return { droppedFastjson: 0, droppedJackson: 0, droppedSnakeYaml: 0 };
|
|
62
|
+
}
|
|
63
|
+
// Same sink-source discovery as SinkSemanticsPass: prefer
|
|
64
|
+
// SinkFilterResult when the real pipeline has run, otherwise fall
|
|
65
|
+
// back to the graph's initial (usually empty) sink array so
|
|
66
|
+
// stand-alone unit-test harnesses can drive the gate directly.
|
|
67
|
+
const sinks = ctx.hasResult('sink-filter')
|
|
68
|
+
? ctx.getResult('sink-filter').sinks
|
|
69
|
+
: graph.ir.taint.sinks;
|
|
70
|
+
// --- Gate A: Fastjson _noneautotype ------------------------------------
|
|
71
|
+
const pomXml = this.dependencyContext?.java?.pomXml;
|
|
72
|
+
const fastjson = pomXml ? resolveFastjsonFromPom(pomXml) : null;
|
|
73
|
+
const fastjsonHardened = fastjson?.noneAutotype === true &&
|
|
74
|
+
!fileReenablesFastjsonAutotype(code);
|
|
75
|
+
// --- Gate B: Jackson polymorphism ---------------------------------------
|
|
76
|
+
const jacksonSafe = !fileEnablesJacksonPolymorphism(code);
|
|
77
|
+
// --- Gate C: SnakeYAML SafeConstructor ----------------------------------
|
|
78
|
+
const snakeYamlSafe = fileConfiguresSnakeYamlSafely(code);
|
|
79
|
+
let droppedFastjson = 0;
|
|
80
|
+
let droppedJackson = 0;
|
|
81
|
+
let droppedSnakeYaml = 0;
|
|
82
|
+
const kept = sinks.filter((sink) => {
|
|
83
|
+
if (sink.type !== 'deserialization')
|
|
84
|
+
return true;
|
|
85
|
+
if (!sink.method)
|
|
86
|
+
return true;
|
|
87
|
+
// Gate A — Fastjson
|
|
88
|
+
if (fastjsonHardened &&
|
|
89
|
+
FASTJSON_METHODS.has(sink.method) &&
|
|
90
|
+
(sink.class === undefined || FASTJSON_CLASSES.has(sink.class))) {
|
|
91
|
+
droppedFastjson++;
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
// Gate B — Jackson
|
|
95
|
+
if (jacksonSafe &&
|
|
96
|
+
JACKSON_METHODS.has(sink.method) &&
|
|
97
|
+
sink.class !== undefined &&
|
|
98
|
+
JACKSON_CLASSES.has(sink.class)) {
|
|
99
|
+
droppedJackson++;
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
// Gate C — SnakeYAML
|
|
103
|
+
if (snakeYamlSafe &&
|
|
104
|
+
SNAKEYAML_METHODS.has(sink.method) &&
|
|
105
|
+
sink.class !== undefined &&
|
|
106
|
+
SNAKEYAML_CLASSES.has(sink.class)) {
|
|
107
|
+
droppedSnakeYaml++;
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
});
|
|
112
|
+
const totalDropped = droppedFastjson + droppedJackson + droppedSnakeYaml;
|
|
113
|
+
if (totalDropped > 0) {
|
|
114
|
+
// Mutate in place so downstream passes see the reduced sink set.
|
|
115
|
+
// Matches SinkSemanticsPass's contract.
|
|
116
|
+
sinks.length = 0;
|
|
117
|
+
sinks.push(...kept);
|
|
118
|
+
}
|
|
119
|
+
return { droppedFastjson, droppedJackson, droppedSnakeYaml };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=deserialization-safety-gate-pass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deserialization-safety-gate-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/deserialization-safety-gate-pass.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sink-filter-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/sink-filter-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAwB,MAAM,sBAAsB,CAAC;AACzG,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"sink-filter-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/sink-filter-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAwB,MAAM,sBAAsB,CAAC;AACzG,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAqmB9E,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,sBAAsB;IACtB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,UAAU,EAAE,cAAc,EAAE,CAAC;CAC9B;AAED,qBAAa,cAAe,YAAW,YAAY,CAAC,gBAAgB,CAAC;IACnE,QAAQ,CAAC,IAAI,iBAAiB;IAC9B,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,gBAAgB;CAk9BxC;AAkRD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,KAAK,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AA4I1G,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EACjC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,EACxB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,OAAO,EAAE,OAAO,EAChB,GAAG,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC,EACrB,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAC3B,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAC/B,QAAQ,CAAC,EAAE,MAAM,GAChB,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CA6E5B;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EACjC,UAAU,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,EAC3C,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,GACvB,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CA2C5B"}
|
|
@@ -118,6 +118,28 @@ const DATA_PARSER_TYPES = new Set([
|
|
|
118
118
|
'NumberFormat', 'DecimalFormat',
|
|
119
119
|
'OptionParser', 'CmdLineParser',
|
|
120
120
|
]);
|
|
121
|
+
// #257 — semantic gate promoting #155's name-based allowlist to an
|
|
122
|
+
// inverse-denylist model. Any Java receiver type name ending in
|
|
123
|
+
// `Parser` that is NOT in the eval denylist below is treated as a
|
|
124
|
+
// domain-specific DSL / query / config parser (its `.parse()` result
|
|
125
|
+
// flows into a data structure, not a script engine). The denylist
|
|
126
|
+
// enumerates the receiver types whose `.parse()` is a real
|
|
127
|
+
// code-evaluation sink and must continue to fire:
|
|
128
|
+
// - GroovyShell / GroovyClassLoader — Groovy runtime eval
|
|
129
|
+
// - ScriptEngine — JSR-223 script eval
|
|
130
|
+
// - CronParser — preserved from the explicit `code_injection` sink
|
|
131
|
+
// pattern in `config-loader.ts:1383` (schedule DSL parsing that
|
|
132
|
+
// was previously flagged as CWE-94 by design).
|
|
133
|
+
// (Spring SpelExpressionParser / Thymeleaf StandardExpressionParser
|
|
134
|
+
// end in `Parser` too, but their sinks use `method: 'parseExpression'`,
|
|
135
|
+
// not `parse`, so stage 9a's `method === 'parse'` guard never touches
|
|
136
|
+
// them regardless of what is in this set.)
|
|
137
|
+
const JAVA_EVAL_PARSER_DENYLIST = new Set([
|
|
138
|
+
'GroovyShell',
|
|
139
|
+
'GroovyClassLoader',
|
|
140
|
+
'ScriptEngine',
|
|
141
|
+
'CronParser',
|
|
142
|
+
]);
|
|
121
143
|
// #156 — compiled-template classes; risk lives at the compile step,
|
|
122
144
|
// not the render step.
|
|
123
145
|
const COMPILED_TEMPLATE_TYPES = new Set([
|
|
@@ -819,10 +841,19 @@ export class SinkFilterPass {
|
|
|
819
841
|
const method = sink.method ?? receiverMatch?.[2];
|
|
820
842
|
// 9a — #155: non-script data parsers (commonmark, hutool, zxing,
|
|
821
843
|
// CLI arg parsers, SimpleDateFormat, DecimalFormat, …).
|
|
844
|
+
// #257: generalize to any `*Parser` type not in the eval
|
|
845
|
+
// denylist — Elide's `ExpressionParser`, in-house query DSLs,
|
|
846
|
+
// config-language parsers, etc. all follow this naming.
|
|
822
847
|
if (method === 'parse' && receiver) {
|
|
823
848
|
const recvType = resolveJavaReceiverType(receiver, sink.line, sourceLines);
|
|
824
|
-
if (recvType
|
|
825
|
-
|
|
849
|
+
if (recvType) {
|
|
850
|
+
if (DATA_PARSER_TYPES.has(recvType))
|
|
851
|
+
return false;
|
|
852
|
+
if (recvType.endsWith('Parser') &&
|
|
853
|
+
!JAVA_EVAL_PARSER_DENYLIST.has(recvType)) {
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
826
857
|
}
|
|
827
858
|
// 9b — #156: compiled-template render/process. Risk lives at the
|
|
828
859
|
// compile step, not the render step.
|