circle-ir 3.153.0 → 3.155.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.
Files changed (43) hide show
  1. package/configs/sinks/code_injection.yaml +2 -1
  2. package/configs/sinks/javascript_dom_xss.yaml +4 -2
  3. package/configs/sinks/xpath.yaml +40 -0
  4. package/dist/analysis/config-loader.d.ts.map +1 -1
  5. package/dist/analysis/config-loader.js +13 -3
  6. package/dist/analysis/config-loader.js.map +1 -1
  7. package/dist/analysis/dfg-walk.d.ts +36 -0
  8. package/dist/analysis/dfg-walk.d.ts.map +1 -0
  9. package/dist/analysis/dfg-walk.js +59 -0
  10. package/dist/analysis/dfg-walk.js.map +1 -0
  11. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  12. package/dist/analysis/passes/language-sources-pass.js +22 -0
  13. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  14. package/dist/analysis/passes/library-profile-sink-gate-pass.d.ts +43 -1
  15. package/dist/analysis/passes/library-profile-sink-gate-pass.d.ts.map +1 -1
  16. package/dist/analysis/passes/library-profile-sink-gate-pass.js +75 -0
  17. package/dist/analysis/passes/library-profile-sink-gate-pass.js.map +1 -1
  18. package/dist/analysis/passes/library-profile-xss-gate-pass.d.ts +104 -0
  19. package/dist/analysis/passes/library-profile-xss-gate-pass.d.ts.map +1 -0
  20. package/dist/analysis/passes/library-profile-xss-gate-pass.js +196 -0
  21. package/dist/analysis/passes/library-profile-xss-gate-pass.js.map +1 -0
  22. package/dist/analysis/passes/weak-crypto-pass.d.ts.map +1 -1
  23. package/dist/analysis/passes/weak-crypto-pass.js +6 -0
  24. package/dist/analysis/passes/weak-crypto-pass.js.map +1 -1
  25. package/dist/analysis/path-classification.d.ts +24 -0
  26. package/dist/analysis/path-classification.d.ts.map +1 -0
  27. package/dist/analysis/path-classification.js +69 -0
  28. package/dist/analysis/path-classification.js.map +1 -0
  29. package/dist/analysis/taint-propagation.d.ts +17 -1
  30. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  31. package/dist/analysis/taint-propagation.js +47 -16
  32. package/dist/analysis/taint-propagation.js.map +1 -1
  33. package/dist/analyzer.d.ts.map +1 -1
  34. package/dist/analyzer.js +25 -1
  35. package/dist/analyzer.js.map +1 -1
  36. package/dist/browser/circle-ir.js +299 -15
  37. package/dist/core/circle-ir-core.cjs +103 -15
  38. package/dist/core/circle-ir-core.js +103 -15
  39. package/dist/graph/code-graph.d.ts +7 -0
  40. package/dist/graph/code-graph.d.ts.map +1 -1
  41. package/dist/graph/code-graph.js +17 -0
  42. package/dist/graph/code-graph.js.map +1 -1
  43. package/package.json +1 -1
@@ -11304,9 +11304,16 @@ var DEFAULT_SINKS = [
11304
11304
  { method: "newBufferedWriter", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11305
11305
  { method: "copy", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0, 1] },
11306
11306
  { method: "move", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0, 1] },
11307
- { method: "exists", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
11308
- { method: "isDirectory", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
11309
- { method: "isRegularFile", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
11307
+ // NOTE: `Files.exists`, `Files.isDirectory`, `Files.isRegularFile`
11308
+ // were removed in 3.154.0 (#245 RC2). These NIO methods are pure
11309
+ // boolean queries they read a filesystem attribute and cannot
11310
+ // cause traversal escape. A CWE-22 sink must consume the path to
11311
+ // open, read, write, delete, list, or link a filesystem entry;
11312
+ // check-only receivers reveal at most a boolean. Empirically
11313
+ // ~12 H+C FPs across the 10-repo Tier 2 cohort
11314
+ // (cognium-ai#189 §4). `java.io.File` instance methods
11315
+ // (`file.isDirectory()`, `file.exists()`, `file.canRead()`, …)
11316
+ // are already not registered as CWE-22 sinks.
11310
11317
  // RandomAccessFile
11311
11318
  { method: "RandomAccessFile", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11312
11319
  // Framework-specific resource loading (Cocoon, Spring, etc.)
@@ -12895,6 +12902,9 @@ var DEFAULT_SANITIZERS = [
12895
12902
  { method: "parseDouble", class: "Double", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12896
12903
  { method: "parseShort", class: "Short", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12897
12904
  { method: "parseByte", class: "Byte", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12905
+ // cognium-dev #238 A.3 — parseBoolean and expanded coercion coverage for XSS/CRLF/log/xpath/ldap/xxe.
12906
+ // A parsed boolean/numeric cannot carry a string-injection payload for any string-based sink.
12907
+ { method: "parseBoolean", class: "Boolean", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection", "xss", "crlf", "log_injection", "xpath_injection", "ldap_injection", "xxe"] },
12898
12908
  // Java UUID parse — UUID.fromString rejects non-UUID strings
12899
12909
  { method: "fromString", class: "UUID", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12900
12910
  // JavaScript numeric coercion (Number/parseInt/parseFloat already covered above; add path_traversal/code_injection)
@@ -15026,6 +15036,23 @@ var CodeGraph = class {
15026
15036
  }
15027
15037
  return this._chainsByFromDef;
15028
15038
  }
15039
+ /**
15040
+ * Mirror of chainsByFromDef keyed by chain.to_def. Enables backward
15041
+ * walks from a use's reaching def to earlier defs in the chain
15042
+ * (cognium-dev #238 — DFG-walk sanitizer credit).
15043
+ */
15044
+ _chainsByToDef = null;
15045
+ get chainsByToDef() {
15046
+ if (!this._chainsByToDef) {
15047
+ this._chainsByToDef = /* @__PURE__ */ new Map();
15048
+ for (const chain of this.ir.dfg.chains ?? []) {
15049
+ const arr = this._chainsByToDef.get(chain.to_def) ?? [];
15050
+ arr.push(chain);
15051
+ this._chainsByToDef.set(chain.to_def, arr);
15052
+ }
15053
+ }
15054
+ return this._chainsByToDef;
15055
+ }
15029
15056
  // ---------------------------------------------------------------------------
15030
15057
  // Call indexes
15031
15058
  // ---------------------------------------------------------------------------
@@ -15950,6 +15977,43 @@ var AnalysisPipeline = class {
15950
15977
  }
15951
15978
  };
15952
15979
 
15980
+ // src/analysis/dfg-walk.ts
15981
+ function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
15982
+ const maxHops = options.maxHops ?? 32;
15983
+ const visited = /* @__PURE__ */ new Set();
15984
+ const lines = /* @__PURE__ */ new Set();
15985
+ let hopCapReached = false;
15986
+ const startDef = defById.get(startDefId);
15987
+ if (!startDef) {
15988
+ return { visited, lines, hopCapReached: false };
15989
+ }
15990
+ visited.add(startDefId);
15991
+ lines.add(startDef.line);
15992
+ const queue = [startDefId];
15993
+ let hops = 0;
15994
+ while (queue.length > 0) {
15995
+ if (hops >= maxHops) {
15996
+ hopCapReached = true;
15997
+ break;
15998
+ }
15999
+ hops++;
16000
+ const currentId = queue.shift();
16001
+ const incoming = chainsByToDef.get(currentId);
16002
+ if (!incoming) continue;
16003
+ for (const chain of incoming) {
16004
+ const fromId = chain.from_def;
16005
+ if (visited.has(fromId)) continue;
16006
+ visited.add(fromId);
16007
+ const fromDef = defById.get(fromId);
16008
+ if (fromDef) {
16009
+ lines.add(fromDef.line);
16010
+ }
16011
+ queue.push(fromId);
16012
+ }
16013
+ }
16014
+ return { visited, lines, hopCapReached };
16015
+ }
16016
+
15953
16017
  // src/analysis/taint-propagation.ts
15954
16018
  function buildSanitizersByLine(sanitizers) {
15955
16019
  const out2 = /* @__PURE__ */ new Map();
@@ -16037,7 +16101,12 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
16037
16101
  taintInfo.line,
16038
16102
  sink.line,
16039
16103
  sink.type,
16040
- sanitizersByLine
16104
+ sanitizersByLine,
16105
+ {
16106
+ startDefId: use.def_id,
16107
+ chainsByToDef: graph.chainsByToDef,
16108
+ defById
16109
+ }
16041
16110
  );
16042
16111
  if (!isSanitized.sanitized) {
16043
16112
  const source = sources.find((s) => s.line === taintInfo.sourceLine);
@@ -16175,20 +16244,39 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
16175
16244
  "crlf",
16176
16245
  "mass_assignment"
16177
16246
  ]);
16178
- function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine) {
16179
- const sanitizersAtTarget = sanitizersByLine.get(toLine);
16180
- if (!sanitizersAtTarget || sanitizersAtTarget.length === 0) {
16181
- return { sanitized: false };
16182
- }
16247
+ function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine, ctx) {
16183
16248
  const isKnownSinkType = KNOWN_SINK_TYPES.has(sinkType);
16184
- for (const san of sanitizersAtTarget) {
16185
- if (isKnownSinkType) {
16186
- if (san.sanitizes.includes(sinkType)) {
16249
+ const sanitizersAtTarget = sanitizersByLine.get(toLine);
16250
+ if (sanitizersAtTarget && sanitizersAtTarget.length > 0) {
16251
+ for (const san of sanitizersAtTarget) {
16252
+ if (isKnownSinkType) {
16253
+ if (san.sanitizes.includes(sinkType)) {
16254
+ return { sanitized: true, sanitizer: san };
16255
+ }
16256
+ } else if (san.sanitizes.length > 0) {
16187
16257
  return { sanitized: true, sanitizer: san };
16188
16258
  }
16189
- } else {
16190
- if (san.sanitizes.length > 0) {
16191
- return { sanitized: true, sanitizer: san };
16259
+ }
16260
+ }
16261
+ if (ctx) {
16262
+ const walk = walkBackwardDefs(
16263
+ ctx.startDefId,
16264
+ ctx.chainsByToDef,
16265
+ ctx.defById,
16266
+ { maxHops: ctx.maxHops ?? 32 }
16267
+ );
16268
+ for (const line of walk.lines) {
16269
+ if (line === toLine) continue;
16270
+ const sansAtLine = sanitizersByLine.get(line);
16271
+ if (!sansAtLine || sansAtLine.length === 0) continue;
16272
+ for (const san of sansAtLine) {
16273
+ if (isKnownSinkType) {
16274
+ if (san.sanitizes.includes(sinkType)) {
16275
+ return { sanitized: true, sanitizer: san };
16276
+ }
16277
+ } else if (san.sanitizes.length > 0) {
16278
+ return { sanitized: true, sanitizer: san };
16279
+ }
16192
16280
  }
16193
16281
  }
16194
16282
  }
@@ -24988,6 +25076,20 @@ function findJavaScriptDOMSinks(sourceCode, language) {
24988
25076
  else if (line.includes(".href")) method = "href";
24989
25077
  else if (line.includes(".cssText")) method = "cssText";
24990
25078
  else if (line.includes("style.textContent")) method = "textContent";
25079
+ if (method === "document.write" || method === "document.writeln") {
25080
+ const argMatch = line.match(/document\.write(?:ln)?\s*\(\s*([^)]*)\)/);
25081
+ if (argMatch) {
25082
+ const arg = argMatch[1].trim();
25083
+ if (!arg.includes("+")) {
25084
+ const doubleQ = /^"(?:[^"\\]|\\.)*"$/;
25085
+ const singleQ = /^'(?:[^'\\]|\\.)*'$/;
25086
+ const backtick = /^`(?:[^`\\$]|\\.|\$(?!\{))*`$/;
25087
+ if (doubleQ.test(arg) || singleQ.test(arg) || backtick.test(arg)) {
25088
+ break;
25089
+ }
25090
+ }
25091
+ }
25092
+ }
24991
25093
  const alreadyExists = sinks.some((s) => s.line === lineNumber && s.cwe === cwe);
24992
25094
  if (!alreadyExists) {
24993
25095
  sinks.push({ type, cwe, severity, line: lineNumber, location: line.trim().substring(0, 80), method });
@@ -31298,6 +31400,153 @@ var LibraryProfileSinkGatePass = class {
31298
31400
  };
31299
31401
  }
31300
31402
  };
31403
+ var CWE22_SPECULATIVE_SOURCE_TYPES = /* @__PURE__ */ new Set([
31404
+ "interprocedural_param",
31405
+ "constructor_field"
31406
+ ]);
31407
+ var LibraryProfileCwe22PathGatePass = class {
31408
+ name = "library-profile-cwe22-path-gate";
31409
+ category = "security";
31410
+ run(ctx) {
31411
+ const { graph } = ctx;
31412
+ const profile = graph.ir.meta.projectProfile;
31413
+ if (!isLibraryShape2(profile)) {
31414
+ return {
31415
+ profile,
31416
+ applied: false,
31417
+ dropped: 0,
31418
+ droppedBySourceType: {}
31419
+ };
31420
+ }
31421
+ const flows = graph.ir.taint.flows;
31422
+ if (!flows || flows.length === 0) {
31423
+ return {
31424
+ profile,
31425
+ applied: true,
31426
+ dropped: 0,
31427
+ droppedBySourceType: {}
31428
+ };
31429
+ }
31430
+ const droppedBySourceType = {};
31431
+ const kept = [];
31432
+ for (const flow of flows) {
31433
+ if (flow.sink_type === "path_traversal" && CWE22_SPECULATIVE_SOURCE_TYPES.has(flow.source_type)) {
31434
+ droppedBySourceType[flow.source_type] = (droppedBySourceType[flow.source_type] ?? 0) + 1;
31435
+ continue;
31436
+ }
31437
+ kept.push(flow);
31438
+ }
31439
+ const dropped = flows.length - kept.length;
31440
+ if (dropped > 0) {
31441
+ flows.length = 0;
31442
+ flows.push(...kept);
31443
+ }
31444
+ return {
31445
+ profile,
31446
+ applied: true,
31447
+ dropped,
31448
+ droppedBySourceType
31449
+ };
31450
+ }
31451
+ };
31452
+
31453
+ // src/analysis/passes/library-profile-xss-gate-pass.ts
31454
+ var XSS_NON_HTML_OUTPUT_CLASSES = /* @__PURE__ */ new Set([
31455
+ // In-memory buffers — no HTML output surface.
31456
+ "StringBuilder",
31457
+ "StringBuffer",
31458
+ "CharArrayWriter",
31459
+ "ByteArrayOutputStream",
31460
+ // CLI stdio — CLI apps, not web response bodies.
31461
+ "PrintStream",
31462
+ "System",
31463
+ // HTTP client builders — these are taint SOURCES (outbound reads),
31464
+ // not XSS sinks. `response.body()`, `.post()`, `.get()` on
31465
+ // hutool `HttpRequest` / `HttpResponse` reach us as sink
31466
+ // matches only because xss.yaml catches String-valued receivers.
31467
+ "HttpRequest",
31468
+ "HttpRequestBuilder",
31469
+ "HttpResponse",
31470
+ // Servlet non-body IO. `HttpSession.setAttribute`, `HttpSession.putValue`,
31471
+ // `HttpServletRequest.setAttribute` are session/request attribute
31472
+ // IO, not HTML output. Under `library/*` there is no JSP renderer
31473
+ // reflecting them back. `HttpServletResponse` itself is NOT on
31474
+ // this list — its writers are genuine XSS sinks.
31475
+ "HttpSession",
31476
+ "ServletRequest",
31477
+ "HttpServletRequest",
31478
+ // Wire-protocol writers (jedis internal).
31479
+ "RedisOutputStream",
31480
+ "SafeEncoder",
31481
+ "RESP2",
31482
+ "Protocol",
31483
+ // JSON parsers — these read JSON into POJOs. Source, not sink.
31484
+ "JSONUtil",
31485
+ "JSON",
31486
+ "ObjectMapper",
31487
+ "JsonReader",
31488
+ // Loggers — log injection is CWE-117 (already covered by #112);
31489
+ // these appear here because xss.yaml has a String-valued catch-all
31490
+ // that hits Logger receivers.
31491
+ "Logger",
31492
+ "LoggerFactory",
31493
+ "Log",
31494
+ "Slf4jLogger",
31495
+ // Router / interceptor context stores. Zuul `RequestContext`,
31496
+ // Sentinel `Context` are internal request-processing state, not
31497
+ // HTML output.
31498
+ "RequestContext",
31499
+ "Context"
31500
+ ]);
31501
+ function isLibraryShape3(profile) {
31502
+ if (!profile || profile === "unknown") return false;
31503
+ return profile.startsWith("library/");
31504
+ }
31505
+ var LibraryProfileXssGatePass = class {
31506
+ name = "library-profile-xss-gate";
31507
+ category = "security";
31508
+ run(ctx) {
31509
+ const { graph } = ctx;
31510
+ const profile = graph.ir.meta.projectProfile;
31511
+ if (!isLibraryShape3(profile)) {
31512
+ return {
31513
+ profile,
31514
+ applied: false,
31515
+ dropped: 0,
31516
+ droppedByClass: {}
31517
+ };
31518
+ }
31519
+ const sinks = ctx.hasResult("sink-filter") ? ctx.getResult("sink-filter").sinks : graph.ir.taint.sinks;
31520
+ if (sinks.length === 0) {
31521
+ return {
31522
+ profile,
31523
+ applied: true,
31524
+ dropped: 0,
31525
+ droppedByClass: {}
31526
+ };
31527
+ }
31528
+ const droppedByClass = {};
31529
+ const kept = [];
31530
+ for (const sink of sinks) {
31531
+ if (sink.type === "xss" && sink.class && XSS_NON_HTML_OUTPUT_CLASSES.has(sink.class)) {
31532
+ droppedByClass[sink.class] = (droppedByClass[sink.class] ?? 0) + 1;
31533
+ continue;
31534
+ }
31535
+ kept.push(sink);
31536
+ }
31537
+ const dropped = sinks.length - kept.length;
31538
+ if (dropped > 0) {
31539
+ sinks.length = 0;
31540
+ sinks.push(...kept);
31541
+ }
31542
+ return {
31543
+ profile,
31544
+ applied: true,
31545
+ dropped,
31546
+ droppedByClass
31547
+ };
31548
+ }
31549
+ };
31301
31550
 
31302
31551
  // src/analysis/passes/taint-propagation-pass.ts
31303
31552
  var TaintPropagationPass = class {
@@ -37107,6 +37356,34 @@ var WeakHashPass = class {
37107
37356
  }
37108
37357
  };
37109
37358
 
37359
+ // src/analysis/path-classification.ts
37360
+ var TEST_PATH_PATTERNS = [
37361
+ // Directory-based conventions
37362
+ /(^|\/)test\//i,
37363
+ /(^|\/)tests\//i,
37364
+ /(^|\/)testing\//i,
37365
+ /(^|\/)__tests__\//i,
37366
+ /(^|\/)spec\//i,
37367
+ // Go test file suffix
37368
+ /_test\.go$/i,
37369
+ // Python pytest naming
37370
+ /(^|\/)test_[^/]+\.py$/i,
37371
+ /_test\.py$/i,
37372
+ // JS/TS unit / spec naming
37373
+ /\.test\.(?:tsx?|jsx?|mjs|cjs)$/i,
37374
+ /\.spec\.(?:tsx?|jsx?|mjs|cjs)$/i,
37375
+ // JVM alt naming
37376
+ /\.test\.(?:java|kt)$/i
37377
+ ];
37378
+ function isTestPath(filepath) {
37379
+ if (!filepath) return false;
37380
+ const normalized = filepath.replace(/\\/g, "/");
37381
+ for (const pattern of TEST_PATH_PATTERNS) {
37382
+ if (pattern.test(normalized)) return true;
37383
+ }
37384
+ return false;
37385
+ }
37386
+
37110
37387
  // src/analysis/passes/weak-crypto-pass.ts
37111
37388
  var WEAK_CIPHER_BASES = /* @__PURE__ */ new Set([
37112
37389
  "des",
@@ -37295,6 +37572,9 @@ var WeakCryptoPass = class {
37295
37572
  if (isProtocolMandatedCryptoFile(file, code)) {
37296
37573
  return { findings: [] };
37297
37574
  }
37575
+ if (isTestPath(file)) {
37576
+ return { findings: [] };
37577
+ }
37298
37578
  const findings = [];
37299
37579
  const constProp = ctx.hasResult("constant-propagation") ? ctx.getResult("constant-propagation") : null;
37300
37580
  const literalBindings = scanLiteralBindings(code, language);
@@ -40765,10 +41045,14 @@ async function analyze(code, filePath, language, options = {}) {
40765
41045
  pipeline.add(new CliMainReflectionSuppressPass());
40766
41046
  if (!disabledPasses.has("library-profile-sink-gate"))
40767
41047
  pipeline.add(new LibraryProfileSinkGatePass());
41048
+ if (!disabledPasses.has("library-profile-xss-gate"))
41049
+ pipeline.add(new LibraryProfileXssGatePass());
40768
41050
  pipeline.add(new TaintPropagationPass());
40769
41051
  pipeline.add(new InterproceduralPass({
40770
41052
  enableEntryPointGate: options.enableEntryPointGate ?? true
40771
41053
  }));
41054
+ if (!disabledPasses.has("library-profile-cwe22-path-gate"))
41055
+ pipeline.add(new LibraryProfileCwe22PathGatePass());
40772
41056
  if (!disabledPasses.has("scan-secrets")) pipeline.add(new ScanSecretsPass());
40773
41057
  if (!disabledPasses.has("dead-code")) pipeline.add(new DeadCodePass());
40774
41058
  if (!disabledPasses.has("missing-await")) pipeline.add(new MissingAwaitPass());
@@ -10699,9 +10699,16 @@ var DEFAULT_SINKS = [
10699
10699
  { method: "newBufferedWriter", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
10700
10700
  { method: "copy", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0, 1] },
10701
10701
  { method: "move", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0, 1] },
10702
- { method: "exists", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
10703
- { method: "isDirectory", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
10704
- { method: "isRegularFile", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
10702
+ // NOTE: `Files.exists`, `Files.isDirectory`, `Files.isRegularFile`
10703
+ // were removed in 3.154.0 (#245 RC2). These NIO methods are pure
10704
+ // boolean queries they read a filesystem attribute and cannot
10705
+ // cause traversal escape. A CWE-22 sink must consume the path to
10706
+ // open, read, write, delete, list, or link a filesystem entry;
10707
+ // check-only receivers reveal at most a boolean. Empirically
10708
+ // ~12 H+C FPs across the 10-repo Tier 2 cohort
10709
+ // (cognium-ai#189 §4). `java.io.File` instance methods
10710
+ // (`file.isDirectory()`, `file.exists()`, `file.canRead()`, …)
10711
+ // are already not registered as CWE-22 sinks.
10705
10712
  // RandomAccessFile
10706
10713
  { method: "RandomAccessFile", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
10707
10714
  // Framework-specific resource loading (Cocoon, Spring, etc.)
@@ -12290,6 +12297,9 @@ var DEFAULT_SANITIZERS = [
12290
12297
  { method: "parseDouble", class: "Double", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12291
12298
  { method: "parseShort", class: "Short", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12292
12299
  { method: "parseByte", class: "Byte", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12300
+ // cognium-dev #238 A.3 — parseBoolean and expanded coercion coverage for XSS/CRLF/log/xpath/ldap/xxe.
12301
+ // A parsed boolean/numeric cannot carry a string-injection payload for any string-based sink.
12302
+ { method: "parseBoolean", class: "Boolean", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection", "xss", "crlf", "log_injection", "xpath_injection", "ldap_injection", "xxe"] },
12293
12303
  // Java UUID parse — UUID.fromString rejects non-UUID strings
12294
12304
  { method: "fromString", class: "UUID", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12295
12305
  // JavaScript numeric coercion (Number/parseInt/parseFloat already covered above; add path_traversal/code_injection)
@@ -14039,6 +14049,23 @@ var CodeGraph = class {
14039
14049
  }
14040
14050
  return this._chainsByFromDef;
14041
14051
  }
14052
+ /**
14053
+ * Mirror of chainsByFromDef keyed by chain.to_def. Enables backward
14054
+ * walks from a use's reaching def to earlier defs in the chain
14055
+ * (cognium-dev #238 — DFG-walk sanitizer credit).
14056
+ */
14057
+ _chainsByToDef = null;
14058
+ get chainsByToDef() {
14059
+ if (!this._chainsByToDef) {
14060
+ this._chainsByToDef = /* @__PURE__ */ new Map();
14061
+ for (const chain of this.ir.dfg.chains ?? []) {
14062
+ const arr = this._chainsByToDef.get(chain.to_def) ?? [];
14063
+ arr.push(chain);
14064
+ this._chainsByToDef.set(chain.to_def, arr);
14065
+ }
14066
+ }
14067
+ return this._chainsByToDef;
14068
+ }
14042
14069
  // ---------------------------------------------------------------------------
14043
14070
  // Call indexes
14044
14071
  // ---------------------------------------------------------------------------
@@ -14209,6 +14236,43 @@ var CodeGraph = class {
14209
14236
  }
14210
14237
  };
14211
14238
 
14239
+ // src/analysis/dfg-walk.ts
14240
+ function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
14241
+ const maxHops = options.maxHops ?? 32;
14242
+ const visited = /* @__PURE__ */ new Set();
14243
+ const lines = /* @__PURE__ */ new Set();
14244
+ let hopCapReached = false;
14245
+ const startDef = defById.get(startDefId);
14246
+ if (!startDef) {
14247
+ return { visited, lines, hopCapReached: false };
14248
+ }
14249
+ visited.add(startDefId);
14250
+ lines.add(startDef.line);
14251
+ const queue = [startDefId];
14252
+ let hops = 0;
14253
+ while (queue.length > 0) {
14254
+ if (hops >= maxHops) {
14255
+ hopCapReached = true;
14256
+ break;
14257
+ }
14258
+ hops++;
14259
+ const currentId = queue.shift();
14260
+ const incoming = chainsByToDef.get(currentId);
14261
+ if (!incoming) continue;
14262
+ for (const chain of incoming) {
14263
+ const fromId = chain.from_def;
14264
+ if (visited.has(fromId)) continue;
14265
+ visited.add(fromId);
14266
+ const fromDef = defById.get(fromId);
14267
+ if (fromDef) {
14268
+ lines.add(fromDef.line);
14269
+ }
14270
+ queue.push(fromId);
14271
+ }
14272
+ }
14273
+ return { visited, lines, hopCapReached };
14274
+ }
14275
+
14212
14276
  // src/analysis/taint-propagation.ts
14213
14277
  function buildSanitizersByLine(sanitizers) {
14214
14278
  const out2 = /* @__PURE__ */ new Map();
@@ -14296,7 +14360,12 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
14296
14360
  taintInfo.line,
14297
14361
  sink.line,
14298
14362
  sink.type,
14299
- sanitizersByLine
14363
+ sanitizersByLine,
14364
+ {
14365
+ startDefId: use.def_id,
14366
+ chainsByToDef: graph.chainsByToDef,
14367
+ defById
14368
+ }
14300
14369
  );
14301
14370
  if (!isSanitized.sanitized) {
14302
14371
  const source = sources.find((s) => s.line === taintInfo.sourceLine);
@@ -14434,20 +14503,39 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
14434
14503
  "crlf",
14435
14504
  "mass_assignment"
14436
14505
  ]);
14437
- function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine) {
14438
- const sanitizersAtTarget = sanitizersByLine.get(toLine);
14439
- if (!sanitizersAtTarget || sanitizersAtTarget.length === 0) {
14440
- return { sanitized: false };
14441
- }
14506
+ function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine, ctx) {
14442
14507
  const isKnownSinkType = KNOWN_SINK_TYPES.has(sinkType);
14443
- for (const san of sanitizersAtTarget) {
14444
- if (isKnownSinkType) {
14445
- if (san.sanitizes.includes(sinkType)) {
14508
+ const sanitizersAtTarget = sanitizersByLine.get(toLine);
14509
+ if (sanitizersAtTarget && sanitizersAtTarget.length > 0) {
14510
+ for (const san of sanitizersAtTarget) {
14511
+ if (isKnownSinkType) {
14512
+ if (san.sanitizes.includes(sinkType)) {
14513
+ return { sanitized: true, sanitizer: san };
14514
+ }
14515
+ } else if (san.sanitizes.length > 0) {
14446
14516
  return { sanitized: true, sanitizer: san };
14447
14517
  }
14448
- } else {
14449
- if (san.sanitizes.length > 0) {
14450
- return { sanitized: true, sanitizer: san };
14518
+ }
14519
+ }
14520
+ if (ctx) {
14521
+ const walk = walkBackwardDefs(
14522
+ ctx.startDefId,
14523
+ ctx.chainsByToDef,
14524
+ ctx.defById,
14525
+ { maxHops: ctx.maxHops ?? 32 }
14526
+ );
14527
+ for (const line of walk.lines) {
14528
+ if (line === toLine) continue;
14529
+ const sansAtLine = sanitizersByLine.get(line);
14530
+ if (!sansAtLine || sansAtLine.length === 0) continue;
14531
+ for (const san of sansAtLine) {
14532
+ if (isKnownSinkType) {
14533
+ if (san.sanitizes.includes(sinkType)) {
14534
+ return { sanitized: true, sanitizer: san };
14535
+ }
14536
+ } else if (san.sanitizes.length > 0) {
14537
+ return { sanitized: true, sanitizer: san };
14538
+ }
14451
14539
  }
14452
14540
  }
14453
14541
  }