circle-ir 4.9.15 → 4.9.17

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.
@@ -1 +1 @@
1
- {"version":3,"file":"library-profile-sink-gate-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/library-profile-sink-gate-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC9E,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAA4B,MAAM,sBAAsB,CAAC;AAY3G,MAAM,WAAW,4BAA4B;IAC3C;;;;OAIG;IACH,OAAO,EAAE,cAAc,GAAG,SAAS,CAAC;IACpC;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;CAClD;AAaD,qBAAa,0BACX,YAAW,YAAY,CAAC,4BAA4B,CAAC;IAErD,QAAQ,CAAC,IAAI,+BAA+B;IAC5C,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,4BAA4B;CA0DpD;AAcD,MAAM,WAAW,iCAAiC;IAChD;;;;OAIG;IACH,OAAO,EAAE,cAAc,GAAG,SAAS,CAAC;IACpC;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,mBAAmB,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;CAC1D;AAED,qBAAa,+BACX,YAAW,YAAY,CAAC,iCAAiC,CAAC;IAE1D,QAAQ,CAAC,IAAI,qCAAqC;IAClD,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,iCAAiC;CAuDzD"}
1
+ {"version":3,"file":"library-profile-sink-gate-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/library-profile-sink-gate-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAG9E,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAA4B,MAAM,sBAAsB,CAAC;AAY3G,MAAM,WAAW,4BAA4B;IAC3C;;;;OAIG;IACH,OAAO,EAAE,cAAc,GAAG,SAAS,CAAC;IACpC;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;CAClD;AAaD,qBAAa,0BACX,YAAW,YAAY,CAAC,4BAA4B,CAAC;IAErD,QAAQ,CAAC,IAAI,+BAA+B;IAC5C,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,4BAA4B;CA0DpD;AAcD,MAAM,WAAW,iCAAiC;IAChD;;;;OAIG;IACH,OAAO,EAAE,cAAc,GAAG,SAAS,CAAC;IACpC;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,mBAAmB,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;CAC1D;AAED,qBAAa,+BACX,YAAW,YAAY,CAAC,iCAAiC,CAAC;IAE1D,QAAQ,CAAC,IAAI,qCAAqC;IAClD,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,iCAAiC;CAwFzD"}
@@ -68,6 +68,7 @@
68
68
  * to a follow-up if the harness rerun shows material Rust
69
69
  * residuals; the Tier 2 8-repo cohort is Java-heavy.
70
70
  */
71
+ import { LIBRARY_API_SURFACE_TAG } from '../library-api-surface-downgrade.js';
71
72
  /**
72
73
  * Sink types eligible for the library-profile drop. Seeded with
73
74
  * `log_injection` (CWE-117) in 3.152.0. Extending this set is a
@@ -167,9 +168,22 @@ export class LibraryProfileCwe22PathGatePass {
167
168
  droppedBySourceType: {},
168
169
  };
169
170
  }
170
- // `TaintPropagationPass` / `InterproceduralPass` populate
171
- // `graph.ir.taint.flows`. Nothing to filter if no flow ever ran.
172
- const flows = graph.ir.taint.flows;
171
+ // cognium-dev#288 — the authoritative flow list is
172
+ // `InterproceduralPassResult.additionalFlows`, which is what the
173
+ // post-pipeline rebuild assigns to `taint.flows`. The comment here used to
174
+ // claim `TaintPropagationPass` / `InterproceduralPass` populate
175
+ // `graph.ir.taint.flows`; they do not — nothing ever assigns it, so this
176
+ // pass read `undefined` and returned early on every run.
177
+ //
178
+ // With the source gate now effective BEFORE flow generation (#288), no
179
+ // flow from a gated source should reach this point at all. That makes this
180
+ // a genuine belt-and-suspenders net rather than the primary mechanism —
181
+ // but it is wired correctly now, so if a new flow-producing path ever
182
+ // bypasses the source list it will still be caught instead of silently
183
+ // doing nothing.
184
+ const flows = ctx.hasResult('interprocedural')
185
+ ? ctx.getResult('interprocedural').additionalFlows
186
+ : graph.ir.taint.flows;
173
187
  if (!flows || flows.length === 0) {
174
188
  return {
175
189
  profile,
@@ -178,24 +192,45 @@ export class LibraryProfileCwe22PathGatePass {
178
192
  droppedBySourceType: {},
179
193
  };
180
194
  }
195
+ // cognium-dev#288 (option A) — TAG, do not delete.
196
+ //
197
+ // Previously this spliced matching flows out of the list. A flow removed
198
+ // here is unrecoverable downstream: the finding simply never exists, with
199
+ // nothing in the output to say a library-shape judgement was applied. That
200
+ // is the wrong default for the deterministic layer, whose job is to expose
201
+ // signals for adjudication (CLAUDE.md), and it is inconsistent with
202
+ // ADR-008, which keeps such findings and downgrades them.
203
+ //
204
+ // `TaintFlowInfo.tags` exists for exactly this (3.105.0): the
205
+ // `library-api-surface:caller-responsibility` tag is already understood by
206
+ // CLI/SARIF consumers as "downgrade and badge". So the flow survives,
207
+ // carrying the reason it was judged speculative.
208
+ //
209
+ // Note for whoever revisits the policy: ADR-008's
210
+ // `DOWNGRADE_ELIGIBLE_RULE_IDS` deliberately EXCLUDES `path_traversal`
211
+ // (only code_injection / template_injection / xpath_injection /
212
+ // sql_injection are eligible), on the reasoning that a library with a path
213
+ // traversal is a bug whatever its shape. So tagging a CWE-22 flow does not
214
+ // downgrade it under `applyProjectProfileTransform` today — the tag is
215
+ // carried, and the severity policy stays where ADR-008 put it. Deleting
216
+ // these flows, as this pass used to, contradicted that decision outright.
181
217
  const droppedBySourceType = {};
182
- const kept = [];
218
+ let tagged = 0;
183
219
  for (const flow of flows) {
184
220
  if (flow.sink_type === 'path_traversal' &&
185
221
  CWE22_SPECULATIVE_SOURCE_TYPES.has(flow.source_type)) {
186
222
  droppedBySourceType[flow.source_type] =
187
223
  (droppedBySourceType[flow.source_type] ?? 0) + 1;
188
- continue;
224
+ const tags = flow.tags ?? [];
225
+ if (!tags.includes(LIBRARY_API_SURFACE_TAG)) {
226
+ flow.tags = [...tags, LIBRARY_API_SURFACE_TAG];
227
+ }
228
+ tagged++;
189
229
  }
190
- kept.push(flow);
191
- }
192
- const dropped = flows.length - kept.length;
193
- // Mutate the flow array in place so downstream consumers
194
- // (`CrossFilePass`, SARIF writer) see the filtered list.
195
- if (dropped > 0) {
196
- flows.length = 0;
197
- flows.push(...kept);
198
230
  }
231
+ // `dropped` keeps its name for result-shape compatibility but now reports
232
+ // how many flows were TAGGED; none are removed.
233
+ const dropped = tagged;
199
234
  return {
200
235
  profile,
201
236
  applied: true,
@@ -1 +1 @@
1
- {"version":3,"file":"library-profile-sink-gate-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/library-profile-sink-gate-pass.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"library-profile-sink-gate-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/library-profile-sink-gate-pass.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"library-profile-source-gate-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/library-profile-source-gate-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC9E,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAavE,MAAM,WAAW,8BAA8B;IAC7C;;;;OAIG;IACH,OAAO,EAAE,cAAc,GAAG,SAAS,CAAC;IACpC;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;CACpD;AAaD,qBAAa,4BACX,YAAW,YAAY,CAAC,8BAA8B,CAAC;IAEvD,QAAQ,CAAC,IAAI,iCAAiC;IAC9C,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,8BAA8B;CAgDtD"}
1
+ {"version":3,"file":"library-profile-source-gate-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/library-profile-source-gate-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC9E,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAe,MAAM,sBAAsB,CAAC;AAcpF,MAAM,WAAW,8BAA8B;IAC7C;;;;OAIG;IACH,OAAO,EAAE,cAAc,GAAG,SAAS,CAAC;IACpC;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;CACpD;AAaD,qBAAa,4BACX,YAAW,YAAY,CAAC,8BAA8B,CAAC;IAEvD,QAAQ,CAAC,IAAI,iCAAiC;IAC9C,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,8BAA8B;CAuEtD"}
@@ -88,7 +88,19 @@ export class LibraryProfileSourceGatePass {
88
88
  droppedByType: {},
89
89
  };
90
90
  }
91
- const sources = graph.ir.taint.sources;
91
+ // cognium-dev#288 — read the AUTHORITATIVE source list, mirroring the
92
+ // fetch pattern `LibraryProfileSinkGatePass` already uses and proves.
93
+ //
94
+ // `graph.ir.taint.sources` is NOT that list: `analyzer.ts` constructs the
95
+ // graph with `taint: { sources: [], sinks: [], sanitizers: [] }` and
96
+ // nothing ever populates it, so this pass used to filter a permanently
97
+ // empty array and report `dropped: 0` on every run. The list that both
98
+ // `InterproceduralPass` and the final `taint` rebuild consume is
99
+ // `SinkFilterResult.sources`. The fallback keeps the stand-alone unit
100
+ // tests working, which build an IR by hand and never run SinkFilterPass.
101
+ const sources = ctx.hasResult('sink-filter')
102
+ ? ctx.getResult('sink-filter').sources
103
+ : graph.ir.taint.sources;
92
104
  if (sources.length === 0) {
93
105
  return {
94
106
  profile,
@@ -97,21 +109,34 @@ export class LibraryProfileSourceGatePass {
97
109
  droppedByType: {},
98
110
  };
99
111
  }
112
+ // cognium-dev#288 (option A) — COUNT, do not delete.
113
+ //
114
+ // This pass used to splice the speculative sources out of the authoritative
115
+ // list. Deleting them destroys information the downstream adjudication
116
+ // layer needs and is invisible in the output: a consumer cannot tell a
117
+ // clean file from a gated one, and `@cognium/project-profile-detect` can
118
+ // misclassify a repo as a library. It is also inconsistent with how this
119
+ // codebase already answers the same "caller's responsibility" question —
120
+ // `applyLibraryApiSurfaceDowngrade` and `applyProjectProfileTransform`
121
+ // (ADR-008) TAG a finding and downgrade its severity while keeping it,
122
+ // recording `original_severity` so the decision is auditable and
123
+ // reversible.
124
+ //
125
+ // So the sources survive and the signal is carried on the FLOWS instead,
126
+ // via the `library-api-surface:caller-responsibility` tag that
127
+ // `TaintFlowInfo.tags` has existed for since 3.105.0. The count is still
128
+ // reported for diagnostics.
100
129
  const droppedByType = {};
101
- const kept = [];
102
130
  for (const src of sources) {
103
131
  if (SPECULATIVE_SOURCE_TYPES.has(src.type)) {
104
132
  droppedByType[src.type] = (droppedByType[src.type] ?? 0) + 1;
105
- continue;
106
133
  }
107
- kept.push(src);
108
134
  }
109
- const dropped = sources.length - kept.length;
110
- // Mutate the array in place so downstream passes see the
111
- // filtered list. Every existing pass reads `graph.ir.taint.sources`
112
- // by reference.
113
- sources.length = 0;
114
- sources.push(...kept);
135
+ const dropped = 0;
136
+ // No mutation: `sources` is handed to `TaintPropagationPass` and
137
+ // `InterproceduralPass` and assigned to `taint.sources` by the rebuild.
138
+ // Leaving it intact is the point of option A — the sources/flows stay and
139
+ // the library-shape judgement is expressed as a tag, not a deletion.
115
140
  return {
116
141
  profile,
117
142
  applied: true,
@@ -1 +1 @@
1
- {"version":3,"file":"library-profile-source-gate-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/library-profile-source-gate-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAKH;;;;;GAKG;AACH,MAAM,wBAAwB,GAA4B,IAAI,GAAG,CAAa;IAC5E,uBAAuB;IACvB,mBAAmB;CACpB,CAAC,CAAC;AA2BH;;;;;GAKG;AACH,SAAS,cAAc,CAAC,OAAmC;IACzD,IAAI,CAAC,OAAO,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACpD,OAAO,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,OAAO,4BAA4B;IAG9B,IAAI,GAAG,6BAA6B,CAAC;IACrC,QAAQ,GAAG,UAAmB,CAAC;IAExC,GAAG,CAAC,GAAgB;QAClB,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC;QACtB,MAAM,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC;QAE7C,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC;YAC7B,OAAO;gBACL,OAAO;gBACP,OAAO,EAAE,KAAK;gBACd,OAAO,EAAE,CAAC;gBACV,aAAa,EAAE,EAAE;aAClB,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC;QACvC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO;gBACL,OAAO;gBACP,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,CAAC;gBACV,aAAa,EAAE,EAAE;aAClB,CAAC;QACJ,CAAC;QAED,MAAM,aAAa,GAAwC,EAAE,CAAC;QAC9D,MAAM,IAAI,GAAG,EAAE,CAAC;QAChB,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;YAC1B,IAAI,wBAAwB,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC3C,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;gBAC7D,SAAS;YACX,CAAC;YACD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjB,CAAC;QAED,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAE7C,yDAAyD;QACzD,oEAAoE;QACpE,gBAAgB;QAChB,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;QACnB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QAEtB,OAAO;YACL,OAAO;YACP,OAAO,EAAE,IAAI;YACb,OAAO;YACP,aAAa;SACd,CAAC;IACJ,CAAC;CACF"}
1
+ {"version":3,"file":"library-profile-source-gate-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/library-profile-source-gate-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAMH;;;;;GAKG;AACH,MAAM,wBAAwB,GAA4B,IAAI,GAAG,CAAa;IAC5E,uBAAuB;IACvB,mBAAmB;CACpB,CAAC,CAAC;AA2BH;;;;;GAKG;AACH,SAAS,cAAc,CAAC,OAAmC;IACzD,IAAI,CAAC,OAAO,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACpD,OAAO,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,OAAO,4BAA4B;IAG9B,IAAI,GAAG,6BAA6B,CAAC;IACrC,QAAQ,GAAG,UAAmB,CAAC;IAExC,GAAG,CAAC,GAAgB;QAClB,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC;QACtB,MAAM,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC;QAE7C,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC;YAC7B,OAAO;gBACL,OAAO;gBACP,OAAO,EAAE,KAAK;gBACd,OAAO,EAAE,CAAC;gBACV,aAAa,EAAE,EAAE;aAClB,CAAC;QACJ,CAAC;QAED,sEAAsE;QACtE,sEAAsE;QACtE,EAAE;QACF,0EAA0E;QAC1E,qEAAqE;QACrE,uEAAuE;QACvE,uEAAuE;QACvE,iEAAiE;QACjE,sEAAsE;QACtE,yEAAyE;QACzE,MAAM,OAAO,GAAkB,GAAG,CAAC,SAAS,CAAC,aAAa,CAAC;YACzD,CAAC,CAAC,GAAG,CAAC,SAAS,CAAmB,aAAa,CAAC,CAAC,OAAO;YACxD,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC;QAC3B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO;gBACL,OAAO;gBACP,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,CAAC;gBACV,aAAa,EAAE,EAAE;aAClB,CAAC;QACJ,CAAC;QAED,qDAAqD;QACrD,EAAE;QACF,4EAA4E;QAC5E,uEAAuE;QACvE,uEAAuE;QACvE,yEAAyE;QACzE,yEAAyE;QACzE,yEAAyE;QACzE,uEAAuE;QACvE,uEAAuE;QACvE,iEAAiE;QACjE,cAAc;QACd,EAAE;QACF,yEAAyE;QACzE,+DAA+D;QAC/D,yEAAyE;QACzE,4BAA4B;QAC5B,MAAM,aAAa,GAAwC,EAAE,CAAC;QAC9D,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;YAC1B,IAAI,wBAAwB,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC3C,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YAC/D,CAAC;QACH,CAAC;QACD,MAAM,OAAO,GAAG,CAAC,CAAC;QAElB,iEAAiE;QACjE,wEAAwE;QACxE,0EAA0E;QAC1E,qEAAqE;QACrE,OAAO;YACL,OAAO;YACP,OAAO,EAAE,IAAI;YACb,OAAO;YACP,aAAa;SACd,CAAC;IACJ,CAAC;CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"taint-propagation-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/taint-propagation-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAU9E,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,aAAa,EAAE,CAAC;CACxB;AAED,qBAAa,oBAAqB,YAAW,YAAY,CAAC,0BAA0B,CAAC;IACnF,QAAQ,CAAC,IAAI,uBAAuB;IACpC,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,0BAA0B;CAqblD;AA6sBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA6FnE"}
1
+ {"version":3,"file":"taint-propagation-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/taint-propagation-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAU9E,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,aAAa,EAAE,CAAC;CACxB;AAED,qBAAa,oBAAqB,YAAW,YAAY,CAAC,0BAA0B,CAAC;IACnF,QAAQ,CAAC,IAAI,uBAAuB;IACpC,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,0BAA0B;CA4hBlD;AA6sBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA6FnE"}
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { propagateTaint } from '../taint-propagation.js';
14
14
  import { isFalsePositive, isCorrelatedPredicateFP } from '../constant-propagation.js';
15
- import { buildJavaTaintedVars, buildPythonTaintedVars, buildRustTaintedVars } from './language-sources-pass.js';
15
+ import { buildJavaTaintedVars, buildPythonTaintedVars, buildRustTaintedVars, findPythonTrustBoundaryViolations, findPythonReturnXSSSinks } from './language-sources-pass.js';
16
16
  import { canSourceReachSink, sourceSemanticsAllowed } from '../findings.js';
17
17
  import { walkBackwardDefs } from '../dfg-walk.js';
18
18
  import { sanitizerCoversSink } from '../sanitizer-index.js';
@@ -86,6 +86,113 @@ export class TaintPropagationPass {
86
86
  flows.push(f);
87
87
  return true;
88
88
  };
89
+ // Supplement: Python trust-boundary (CWE-501) flows — cognium-dev#363.
90
+ //
91
+ // `LanguageSourcesPass` detects `session[<tainted>] = …` and synthesises a
92
+ // `trust_boundary` sink for it, but that sink has no `method` and no
93
+ // arguments because a subscript assignment is not a call. Every flow
94
+ // builder here matches a source against a sink's ARGUMENTS, so nothing
95
+ // could ever connect to it: the sink was reported while `taint.flows`
96
+ // stayed empty, which is why OWASP BenchmarkPython scores the whole
97
+ // `trustbound` category 0% — its runner (correctly, post-#265) requires an
98
+ // unsanitised flow of the expected type rather than source+sink
99
+ // co-occurrence.
100
+ //
101
+ // The detector already computes both ends of the flow, so emit it here
102
+ // rather than teaching the argument matchers about subscript assignment.
103
+ // Reuses the exported detector so the sink and the flow can never disagree
104
+ // about what qualifies.
105
+ if (ctx.language === 'python' && typeof ctx.code === 'string') {
106
+ const pyTainted = buildPythonTaintedVars(ctx.code);
107
+ if (pyTainted.size > 0) {
108
+ for (const v of findPythonTrustBoundaryViolations(ctx.code, pyTainted)) {
109
+ const sink = sinks.find(sk => sk.line === v.sinkLine && sk.type === 'trust_boundary');
110
+ if (!sink)
111
+ continue;
112
+ // Prefer a real registered source on the detector's line; fall back
113
+ // to any source at or before it, so a derived tainted var (whose
114
+ // origin line is the read, not the assignment) still resolves.
115
+ const src = sources.find(sc => sc.line === v.sourceLine) ??
116
+ sources
117
+ .filter(sc => sc.line <= v.sinkLine)
118
+ .sort((a, b) => b.line - a.line)[0];
119
+ if (!src)
120
+ continue;
121
+ if (constProp.unreachableLines.has(v.sinkLine))
122
+ continue;
123
+ pushIfNew({
124
+ source_line: src.line,
125
+ sink_line: v.sinkLine,
126
+ source_type: src.type,
127
+ sink_type: 'trust_boundary',
128
+ path: [
129
+ { variable: src.variable ?? 'session_key', line: src.line, type: 'source' },
130
+ { variable: src.variable ?? 'session_key', line: v.sinkLine, type: 'sink' },
131
+ ],
132
+ confidence: (src.confidence ?? 1) * (sink.confidence ?? 1),
133
+ sanitized: false,
134
+ });
135
+ }
136
+ }
137
+ }
138
+ // Supplement: Python return-XSS (CWE-79) flows — cognium-dev#368.
139
+ //
140
+ // Same wiring gap as #363, on a different detector. `LanguageSourcesPass`
141
+ // synthesises an `xss` sink for `return f"<div>{taint}</div>"` and
142
+ // `return "<div>" + taint`, but that sink has no `method` and no
143
+ // arguments, so no argument matcher can connect a source to it — the sink
144
+ // was reported while `taint.flows` stayed empty. Returning interpolated
145
+ // request data from a Flask view is real reflected XSS (a `str` return
146
+ // gets `text/html`), so the flow belongs here.
147
+ //
148
+ // Sanitizer-aware by construction: the registered Python escapers
149
+ // (`html.escape`, `markupsafe.escape`, `flask.escape`, …) are detected ON
150
+ // the return line, so `return f"<div>{html.escape(p)}</div>"` carries an
151
+ // xss-covering sanitizer at the sink line and is skipped. Without that
152
+ // check this would trade a false-negative class for a worse
153
+ // false-positive one.
154
+ if (ctx.language === 'python' && typeof ctx.code === 'string') {
155
+ const pyTainted = buildPythonTaintedVars(ctx.code);
156
+ if (pyTainted.size > 0) {
157
+ const xssSanitizerLines = sanitizers
158
+ .filter(sa => sa.sanitizes.includes('xss'))
159
+ .map(sa => sa.line);
160
+ for (const rx of findPythonReturnXSSSinks(ctx.code, pyTainted)) {
161
+ if (constProp.unreachableLines.has(rx.sinkLine))
162
+ continue;
163
+ const sink = sinks.find(sk => sk.line === rx.sinkLine && sk.type === 'xss');
164
+ if (!sink)
165
+ continue;
166
+ const src = sources
167
+ .filter(sc => sc.line <= rx.sinkLine)
168
+ .sort((a, b) => b.line - a.line)[0];
169
+ if (!src)
170
+ continue;
171
+ // An xss-covering sanitizer anywhere on the source -> sink range
172
+ // suppresses the flow. Checking only the SINK line is not enough:
173
+ // `n = int(request.args.get('n','0'))` sanitises on the source line
174
+ // and renders on the next (#100.2), and that case passed before only
175
+ // because no flow existed at all. Deliberately coarse — for an
176
+ // additive emitter the safe direction is to emit fewer flows, so an
177
+ // unrelated in-range sanitizer costs a missed finding rather than a
178
+ // false positive.
179
+ if (xssSanitizerLines.some(l => l >= src.line && l <= rx.sinkLine))
180
+ continue;
181
+ pushIfNew({
182
+ source_line: src.line,
183
+ sink_line: rx.sinkLine,
184
+ source_type: src.type,
185
+ sink_type: 'xss',
186
+ path: [
187
+ { variable: src.variable ?? 'response', line: src.line, type: 'source' },
188
+ { variable: src.variable ?? 'response', line: rx.sinkLine, type: 'sink' },
189
+ ],
190
+ confidence: (src.confidence ?? 1) * (sink.confidence ?? 1),
191
+ sanitized: false,
192
+ });
193
+ }
194
+ }
195
+ }
89
196
  // Supplement: array element flows
90
197
  // Sprint 82 (#189) — dedup keys on (source_line, sink_line, sink_type) so
91
198
  // distinct sink-types at the same call site (e.g. `res.redirect` registered