circle-ir 3.141.0 → 3.144.3
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/configs/sink-semantics.json +53 -0
- package/dist/analysis/config-loader.d.ts +24 -2
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +92 -2
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/findings.d.ts +32 -0
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +52 -2
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +19 -0
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +133 -3
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.js +37 -4
- package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +10 -1
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/sink-semantics-pass.d.ts +52 -0
- package/dist/analysis/passes/sink-semantics-pass.d.ts.map +1 -0
- package/dist/analysis/passes/sink-semantics-pass.js +100 -0
- package/dist/analysis/passes/sink-semantics-pass.js.map +1 -0
- package/dist/analysis/passes/source-semantics-pass.d.ts +66 -0
- package/dist/analysis/passes/source-semantics-pass.d.ts.map +1 -0
- package/dist/analysis/passes/source-semantics-pass.js +165 -0
- package/dist/analysis/passes/source-semantics-pass.js.map +1 -0
- package/dist/analysis/passes/taint-propagation-pass.js +91 -10
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +55 -3
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +14 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +363 -15
- package/dist/core/circle-ir-core.cjs +112 -8
- package/dist/core/circle-ir-core.js +112 -8
- package/dist/core/extractors/calls.js +14 -0
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/types/config.d.ts +47 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +43 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SinkSemanticsPass — cognium-dev #139 Tier A
|
|
3
|
+
*
|
|
4
|
+
* Consults a curated `<ClassName>#<methodName>` → `real_class` +
|
|
5
|
+
* `overrides` registry (`configs/sink-semantics.json`) and drops
|
|
6
|
+
* sinks whose emitted `SinkType` label disagrees with the registry's
|
|
7
|
+
* declared real-behavior classification.
|
|
8
|
+
*
|
|
9
|
+
* Motivation: the taint-matcher's `configs/sinks/*.yaml` patterns
|
|
10
|
+
* often use method-only matches (no class filter) which produce FPs
|
|
11
|
+
* when unrelated classes happen to share a method name. Canonical
|
|
12
|
+
* example:
|
|
13
|
+
*
|
|
14
|
+
* public byte[] get(byte[] key) {
|
|
15
|
+
* return connection.executeCommand(commandObjects.get(key));
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* The `executeCommand` pattern in `configs/sinks/command.yaml` has no
|
|
19
|
+
* `class` filter, so `Jedis.executeCommand(...)` — Redis wire-protocol
|
|
20
|
+
* serialization — is flagged as `command_injection`. The registry
|
|
21
|
+
* fixes this by listing `Jedis#executeCommand → drop
|
|
22
|
+
* command_injection` (with `real_class: db_protocol` as a documenting
|
|
23
|
+
* label).
|
|
24
|
+
*
|
|
25
|
+
* The gate is deliberately narrow (~8 seed entries as of 3.144.0);
|
|
26
|
+
* each entry is class-scoped so `Runtime.exec`, `ProcessBuilder.start`,
|
|
27
|
+
* `Statement.execute`, `Class.forName`, and `Method.invoke` remain
|
|
28
|
+
* unaffected. Unresolved receivers (`sink.class === undefined`) fall
|
|
29
|
+
* through — the gate is false-negative-safe.
|
|
30
|
+
*
|
|
31
|
+
* Pipeline slot: runs after `SinkFilterPass` (so unrelated FP
|
|
32
|
+
* suppressions have already fired) and before `TaintPropagationPass`
|
|
33
|
+
* (so the flow generators never see the dropped sinks).
|
|
34
|
+
*
|
|
35
|
+
* Tier B (speculative verifier on disagreements) is explicitly OUT
|
|
36
|
+
* of scope for circle-ir. Any speculative-verification layer belongs
|
|
37
|
+
* in cognium-ai / circle-ir-ai; results from that layer can be
|
|
38
|
+
* promoted to Tier A registry entries by hand.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Build a signature → overrides lookup map from a flat entry list.
|
|
42
|
+
* Signature format: `<ClassName>#<methodName>` (simple names only;
|
|
43
|
+
* case-sensitive).
|
|
44
|
+
*/
|
|
45
|
+
function buildRegistry(entries) {
|
|
46
|
+
const registry = new Map();
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
const existing = registry.get(entry.signature);
|
|
49
|
+
if (existing) {
|
|
50
|
+
// Last-write-wins for duplicate signatures; also union the
|
|
51
|
+
// overrides so multiple files can extend the same signature.
|
|
52
|
+
for (const t of entry.overrides)
|
|
53
|
+
existing.add(t);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
registry.set(entry.signature, new Set(entry.overrides));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return registry;
|
|
60
|
+
}
|
|
61
|
+
export class SinkSemanticsPass {
|
|
62
|
+
name = 'sink-semantics';
|
|
63
|
+
category = 'security';
|
|
64
|
+
run(ctx) {
|
|
65
|
+
const { graph, config } = ctx;
|
|
66
|
+
const entries = config.sinkSemantics ?? [];
|
|
67
|
+
if (entries.length === 0) {
|
|
68
|
+
// No registry loaded — nothing to do. Preserves legacy callers
|
|
69
|
+
// that construct a TaintConfig without `sinkSemantics`.
|
|
70
|
+
return { droppedCount: 0, registrySize: 0 };
|
|
71
|
+
}
|
|
72
|
+
const registry = buildRegistry(entries);
|
|
73
|
+
const sinks = graph.ir.taint.sinks;
|
|
74
|
+
let droppedCount = 0;
|
|
75
|
+
const kept = sinks.filter((sink) => {
|
|
76
|
+
// Unresolved receiver → registry cannot apply. Preserve the
|
|
77
|
+
// sink so the normal flow generator still processes it.
|
|
78
|
+
if (!sink.class || !sink.method)
|
|
79
|
+
return true;
|
|
80
|
+
const signature = `${sink.class}#${sink.method}`;
|
|
81
|
+
const overrides = registry.get(signature);
|
|
82
|
+
if (!overrides)
|
|
83
|
+
return true;
|
|
84
|
+
if (overrides.has(sink.type)) {
|
|
85
|
+
droppedCount++;
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
});
|
|
90
|
+
// Mutate in place so downstream passes (TaintPropagationPass)
|
|
91
|
+
// see the reduced sink set. Preserves array identity for any
|
|
92
|
+
// consumer that captured a reference before this pass ran.
|
|
93
|
+
if (droppedCount > 0) {
|
|
94
|
+
sinks.length = 0;
|
|
95
|
+
sinks.push(...kept);
|
|
96
|
+
}
|
|
97
|
+
return { droppedCount, registrySize: registry.size };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=sink-semantics-pass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sink-semantics-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/sink-semantics-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAaH;;;;GAIG;AACH,SAAS,aAAa,CACpB,OAAsC;IAEtC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;IAClD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC/C,IAAI,QAAQ,EAAE,CAAC;YACb,2DAA2D;YAC3D,6DAA6D;YAC7D,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,SAAS;gBAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACnD,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,OAAO,iBAAiB;IAGnB,IAAI,GAAG,gBAAgB,CAAC;IACxB,QAAQ,GAAG,UAAmB,CAAC;IAExC,GAAG,CAAC,GAAgB;QAClB,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC;QAC9B,MAAM,OAAO,GAAG,MAAM,CAAC,aAAa,IAAI,EAAE,CAAC;QAC3C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,+DAA+D;YAC/D,wDAAwD;YACxD,OAAO,EAAE,YAAY,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;QAC9C,CAAC;QAED,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC;QAEnC,IAAI,YAAY,GAAG,CAAC,CAAC;QACrB,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;YACjC,4DAA4D;YAC5D,wDAAwD;YACxD,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM;gBAAE,OAAO,IAAI,CAAC;YAC7C,MAAM,SAAS,GAAG,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACjD,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAC1C,IAAI,CAAC,SAAS;gBAAE,OAAO,IAAI,CAAC;YAC5B,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC7B,YAAY,EAAE,CAAC;gBACf,OAAO,KAAK,CAAC;YACf,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;QAEH,8DAA8D;QAC9D,6DAA6D;QAC7D,2DAA2D;QAC3D,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;YACrB,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;YACjB,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QACtB,CAAC;QAED,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;IACvD,CAAC;CACF"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SourceSemanticsPass — cognium-dev #138
|
|
3
|
+
*
|
|
4
|
+
* Tags every entry in `graph.ir.taint.sources` with three optional
|
|
5
|
+
* booleans (`constant`, `spi`, `demoPath`) that downstream passes use
|
|
6
|
+
* to drop or downgrade false-positive taint flows:
|
|
7
|
+
*
|
|
8
|
+
* Filter 1 — Constant folding
|
|
9
|
+
* `String API_KEY = "abc";` / `static final String KEY = "abc";` /
|
|
10
|
+
* `String v = SomeEnum.VALUE;` → `constant = true`.
|
|
11
|
+
* A compile-time constant string cannot carry attacker-controlled
|
|
12
|
+
* data, so it is dropped for every taint sink type. The
|
|
13
|
+
* hardcoded-credential rule continues to fire (that is precisely
|
|
14
|
+
* the point of that rule).
|
|
15
|
+
*
|
|
16
|
+
* Filter 2 — SPI (Service Provider Interface)
|
|
17
|
+
* `ServiceLoader.load(Plugin.class)` /
|
|
18
|
+
* `ServiceLoader.loadInstalled(...)` / `ServiceLoader.stream(...)` /
|
|
19
|
+
* `Class.forName(name)` co-located with a
|
|
20
|
+
* `.getResources("META-INF/services/…")` lookup within ±30 lines
|
|
21
|
+
* of the same method → `spi = true`. SPI-loaded values are
|
|
22
|
+
* provider-controlled configuration, not attacker input.
|
|
23
|
+
*
|
|
24
|
+
* Filter 3 — Demo path
|
|
25
|
+
* Path components matching `demo`, `example(s)`, `samples`,
|
|
26
|
+
* `integration-tests`, or `integration_tests` (case-insensitive)
|
|
27
|
+
* → `demoPath = true` on every source in the file. This tag is
|
|
28
|
+
* never used to drop flows — `scan-secrets-pass` consumes it to
|
|
29
|
+
* downgrade hardcoded-credential findings on demo paths from
|
|
30
|
+
* `high` → `info` and `warning/error` → `note`.
|
|
31
|
+
*
|
|
32
|
+
* This pass is a pure source-tagger: it never emits SAST findings and
|
|
33
|
+
* never removes sources from `graph.ir.taint.sources`. All
|
|
34
|
+
* consumption is downstream (findings.ts, taint-propagation-pass,
|
|
35
|
+
* scan-secrets-pass).
|
|
36
|
+
*
|
|
37
|
+
* Consumption policy is documented on the `TaintSource` type
|
|
38
|
+
* (`src/types/index.ts`) and in `findings.ts:sourceSemanticsAllowed`.
|
|
39
|
+
*
|
|
40
|
+
* Pipeline slot: runs after `LanguageSourcesPass` (so the full source
|
|
41
|
+
* list is available) and before `TaintPropagationPass` (so the tags
|
|
42
|
+
* are visible to flow generation).
|
|
43
|
+
*/
|
|
44
|
+
import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
|
|
45
|
+
/**
|
|
46
|
+
* Path-component regex used by both this pass (source tagging) and
|
|
47
|
+
* `scan-secrets-pass` (severity downgrade). Matches when any path
|
|
48
|
+
* segment equals one of the demo/example/samples/integration-tests
|
|
49
|
+
* tokens, case-insensitive. A bare filename like `DemoParser.java`
|
|
50
|
+
* does NOT match (no leading or trailing path separator).
|
|
51
|
+
*/
|
|
52
|
+
export declare const DEMO_PATH_RE: RegExp;
|
|
53
|
+
export interface SourceSemanticsResult {
|
|
54
|
+
/** Number of sources tagged with `constant = true`. */
|
|
55
|
+
constantCount: number;
|
|
56
|
+
/** Number of sources tagged with `spi = true`. */
|
|
57
|
+
spiCount: number;
|
|
58
|
+
/** Number of sources tagged with `demoPath = true`. */
|
|
59
|
+
demoPathCount: number;
|
|
60
|
+
}
|
|
61
|
+
export declare class SourceSemanticsPass implements AnalysisPass<SourceSemanticsResult> {
|
|
62
|
+
readonly name = "source-semantics";
|
|
63
|
+
readonly category: "security";
|
|
64
|
+
run(ctx: PassContext): SourceSemanticsResult;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=source-semantics-pass.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-semantics-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/source-semantics-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAG9E;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,QACiE,CAAC;AA2F3F,MAAM,WAAW,qBAAqB;IACpC,uDAAuD;IACvD,aAAa,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,mBACX,YAAW,YAAY,CAAC,qBAAqB,CAAC;IAE9C,QAAQ,CAAC,IAAI,sBAAsB;IACnC,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,qBAAqB;CAgC7C"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SourceSemanticsPass — cognium-dev #138
|
|
3
|
+
*
|
|
4
|
+
* Tags every entry in `graph.ir.taint.sources` with three optional
|
|
5
|
+
* booleans (`constant`, `spi`, `demoPath`) that downstream passes use
|
|
6
|
+
* to drop or downgrade false-positive taint flows:
|
|
7
|
+
*
|
|
8
|
+
* Filter 1 — Constant folding
|
|
9
|
+
* `String API_KEY = "abc";` / `static final String KEY = "abc";` /
|
|
10
|
+
* `String v = SomeEnum.VALUE;` → `constant = true`.
|
|
11
|
+
* A compile-time constant string cannot carry attacker-controlled
|
|
12
|
+
* data, so it is dropped for every taint sink type. The
|
|
13
|
+
* hardcoded-credential rule continues to fire (that is precisely
|
|
14
|
+
* the point of that rule).
|
|
15
|
+
*
|
|
16
|
+
* Filter 2 — SPI (Service Provider Interface)
|
|
17
|
+
* `ServiceLoader.load(Plugin.class)` /
|
|
18
|
+
* `ServiceLoader.loadInstalled(...)` / `ServiceLoader.stream(...)` /
|
|
19
|
+
* `Class.forName(name)` co-located with a
|
|
20
|
+
* `.getResources("META-INF/services/…")` lookup within ±30 lines
|
|
21
|
+
* of the same method → `spi = true`. SPI-loaded values are
|
|
22
|
+
* provider-controlled configuration, not attacker input.
|
|
23
|
+
*
|
|
24
|
+
* Filter 3 — Demo path
|
|
25
|
+
* Path components matching `demo`, `example(s)`, `samples`,
|
|
26
|
+
* `integration-tests`, or `integration_tests` (case-insensitive)
|
|
27
|
+
* → `demoPath = true` on every source in the file. This tag is
|
|
28
|
+
* never used to drop flows — `scan-secrets-pass` consumes it to
|
|
29
|
+
* downgrade hardcoded-credential findings on demo paths from
|
|
30
|
+
* `high` → `info` and `warning/error` → `note`.
|
|
31
|
+
*
|
|
32
|
+
* This pass is a pure source-tagger: it never emits SAST findings and
|
|
33
|
+
* never removes sources from `graph.ir.taint.sources`. All
|
|
34
|
+
* consumption is downstream (findings.ts, taint-propagation-pass,
|
|
35
|
+
* scan-secrets-pass).
|
|
36
|
+
*
|
|
37
|
+
* Consumption policy is documented on the `TaintSource` type
|
|
38
|
+
* (`src/types/index.ts`) and in `findings.ts:sourceSemanticsAllowed`.
|
|
39
|
+
*
|
|
40
|
+
* Pipeline slot: runs after `LanguageSourcesPass` (so the full source
|
|
41
|
+
* list is available) and before `TaintPropagationPass` (so the tags
|
|
42
|
+
* are visible to flow generation).
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Path-component regex used by both this pass (source tagging) and
|
|
46
|
+
* `scan-secrets-pass` (severity downgrade). Matches when any path
|
|
47
|
+
* segment equals one of the demo/example/samples/integration-tests
|
|
48
|
+
* tokens, case-insensitive. A bare filename like `DemoParser.java`
|
|
49
|
+
* does NOT match (no leading or trailing path separator).
|
|
50
|
+
*/
|
|
51
|
+
export const DEMO_PATH_RE = /(?:^|\/)(?:demo|example|examples|samples|integration-tests|integration_tests)(?:\/|$)/i;
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Filter 1 — Constant
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// `[final ] TYPE ident = "string literal";` — canonical simple form.
|
|
56
|
+
// Allows generics / arrays in the type position (`List<String>`, `String[]`).
|
|
57
|
+
const CONST_STRING_ASSIGN_RE = /^\s*(?:final\s+|static\s+final\s+)?[A-Za-z_][\w.<>\[\]]*\s+[A-Za-z_]\w*\s*=\s*"[^"]*"\s*;?\s*$/;
|
|
58
|
+
// `[public|private|protected ] static final TYPE ident = <literal-or-simple-expr>`
|
|
59
|
+
// The RHS check gates on either a string literal, a numeric/boolean literal,
|
|
60
|
+
// or a bare identifier reference (constant reference chain). We intentionally
|
|
61
|
+
// accept the presence of `static final` as a strong signal — Java's language
|
|
62
|
+
// rules already ensure the value is compile-time constant or effectively so.
|
|
63
|
+
const STATIC_FINAL_RE = /^\s*(?:public\s+|private\s+|protected\s+)?static\s+final\s+/;
|
|
64
|
+
// `ident = SomeEnum.VALUE;` — enum constant reference. Requires an
|
|
65
|
+
// UPPER_CASE constant identifier after the dot to avoid matching
|
|
66
|
+
// regular field access like `user.name`. The type identifier (LHS of
|
|
67
|
+
// the dot) is allowed to be either PascalCase (`SomeEnum`) or
|
|
68
|
+
// SCREAMING_SNAKE (`SOME_ENUM`) — both are common in Java / Kotlin
|
|
69
|
+
// enum types.
|
|
70
|
+
const ENUM_CONST_REF_RE = /=\s*[A-Z][A-Za-z0-9_]*\.[A-Z][A-Z0-9_]*\s*;?\s*$/;
|
|
71
|
+
function isConstantSource(code) {
|
|
72
|
+
if (!code)
|
|
73
|
+
return false;
|
|
74
|
+
if (CONST_STRING_ASSIGN_RE.test(code))
|
|
75
|
+
return true;
|
|
76
|
+
if (STATIC_FINAL_RE.test(code)) {
|
|
77
|
+
// For static final, require the RHS to look like a literal or a
|
|
78
|
+
// simple constant reference (no method calls / no `new`).
|
|
79
|
+
const rhs = code.split('=').slice(1).join('=').trim();
|
|
80
|
+
if (rhs.length === 0)
|
|
81
|
+
return false;
|
|
82
|
+
if (/^"[^"]*"\s*;?\s*$/.test(rhs))
|
|
83
|
+
return true; // string literal
|
|
84
|
+
if (/^-?\d+(?:\.\d+)?[fFdDlL]?\s*;?\s*$/.test(rhs))
|
|
85
|
+
return true; // numeric
|
|
86
|
+
if (/^(?:true|false)\s*;?\s*$/.test(rhs))
|
|
87
|
+
return true; // boolean
|
|
88
|
+
if (/^[A-Za-z_][\w.]*\s*;?\s*$/.test(rhs))
|
|
89
|
+
return true; // ident ref
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (ENUM_CONST_REF_RE.test(code))
|
|
93
|
+
return true;
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Filter 2 — SPI
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
const SERVICE_LOADER_RE = /\bServiceLoader\.(?:load|loadInstalled|stream)\s*\(/;
|
|
100
|
+
const CLASS_FOR_NAME_RE = /\bClass\.forName\s*\(/;
|
|
101
|
+
const META_INF_SERVICES_RE = /getResources?\s*\(\s*"META-INF\/services\//;
|
|
102
|
+
/**
|
|
103
|
+
* SPI window size (lines). Matches the ±30-line window that
|
|
104
|
+
* `sink-filter-pass.ts` Stage 9f uses for the same co-location check.
|
|
105
|
+
*/
|
|
106
|
+
const SPI_WINDOW = 30;
|
|
107
|
+
function isSpiSource(source, lines) {
|
|
108
|
+
const code = source.code;
|
|
109
|
+
if (!code)
|
|
110
|
+
return false;
|
|
111
|
+
if (SERVICE_LOADER_RE.test(code))
|
|
112
|
+
return true;
|
|
113
|
+
if (CLASS_FOR_NAME_RE.test(code)) {
|
|
114
|
+
// Check for META-INF/services lookup within ±SPI_WINDOW lines of
|
|
115
|
+
// the source line. `source.line` is 1-indexed.
|
|
116
|
+
const start = Math.max(0, source.line - 1 - SPI_WINDOW);
|
|
117
|
+
const end = Math.min(lines.length, source.line - 1 + SPI_WINDOW + 1);
|
|
118
|
+
for (let i = start; i < end; i++) {
|
|
119
|
+
if (META_INF_SERVICES_RE.test(lines[i]))
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Filter 3 — DemoPath
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
function isDemoPathFile(file) {
|
|
129
|
+
if (!file)
|
|
130
|
+
return false;
|
|
131
|
+
return DEMO_PATH_RE.test(file);
|
|
132
|
+
}
|
|
133
|
+
export class SourceSemanticsPass {
|
|
134
|
+
name = 'source-semantics';
|
|
135
|
+
category = 'security';
|
|
136
|
+
run(ctx) {
|
|
137
|
+
const { graph, code } = ctx;
|
|
138
|
+
const sources = graph.ir.taint.sources;
|
|
139
|
+
if (sources.length === 0) {
|
|
140
|
+
return { constantCount: 0, spiCount: 0, demoPathCount: 0 };
|
|
141
|
+
}
|
|
142
|
+
const file = graph.ir.meta.file;
|
|
143
|
+
const demoPath = isDemoPathFile(file);
|
|
144
|
+
const lines = code.split('\n');
|
|
145
|
+
let constantCount = 0;
|
|
146
|
+
let spiCount = 0;
|
|
147
|
+
let demoPathCount = 0;
|
|
148
|
+
for (const source of sources) {
|
|
149
|
+
if (isConstantSource(source.code)) {
|
|
150
|
+
source.constant = true;
|
|
151
|
+
constantCount++;
|
|
152
|
+
}
|
|
153
|
+
if (isSpiSource(source, lines)) {
|
|
154
|
+
source.spi = true;
|
|
155
|
+
spiCount++;
|
|
156
|
+
}
|
|
157
|
+
if (demoPath) {
|
|
158
|
+
source.demoPath = true;
|
|
159
|
+
demoPathCount++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { constantCount, spiCount, demoPathCount };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=source-semantics-pass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-semantics-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/source-semantics-pass.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { propagateTaint } from '../taint-propagation.js';
|
|
14
14
|
import { isFalsePositive, isCorrelatedPredicateFP } from '../constant-propagation.js';
|
|
15
|
-
import { buildPythonTaintedVars, buildRustTaintedVars } from './language-sources-pass.js';
|
|
16
|
-
import { canSourceReachSink } from '../findings.js';
|
|
15
|
+
import { buildJavaTaintedVars, buildPythonTaintedVars, buildRustTaintedVars } from './language-sources-pass.js';
|
|
16
|
+
import { canSourceReachSink, sourceSemanticsAllowed } from '../findings.js';
|
|
17
17
|
export class TaintPropagationPass {
|
|
18
18
|
name = 'taint-propagation';
|
|
19
19
|
category = 'security';
|
|
@@ -1076,6 +1076,36 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
1076
1076
|
}
|
|
1077
1077
|
}
|
|
1078
1078
|
}
|
|
1079
|
+
// Java alias expansion (cognium-dev #220): mirror the Rust branch so
|
|
1080
|
+
// that the array-form `Runtime.exec(String[])` shape
|
|
1081
|
+
// String cmd = "echo " + arg;
|
|
1082
|
+
// Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd});
|
|
1083
|
+
// produces a flow back to `arg`. Without this, the variable-scan below
|
|
1084
|
+
// never sees `cmd` (only `arg` is in the source's variable field) and
|
|
1085
|
+
// the array literal defeats the sink-arg colocation heuristic. Seeds
|
|
1086
|
+
// with real source variables (HTTP source `arg` or the now-populated
|
|
1087
|
+
// interprocedural_param parameter name) and iterates to a fixpoint.
|
|
1088
|
+
if (language === 'java' && typeof code === 'string' && sourcesWithVar.length > 0) {
|
|
1089
|
+
const seedVars = new Set(sourcesWithVar.map(s => s.variable));
|
|
1090
|
+
const derived = buildJavaTaintedVars(code, seedVars);
|
|
1091
|
+
if (derived.size > 0) {
|
|
1092
|
+
let anchor = sourcesWithVar[0];
|
|
1093
|
+
for (const s of sourcesWithVar) {
|
|
1094
|
+
if (s.line < anchor.line)
|
|
1095
|
+
anchor = s;
|
|
1096
|
+
}
|
|
1097
|
+
const existingVars = new Set(sourcesWithVar.map(s => s.variable));
|
|
1098
|
+
for (const [varName] of derived) {
|
|
1099
|
+
if (!varName || existingVars.has(varName))
|
|
1100
|
+
continue;
|
|
1101
|
+
sourcesWithVar.push({
|
|
1102
|
+
...anchor,
|
|
1103
|
+
variable: varName,
|
|
1104
|
+
});
|
|
1105
|
+
existingVars.add(varName);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1079
1109
|
// Pre-compile word-boundary regexes per unique source variable.
|
|
1080
1110
|
// Escape regex-special characters defensively (variable names should be
|
|
1081
1111
|
// plain identifiers but Python attribute paths like `obj.attr` could leak in).
|
|
@@ -1128,6 +1158,14 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
1128
1158
|
const re = reCache.get(source.variable);
|
|
1129
1159
|
if (!re || !re.test(expr))
|
|
1130
1160
|
continue;
|
|
1161
|
+
// cognium-dev #138: source-semantics gate. Drop flows whose
|
|
1162
|
+
// source was tagged constant / SPI-loaded by
|
|
1163
|
+
// SourceSemanticsPass — a compile-time constant cannot carry
|
|
1164
|
+
// attacker-controlled data, and SPI-loaded values are
|
|
1165
|
+
// provider-controlled configuration (see the predicate's
|
|
1166
|
+
// JSDoc in findings.ts for the exact per-sink policy).
|
|
1167
|
+
if (!sourceSemanticsAllowed(source, sink.type))
|
|
1168
|
+
continue;
|
|
1131
1169
|
// Dedupe by (source_line, sink_line, sink.type) — a single source
|
|
1132
1170
|
// can reach multiple distinct sinks at the same line (e.g. an
|
|
1133
1171
|
// execute() call modeled as both `xss` and `sql_injection`).
|
|
@@ -1194,16 +1232,21 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
1194
1232
|
// for the simple cases where the source pattern matches the iterable
|
|
1195
1233
|
// and the loop variable is used on the same line.
|
|
1196
1234
|
//
|
|
1197
|
-
//
|
|
1198
|
-
//
|
|
1199
|
-
//
|
|
1200
|
-
//
|
|
1201
|
-
//
|
|
1202
|
-
//
|
|
1235
|
+
// Sources without a `variable` field are the classic inline-pattern case
|
|
1236
|
+
// (e.g. `exec(req.getParameter("u"))`). Sources WITH a `variable` field
|
|
1237
|
+
// are also admitted when the LHS-bound identifier does NOT appear in the
|
|
1238
|
+
// sink call's source-line `code`. This covers Sprint 93 (#189)
|
|
1239
|
+
// `Object o = y.load(req.getParameter("y"))` where Java LHS-binding tags
|
|
1240
|
+
// the source with the outer assignment target `o`, yet `o` cannot appear
|
|
1241
|
+
// in the sink's own arguments because it is being *defined by* the sink
|
|
1242
|
+
// expression. The variable-scan path already emits when the sink args
|
|
1243
|
+
// reference the source var, so gating on absence of the var in the sink
|
|
1244
|
+
// code preserves the "assignment-after-use is impossible" regression
|
|
1245
|
+
// guard (`uid = source(); doSink("x = " + uid)` on the same line would
|
|
1246
|
+
// still have `uid` present in the sink code and thus fall to
|
|
1247
|
+
// variable-scan, not colocation).
|
|
1203
1248
|
const sourcesByLine = new Map();
|
|
1204
1249
|
for (const s of sources) {
|
|
1205
|
-
if (s.variable && s.variable.length > 0)
|
|
1206
|
-
continue;
|
|
1207
1250
|
const arr = sourcesByLine.get(s.line) ?? [];
|
|
1208
1251
|
arr.push(s);
|
|
1209
1252
|
sourcesByLine.set(s.line, arr);
|
|
@@ -1217,6 +1260,44 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
1217
1260
|
for (const source of colocSources) {
|
|
1218
1261
|
if (!canSourceReachSink(source.type, sink.type))
|
|
1219
1262
|
continue;
|
|
1263
|
+
// cognium-dev #138: source-semantics gate for inline sources.
|
|
1264
|
+
// Drop flows tagged constant / SPI (see sourceSemanticsAllowed
|
|
1265
|
+
// JSDoc for the per-sink policy).
|
|
1266
|
+
if (!sourceSemanticsAllowed(source, sink.type))
|
|
1267
|
+
continue;
|
|
1268
|
+
// Variable-scan handoff — if the source carries an LHS-bound
|
|
1269
|
+
// variable AND that identifier is textually present in the RHS of
|
|
1270
|
+
// the sink's own source-line code, the variable-scan path is
|
|
1271
|
+
// authoritative for this pair. Skipping the colocation emission
|
|
1272
|
+
// here preserves the "assignment-then-same-line-use is impossible"
|
|
1273
|
+
// regression guard (`uid = source(); doSink(uid)` collapsed onto
|
|
1274
|
+
// one line). When the LHS var is absent from the RHS (i.e. the
|
|
1275
|
+
// sink expression IS the RHS being assigned to that var), the
|
|
1276
|
+
// source is nested inside the sink expression (Sprint 93 (#189)
|
|
1277
|
+
// nested `Object o = y.load(req.getParameter("y"))` pattern) and
|
|
1278
|
+
// the colocation emission is the only path that will surface the
|
|
1279
|
+
// flow. We strip an optional leading `TYPE ident =` prefix from
|
|
1280
|
+
// the sink code so the LHS binding of THIS line does not
|
|
1281
|
+
// spuriously match the source var.
|
|
1282
|
+
const sourceVar = source.variable;
|
|
1283
|
+
if (sourceVar && sourceVar.length > 0) {
|
|
1284
|
+
const sinkCode = sink.code;
|
|
1285
|
+
if (!sinkCode) {
|
|
1286
|
+
// Conservative fallback: without sink source-line text we
|
|
1287
|
+
// cannot verify the nested-source shape, so preserve the
|
|
1288
|
+
// pre-Sprint-93 behaviour of dropping variable-bound sources
|
|
1289
|
+
// from colocation. The variable-scan supplement remains
|
|
1290
|
+
// authoritative for these cases.
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
// Strip a possible declaration/assignment LHS: `TYPE var =` or
|
|
1294
|
+
// `var =`. Matches single `=` only (not ==, !=, <=, >=).
|
|
1295
|
+
const assignMatch = sinkCode.match(/^\s*(?:[A-Za-z_][\w.<>[\]\s,?]*\s+)?[A-Za-z_]\w*\s*=(?!=)\s*/);
|
|
1296
|
+
const rhs = assignMatch ? sinkCode.slice(assignMatch[0].length) : sinkCode;
|
|
1297
|
+
if (new RegExp(`\\b${sourceVar}\\b`).test(rhs)) {
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1220
1301
|
// Skip the degenerate `file_input` → `path_traversal` colocation
|
|
1221
1302
|
// where the source and sink describe the SAME call (one being the
|
|
1222
1303
|
// chained accessor of the other). Example: Python
|