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.
Files changed (45) hide show
  1. package/configs/sink-semantics.json +53 -0
  2. package/dist/analysis/config-loader.d.ts +24 -2
  3. package/dist/analysis/config-loader.d.ts.map +1 -1
  4. package/dist/analysis/config-loader.js +92 -2
  5. package/dist/analysis/config-loader.js.map +1 -1
  6. package/dist/analysis/findings.d.ts +32 -0
  7. package/dist/analysis/findings.d.ts.map +1 -1
  8. package/dist/analysis/findings.js +52 -2
  9. package/dist/analysis/findings.js.map +1 -1
  10. package/dist/analysis/passes/language-sources-pass.d.ts +19 -0
  11. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  12. package/dist/analysis/passes/language-sources-pass.js +133 -3
  13. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  14. package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
  15. package/dist/analysis/passes/scan-secrets-pass.js +37 -4
  16. package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
  17. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  18. package/dist/analysis/passes/sink-filter-pass.js +10 -1
  19. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  20. package/dist/analysis/passes/sink-semantics-pass.d.ts +52 -0
  21. package/dist/analysis/passes/sink-semantics-pass.d.ts.map +1 -0
  22. package/dist/analysis/passes/sink-semantics-pass.js +100 -0
  23. package/dist/analysis/passes/sink-semantics-pass.js.map +1 -0
  24. package/dist/analysis/passes/source-semantics-pass.d.ts +66 -0
  25. package/dist/analysis/passes/source-semantics-pass.d.ts.map +1 -0
  26. package/dist/analysis/passes/source-semantics-pass.js +165 -0
  27. package/dist/analysis/passes/source-semantics-pass.js.map +1 -0
  28. package/dist/analysis/passes/taint-propagation-pass.js +91 -10
  29. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  30. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  31. package/dist/analysis/taint-matcher.js +55 -3
  32. package/dist/analysis/taint-matcher.js.map +1 -1
  33. package/dist/analyzer.d.ts.map +1 -1
  34. package/dist/analyzer.js +14 -0
  35. package/dist/analyzer.js.map +1 -1
  36. package/dist/browser/circle-ir.js +363 -15
  37. package/dist/core/circle-ir-core.cjs +112 -8
  38. package/dist/core/circle-ir-core.js +112 -8
  39. package/dist/core/extractors/calls.js +14 -0
  40. package/dist/core/extractors/calls.js.map +1 -1
  41. package/dist/types/config.d.ts +47 -0
  42. package/dist/types/config.d.ts.map +1 -1
  43. package/dist/types/index.d.ts +43 -0
  44. package/dist/types/index.d.ts.map +1 -1
  45. 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
- // Restricted to sources with no `variable` field: an inline-pattern
1198
- // source has no enclosing assignment, so it can't be confused with an
1199
- // assignment-style source whose use happens to land on the same line
1200
- // (the latter must still respect "source precedes sink" — see the
1201
- // taint-propagation-pass "does NOT emit when source line is at or
1202
- // after sink line" regression guard).
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