circle-ir 3.141.0 → 3.144.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 (41) 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/scan-secrets-pass.d.ts.map +1 -1
  11. package/dist/analysis/passes/scan-secrets-pass.js +37 -4
  12. package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
  13. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  14. package/dist/analysis/passes/sink-filter-pass.js +10 -1
  15. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  16. package/dist/analysis/passes/sink-semantics-pass.d.ts +52 -0
  17. package/dist/analysis/passes/sink-semantics-pass.d.ts.map +1 -0
  18. package/dist/analysis/passes/sink-semantics-pass.js +100 -0
  19. package/dist/analysis/passes/sink-semantics-pass.js.map +1 -0
  20. package/dist/analysis/passes/source-semantics-pass.d.ts +66 -0
  21. package/dist/analysis/passes/source-semantics-pass.d.ts.map +1 -0
  22. package/dist/analysis/passes/source-semantics-pass.js +165 -0
  23. package/dist/analysis/passes/source-semantics-pass.js.map +1 -0
  24. package/dist/analysis/passes/taint-propagation-pass.js +60 -9
  25. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  26. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  27. package/dist/analysis/taint-matcher.js +10 -0
  28. package/dist/analysis/taint-matcher.js.map +1 -1
  29. package/dist/analyzer.d.ts.map +1 -1
  30. package/dist/analyzer.js +14 -0
  31. package/dist/analyzer.js.map +1 -1
  32. package/dist/browser/circle-ir.js +238 -10
  33. package/dist/core/circle-ir-core.cjs +85 -4
  34. package/dist/core/circle-ir-core.js +85 -4
  35. package/dist/core/extractors/calls.js +14 -0
  36. package/dist/core/extractors/calls.js.map +1 -1
  37. package/dist/types/config.d.ts +47 -0
  38. package/dist/types/config.d.ts.map +1 -1
  39. package/dist/types/index.d.ts +43 -0
  40. package/dist/types/index.d.ts.map +1 -1
  41. package/package.json +1 -1
@@ -6337,6 +6337,12 @@ function resolveReceiverType(receiver, context) {
6337
6337
  return { simpleName: null, fqn: null };
6338
6338
  }
6339
6339
  if (receiver === "super") return { simpleName: null, fqn: null };
6340
+ const ctorMatch = receiver.match(/^new\s+([A-Za-z_$][\w$.]*)\s*[<(]/);
6341
+ if (ctorMatch) {
6342
+ const ctorClass = ctorMatch[1];
6343
+ const simple = ctorClass.includes(".") ? ctorClass.substring(ctorClass.lastIndexOf(".") + 1) : ctorClass;
6344
+ return resolveFqn(simple, context);
6345
+ }
6340
6346
  const declaredType = context.localVarTypes.get(receiver) ?? context.paramTypes.get(receiver) ?? context.fieldTypes.get(receiver);
6341
6347
  if (declaredType) {
6342
6348
  return resolveFqn(stripGenerics(declaredType), context);
@@ -11451,6 +11457,14 @@ var DEFAULT_SINKS = [
11451
11457
  // reflection remains a downstream concern of body-writing sinks.
11452
11458
  { method: "setHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
11453
11459
  { method: "addHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
11460
+ // Cookie constructor + addCookie — HTTP response splitting via cookie
11461
+ // name/value (CWE-113). Reflects the #189 Sprint 92 V02SetCookie fixture
11462
+ // where `res.addCookie(new Cookie(name, req.getParameter("v")))` allows
11463
+ // CRLF injection into the Set-Cookie header. Both the ctor arg[1] (value)
11464
+ // and the addCookie arg[0] (Cookie handle) are flagged so intermediate-var
11465
+ // and inline-ctor shapes both surface.
11466
+ { method: "Cookie", class: "constructor", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0, 1] },
11467
+ { method: "addCookie", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0] },
11454
11468
  // Note: `sendRedirect` is primarily classified as `ssrf` / open-redirect
11455
11469
  // (CWE-601) further down — see entry near line 1195. CRLF via Location
11456
11470
  // header is a secondary concern; keeping the canonical SSRF entry avoids
@@ -12890,11 +12904,62 @@ var DEFAULT_SANITIZERS = [
12890
12904
  { method: "UUID", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12891
12905
  { method: "Decimal", class: "decimal", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] }
12892
12906
  ];
12907
+ var DEFAULT_SINK_SEMANTICS = [
12908
+ {
12909
+ signature: "Jedis#executeCommand",
12910
+ real_class: "db_protocol",
12911
+ overrides: ["command_injection", "code_injection"],
12912
+ note: "Redis wire-protocol serialization, not OS exec"
12913
+ },
12914
+ {
12915
+ signature: "Connection#executeCommand",
12916
+ real_class: "db_protocol",
12917
+ overrides: ["command_injection", "code_injection"],
12918
+ note: "Jedis abstract Connection base"
12919
+ },
12920
+ {
12921
+ signature: "JedisCluster#executeCommand",
12922
+ real_class: "db_protocol",
12923
+ overrides: ["command_injection", "code_injection"],
12924
+ note: "Jedis cluster client"
12925
+ },
12926
+ {
12927
+ signature: "Func1#exec",
12928
+ real_class: "functional_dispatch",
12929
+ overrides: ["command_injection", "code_injection"],
12930
+ note: "RxJava functional dispatch, not OS exec"
12931
+ },
12932
+ {
12933
+ signature: "Action0#call",
12934
+ real_class: "functional_dispatch",
12935
+ overrides: ["command_injection"],
12936
+ note: "RxJava Action0 dispatch"
12937
+ },
12938
+ {
12939
+ signature: "Action1#call",
12940
+ real_class: "functional_dispatch",
12941
+ overrides: ["command_injection"],
12942
+ note: "RxJava Action1 dispatch"
12943
+ },
12944
+ {
12945
+ signature: "Unsafe#defineAnonymousClass",
12946
+ real_class: "jdk_internal",
12947
+ overrides: ["code_injection"],
12948
+ note: "sun.misc.Unsafe JDK-internal reflective bridge"
12949
+ },
12950
+ {
12951
+ signature: "MethodHandle#invokeExact",
12952
+ real_class: "jdk_internal",
12953
+ overrides: ["code_injection"],
12954
+ note: "java.lang.invoke.MethodHandle \u2014 JDK-internal"
12955
+ }
12956
+ ];
12893
12957
  function getDefaultConfig() {
12894
12958
  return {
12895
12959
  sources: DEFAULT_SOURCES,
12896
12960
  sinks: DEFAULT_SINKS,
12897
- sanitizers: DEFAULT_SANITIZERS
12961
+ sanitizers: DEFAULT_SANITIZERS,
12962
+ sinkSemantics: DEFAULT_SINK_SEMANTICS
12898
12963
  };
12899
12964
  }
12900
12965
  var DEFAULT_HEADER_RULES = [
@@ -13738,6 +13803,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13738
13803
  const confidence = calculateSinkConfidence(call, pattern);
13739
13804
  const existing = sinkMap.get(key);
13740
13805
  if (!existing || confidence > existing.confidence) {
13806
+ const receiverType = call.receiver_type;
13807
+ const simpleClass = receiverType ? receiverType.split(".").pop() || void 0 : void 0;
13741
13808
  sinkMap.set(key, {
13742
13809
  type: pattern.type,
13743
13810
  cwe: pattern.cwe,
@@ -13745,7 +13812,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13745
13812
  line: call.location.line,
13746
13813
  confidence,
13747
13814
  method: call.method_name,
13748
- argPositions: pattern.arg_positions
13815
+ argPositions: pattern.arg_positions,
13816
+ class: simpleClass
13749
13817
  });
13750
13818
  }
13751
13819
  }
@@ -14700,12 +14768,20 @@ function canSourceReachSink(sourceType, sinkType) {
14700
14768
  // flow detector silently dropped `http_* → trust_boundary` co-located
14701
14769
  // flows like `req.getSession().setAttribute("u", req.getParameter("u"))`
14702
14770
  // (0% recall on OWASP Java trustbound category).
14703
- http_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect", "trust_boundary"],
14771
+ // deserialization added Sprint 93 (#189): SnakeYAML/Jackson/etc. sinks
14772
+ // like `new Yaml().load(req.getParameter("y"))` are real RCE gadget chains
14773
+ // (CWE-502). The reach map previously restricted deserialization to
14774
+ // http_body so http_param/http_query request-derived values feeding
14775
+ // Yaml.load, ObjectInputStream ctor, XMLDecoder ctor, etc. silently
14776
+ // dropped their inline-colocation flow. Typed-overload FPs are gated by
14777
+ // the sink pattern's `safe_if_class_literal_at` flag (Jackson readValue,
14778
+ // Yaml.loadAs, Gson.fromJson) so the wider reach does not regress.
14779
+ http_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect", "trust_boundary", "deserialization"],
14704
14780
  http_body: ["sql_injection", "command_injection", "deserialization", "xxe", "xss", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment", "open_redirect", "trust_boundary"],
14705
14781
  http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect", "trust_boundary"],
14706
14782
  http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect", "trust_boundary"],
14707
14783
  http_path: ["path_traversal", "sql_injection", "ssrf", "mybatis_mapper_call", "open_redirect", "trust_boundary"],
14708
- http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect", "trust_boundary"],
14784
+ http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect", "trust_boundary", "deserialization"],
14709
14785
  // ssrf added Sprint 57 #200: bash CGI/webhook handlers and scripts that
14710
14786
  // take a URL on stdin or as a positional CLI arg (`curl "$1"`,
14711
14787
  // `wget "$(read line)"`) and curl/wget it server-side are textbook SSRF
@@ -14727,6 +14803,15 @@ function canSourceReachSink(sourceType, sinkType) {
14727
14803
  const validSinks = sourceToSinkMapping[sourceType];
14728
14804
  return validSinks ? validSinks.includes(sinkType) : false;
14729
14805
  }
14806
+ function sourceSemanticsAllowed(source, sinkType) {
14807
+ if (source.constant === true) {
14808
+ return false;
14809
+ }
14810
+ if (source.spi === true) {
14811
+ return sinkType === "code_injection";
14812
+ }
14813
+ return true;
14814
+ }
14730
14815
 
14731
14816
  // src/analysis/findings-instrumentation.ts
14732
14817
  var instrumentEnabled = false;
@@ -28967,6 +29052,80 @@ function findJsTemplateInjectionSstiFindings(code, file) {
28967
29052
  return findings;
28968
29053
  }
28969
29054
 
29055
+ // src/analysis/passes/source-semantics-pass.ts
29056
+ var DEMO_PATH_RE = /(?:^|\/)(?:demo|example|examples|samples|integration-tests|integration_tests)(?:\/|$)/i;
29057
+ var CONST_STRING_ASSIGN_RE = /^\s*(?:final\s+|static\s+final\s+)?[A-Za-z_][\w.<>\[\]]*\s+[A-Za-z_]\w*\s*=\s*"[^"]*"\s*;?\s*$/;
29058
+ var STATIC_FINAL_RE = /^\s*(?:public\s+|private\s+|protected\s+)?static\s+final\s+/;
29059
+ var ENUM_CONST_REF_RE = /=\s*[A-Z][A-Za-z0-9_]*\.[A-Z][A-Z0-9_]*\s*;?\s*$/;
29060
+ function isConstantSource(code) {
29061
+ if (!code) return false;
29062
+ if (CONST_STRING_ASSIGN_RE.test(code)) return true;
29063
+ if (STATIC_FINAL_RE.test(code)) {
29064
+ const rhs = code.split("=").slice(1).join("=").trim();
29065
+ if (rhs.length === 0) return false;
29066
+ if (/^"[^"]*"\s*;?\s*$/.test(rhs)) return true;
29067
+ if (/^-?\d+(?:\.\d+)?[fFdDlL]?\s*;?\s*$/.test(rhs)) return true;
29068
+ if (/^(?:true|false)\s*;?\s*$/.test(rhs)) return true;
29069
+ if (/^[A-Za-z_][\w.]*\s*;?\s*$/.test(rhs)) return true;
29070
+ return false;
29071
+ }
29072
+ if (ENUM_CONST_REF_RE.test(code)) return true;
29073
+ return false;
29074
+ }
29075
+ var SERVICE_LOADER_RE = /\bServiceLoader\.(?:load|loadInstalled|stream)\s*\(/;
29076
+ var CLASS_FOR_NAME_RE = /\bClass\.forName\s*\(/;
29077
+ var META_INF_SERVICES_RE = /getResources?\s*\(\s*"META-INF\/services\//;
29078
+ var SPI_WINDOW = 30;
29079
+ function isSpiSource(source, lines) {
29080
+ const code = source.code;
29081
+ if (!code) return false;
29082
+ if (SERVICE_LOADER_RE.test(code)) return true;
29083
+ if (CLASS_FOR_NAME_RE.test(code)) {
29084
+ const start2 = Math.max(0, source.line - 1 - SPI_WINDOW);
29085
+ const end = Math.min(lines.length, source.line - 1 + SPI_WINDOW + 1);
29086
+ for (let i2 = start2; i2 < end; i2++) {
29087
+ if (META_INF_SERVICES_RE.test(lines[i2])) return true;
29088
+ }
29089
+ }
29090
+ return false;
29091
+ }
29092
+ function isDemoPathFile(file) {
29093
+ if (!file) return false;
29094
+ return DEMO_PATH_RE.test(file);
29095
+ }
29096
+ var SourceSemanticsPass = class {
29097
+ name = "source-semantics";
29098
+ category = "security";
29099
+ run(ctx) {
29100
+ const { graph, code } = ctx;
29101
+ const sources = graph.ir.taint.sources;
29102
+ if (sources.length === 0) {
29103
+ return { constantCount: 0, spiCount: 0, demoPathCount: 0 };
29104
+ }
29105
+ const file = graph.ir.meta.file;
29106
+ const demoPath = isDemoPathFile(file);
29107
+ const lines = code.split("\n");
29108
+ let constantCount = 0;
29109
+ let spiCount = 0;
29110
+ let demoPathCount = 0;
29111
+ for (const source of sources) {
29112
+ if (isConstantSource(source.code)) {
29113
+ source.constant = true;
29114
+ constantCount++;
29115
+ }
29116
+ if (isSpiSource(source, lines)) {
29117
+ source.spi = true;
29118
+ spiCount++;
29119
+ }
29120
+ if (demoPath) {
29121
+ source.demoPath = true;
29122
+ demoPathCount++;
29123
+ }
29124
+ }
29125
+ return { constantCount, spiCount, demoPathCount };
29126
+ }
29127
+ };
29128
+
28970
29129
  // src/analysis/passes/sink-filter-pass.ts
28971
29130
  var JS_XSS_SANITIZERS = [
28972
29131
  /\bDOMPurify\.sanitize\s*\(/,
@@ -29514,7 +29673,7 @@ var SinkFilterPass = class {
29514
29673
  const sourceLines = ctx.code.split("\n");
29515
29674
  filtered = filtered.filter((sink) => {
29516
29675
  if (sink.type !== "command_injection") return true;
29517
- if (sink.method !== "ProcessBuilder") return true;
29676
+ if (sink.method !== "ProcessBuilder" && sink.method !== "start") return true;
29518
29677
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
29519
29678
  if (!/\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText)) return true;
29520
29679
  if (PROCESS_BUILDER_ARGV_FORM_RE.test(sinkLineText)) return false;
@@ -30258,6 +30417,50 @@ function filterSanitizedSinks(sinks, sanitizers, calls) {
30258
30417
  });
30259
30418
  }
30260
30419
 
30420
+ // src/analysis/passes/sink-semantics-pass.ts
30421
+ function buildRegistry(entries) {
30422
+ const registry = /* @__PURE__ */ new Map();
30423
+ for (const entry of entries) {
30424
+ const existing = registry.get(entry.signature);
30425
+ if (existing) {
30426
+ for (const t of entry.overrides) existing.add(t);
30427
+ } else {
30428
+ registry.set(entry.signature, new Set(entry.overrides));
30429
+ }
30430
+ }
30431
+ return registry;
30432
+ }
30433
+ var SinkSemanticsPass = class {
30434
+ name = "sink-semantics";
30435
+ category = "security";
30436
+ run(ctx) {
30437
+ const { graph, config } = ctx;
30438
+ const entries = config.sinkSemantics ?? [];
30439
+ if (entries.length === 0) {
30440
+ return { droppedCount: 0, registrySize: 0 };
30441
+ }
30442
+ const registry = buildRegistry(entries);
30443
+ const sinks = graph.ir.taint.sinks;
30444
+ let droppedCount = 0;
30445
+ const kept = sinks.filter((sink) => {
30446
+ if (!sink.class || !sink.method) return true;
30447
+ const signature = `${sink.class}#${sink.method}`;
30448
+ const overrides = registry.get(signature);
30449
+ if (!overrides) return true;
30450
+ if (overrides.has(sink.type)) {
30451
+ droppedCount++;
30452
+ return false;
30453
+ }
30454
+ return true;
30455
+ });
30456
+ if (droppedCount > 0) {
30457
+ sinks.length = 0;
30458
+ sinks.push(...kept);
30459
+ }
30460
+ return { droppedCount, registrySize: registry.size };
30461
+ }
30462
+ };
30463
+
30261
30464
  // src/analysis/passes/taint-propagation-pass.ts
30262
30465
  var TaintPropagationPass = class {
30263
30466
  name = "taint-propagation";
@@ -30916,6 +31119,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
30916
31119
  }
30917
31120
  const re = reCache.get(source.variable);
30918
31121
  if (!re || !re.test(expr)) continue;
31122
+ if (!sourceSemanticsAllowed(source, sink.type)) continue;
30919
31123
  if (flows.some(
30920
31124
  (f) => f.source_line === source.line && f.sink_line === sink.line && f.sink_type === sink.type
30921
31125
  )) continue;
@@ -30944,7 +31148,6 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
30944
31148
  }
30945
31149
  const sourcesByLine = /* @__PURE__ */ new Map();
30946
31150
  for (const s of sources) {
30947
- if (s.variable && s.variable.length > 0) continue;
30948
31151
  const arr = sourcesByLine.get(s.line) ?? [];
30949
31152
  arr.push(s);
30950
31153
  sourcesByLine.set(s.line, arr);
@@ -30955,6 +31158,19 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
30955
31158
  if (!colocSources || colocSources.length === 0) continue;
30956
31159
  for (const source of colocSources) {
30957
31160
  if (!canSourceReachSink(source.type, sink.type)) continue;
31161
+ if (!sourceSemanticsAllowed(source, sink.type)) continue;
31162
+ const sourceVar = source.variable;
31163
+ if (sourceVar && sourceVar.length > 0) {
31164
+ const sinkCode = sink.code;
31165
+ if (!sinkCode) {
31166
+ continue;
31167
+ }
31168
+ const assignMatch = sinkCode.match(/^\s*(?:[A-Za-z_][\w.<>[\]\s,?]*\s+)?[A-Za-z_]\w*\s*=(?!=)\s*/);
31169
+ const rhs = assignMatch ? sinkCode.slice(assignMatch[0].length) : sinkCode;
31170
+ if (new RegExp(`\\b${sourceVar}\\b`).test(rhs)) {
31171
+ continue;
31172
+ }
31173
+ }
30958
31174
  if (source.type === "file_input" && sink.type === "path_traversal" && sink.method && source.location.includes(`${sink.method}(`)) {
30959
31175
  continue;
30960
31176
  }
@@ -35059,6 +35275,13 @@ function isProtocolMandatedCryptoFile(file, code) {
35059
35275
  }
35060
35276
 
35061
35277
  // src/analysis/passes/scan-secrets-pass.ts
35278
+ function applyDemoDowngrade(demoPath, severity, level) {
35279
+ if (!demoPath) return { severity, level };
35280
+ if (severity === "high") {
35281
+ return { severity: "low", level: "note" };
35282
+ }
35283
+ return { severity, level };
35284
+ }
35062
35285
  var TEST_PATH_RE3 = /(?:^|[\\/])(?:test|tests|spec|specs|__tests?__|__mocks?__|fixtures?|testdata)(?:[\\/]|$)/i;
35063
35286
  var TEST_FILENAME_RE = /(?:\.(?:test|spec)\.[cm]?[jt]sx?|_test\.go|_test\.py|Test\.java|Tests\.java)$/i;
35064
35287
  function isTestFile(file) {
@@ -35374,6 +35597,7 @@ var ScanSecretsPass = class {
35374
35597
  if (isTestFile(file) || isGeneratedFile(file)) {
35375
35598
  return { providerFindings: 0, entropyFindings: 0 };
35376
35599
  }
35600
+ const demoPath = DEMO_PATH_RE.test(file);
35377
35601
  const lines = ctx.code.split("\n");
35378
35602
  const prior = ctx.getFindings?.() ?? [];
35379
35603
  const seen = /* @__PURE__ */ new Set();
@@ -35400,14 +35624,15 @@ var ScanSecretsPass = class {
35400
35624
  const key = `${lineNum}:hardcoded-credential`;
35401
35625
  if (seen.has(key)) continue;
35402
35626
  seen.add(key);
35627
+ const dg = applyDemoDowngrade(demoPath, pattern.severity, pattern.level);
35403
35628
  ctx.addFinding({
35404
35629
  id: `hardcoded-credential-${file}-${lineNum}`,
35405
35630
  pass: this.name,
35406
35631
  category: this.category,
35407
35632
  rule_id: "hardcoded-credential",
35408
35633
  cwe: "CWE-798",
35409
- severity: pattern.severity,
35410
- level: pattern.level,
35634
+ severity: dg.severity,
35635
+ level: dg.level,
35411
35636
  message: `Hardcoded credential: ${pattern.name} detected`,
35412
35637
  file,
35413
35638
  line: lineNum,
@@ -35427,14 +35652,15 @@ var ScanSecretsPass = class {
35427
35652
  const key = `${lineNum}:hardcoded-credential`;
35428
35653
  if (seen.has(key)) continue;
35429
35654
  seen.add(key);
35655
+ const dg = applyDemoDowngrade(demoPath, "high", "error");
35430
35656
  ctx.addFinding({
35431
35657
  id: `hardcoded-credential-${file}-${lineNum}`,
35432
35658
  pass: this.name,
35433
35659
  category: this.category,
35434
35660
  rule_id: "hardcoded-credential",
35435
35661
  cwe: "CWE-798",
35436
- severity: "high",
35437
- level: "error",
35662
+ severity: dg.severity,
35663
+ level: dg.level,
35438
35664
  message: `Hardcoded credential: \`${hit.name}\` assigned a literal value`,
35439
35665
  file,
35440
35666
  line: lineNum,
@@ -39748,7 +39974,9 @@ async function analyze(code, filePath, language, options = {}) {
39748
39974
  pipeline.add(new TaintMatcherPass());
39749
39975
  pipeline.add(new ConstantPropagationPass(tree));
39750
39976
  pipeline.add(new LanguageSourcesPass());
39977
+ if (!disabledPasses.has("source-semantics")) pipeline.add(new SourceSemanticsPass());
39751
39978
  pipeline.add(new SinkFilterPass());
39979
+ if (!disabledPasses.has("sink-semantics")) pipeline.add(new SinkSemanticsPass());
39752
39980
  pipeline.add(new TaintPropagationPass());
39753
39981
  pipeline.add(new InterproceduralPass({
39754
39982
  enableEntryPointGate: options.enableEntryPointGate ?? true
@@ -6383,6 +6383,12 @@ function resolveReceiverType(receiver, context) {
6383
6383
  return { simpleName: null, fqn: null };
6384
6384
  }
6385
6385
  if (receiver === "super") return { simpleName: null, fqn: null };
6386
+ const ctorMatch = receiver.match(/^new\s+([A-Za-z_$][\w$.]*)\s*[<(]/);
6387
+ if (ctorMatch) {
6388
+ const ctorClass = ctorMatch[1];
6389
+ const simple = ctorClass.includes(".") ? ctorClass.substring(ctorClass.lastIndexOf(".") + 1) : ctorClass;
6390
+ return resolveFqn(simple, context);
6391
+ }
6386
6392
  const declaredType = context.localVarTypes.get(receiver) ?? context.paramTypes.get(receiver) ?? context.fieldTypes.get(receiver);
6387
6393
  if (declaredType) {
6388
6394
  return resolveFqn(stripGenerics(declaredType), context);
@@ -10109,12 +10115,25 @@ function loadSinkConfigs(configs) {
10109
10115
  }
10110
10116
  return { sinks, sanitizers };
10111
10117
  }
10112
- function createTaintConfig(sourceContents, sinkContents) {
10118
+ function loadSinkSemanticsConfigs(configs) {
10119
+ const entries = [];
10120
+ for (const config of configs) {
10121
+ if (config.sinks) {
10122
+ entries.push(...config.sinks);
10123
+ }
10124
+ }
10125
+ return entries;
10126
+ }
10127
+ function createTaintConfig(sourceContents, sinkContents, sinkSemanticsContents = []) {
10113
10128
  const sourceConfigs = sourceContents.map((c) => parseConfig(c));
10114
10129
  const sinkConfigs = sinkContents.map((c) => parseConfig(c));
10130
+ const sinkSemanticsConfigs = sinkSemanticsContents.map(
10131
+ (c) => parseConfig(c)
10132
+ );
10115
10133
  const sources = loadSourceConfigs(sourceConfigs);
10116
10134
  const { sinks, sanitizers } = loadSinkConfigs(sinkConfigs);
10117
- return { sources, sinks, sanitizers };
10135
+ const sinkSemantics = loadSinkSemanticsConfigs(sinkSemanticsConfigs);
10136
+ return { sources, sinks, sanitizers, sinkSemantics };
10118
10137
  }
10119
10138
  var DEFAULT_SOURCES = [
10120
10139
  // HTTP Sources (Servlet API)
@@ -10833,6 +10852,14 @@ var DEFAULT_SINKS = [
10833
10852
  // reflection remains a downstream concern of body-writing sinks.
10834
10853
  { method: "setHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10835
10854
  { method: "addHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10855
+ // Cookie constructor + addCookie — HTTP response splitting via cookie
10856
+ // name/value (CWE-113). Reflects the #189 Sprint 92 V02SetCookie fixture
10857
+ // where `res.addCookie(new Cookie(name, req.getParameter("v")))` allows
10858
+ // CRLF injection into the Set-Cookie header. Both the ctor arg[1] (value)
10859
+ // and the addCookie arg[0] (Cookie handle) are flagged so intermediate-var
10860
+ // and inline-ctor shapes both surface.
10861
+ { method: "Cookie", class: "constructor", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0, 1] },
10862
+ { method: "addCookie", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0] },
10836
10863
  // Note: `sendRedirect` is primarily classified as `ssrf` / open-redirect
10837
10864
  // (CWE-601) further down — see entry near line 1195. CRLF via Location
10838
10865
  // header is a secondary concern; keeping the canonical SSRF entry avoids
@@ -12272,11 +12299,62 @@ var DEFAULT_SANITIZERS = [
12272
12299
  { method: "UUID", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12273
12300
  { method: "Decimal", class: "decimal", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] }
12274
12301
  ];
12302
+ var DEFAULT_SINK_SEMANTICS = [
12303
+ {
12304
+ signature: "Jedis#executeCommand",
12305
+ real_class: "db_protocol",
12306
+ overrides: ["command_injection", "code_injection"],
12307
+ note: "Redis wire-protocol serialization, not OS exec"
12308
+ },
12309
+ {
12310
+ signature: "Connection#executeCommand",
12311
+ real_class: "db_protocol",
12312
+ overrides: ["command_injection", "code_injection"],
12313
+ note: "Jedis abstract Connection base"
12314
+ },
12315
+ {
12316
+ signature: "JedisCluster#executeCommand",
12317
+ real_class: "db_protocol",
12318
+ overrides: ["command_injection", "code_injection"],
12319
+ note: "Jedis cluster client"
12320
+ },
12321
+ {
12322
+ signature: "Func1#exec",
12323
+ real_class: "functional_dispatch",
12324
+ overrides: ["command_injection", "code_injection"],
12325
+ note: "RxJava functional dispatch, not OS exec"
12326
+ },
12327
+ {
12328
+ signature: "Action0#call",
12329
+ real_class: "functional_dispatch",
12330
+ overrides: ["command_injection"],
12331
+ note: "RxJava Action0 dispatch"
12332
+ },
12333
+ {
12334
+ signature: "Action1#call",
12335
+ real_class: "functional_dispatch",
12336
+ overrides: ["command_injection"],
12337
+ note: "RxJava Action1 dispatch"
12338
+ },
12339
+ {
12340
+ signature: "Unsafe#defineAnonymousClass",
12341
+ real_class: "jdk_internal",
12342
+ overrides: ["code_injection"],
12343
+ note: "sun.misc.Unsafe JDK-internal reflective bridge"
12344
+ },
12345
+ {
12346
+ signature: "MethodHandle#invokeExact",
12347
+ real_class: "jdk_internal",
12348
+ overrides: ["code_injection"],
12349
+ note: "java.lang.invoke.MethodHandle \u2014 JDK-internal"
12350
+ }
12351
+ ];
12275
12352
  function getDefaultConfig() {
12276
12353
  return {
12277
12354
  sources: DEFAULT_SOURCES,
12278
12355
  sinks: DEFAULT_SINKS,
12279
- sanitizers: DEFAULT_SANITIZERS
12356
+ sanitizers: DEFAULT_SANITIZERS,
12357
+ sinkSemantics: DEFAULT_SINK_SEMANTICS
12280
12358
  };
12281
12359
  }
12282
12360
 
@@ -13033,6 +13111,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13033
13111
  const confidence = calculateSinkConfidence(call, pattern);
13034
13112
  const existing = sinkMap.get(key);
13035
13113
  if (!existing || confidence > existing.confidence) {
13114
+ const receiverType = call.receiver_type;
13115
+ const simpleClass = receiverType ? receiverType.split(".").pop() || void 0 : void 0;
13036
13116
  sinkMap.set(key, {
13037
13117
  type: pattern.type,
13038
13118
  cwe: pattern.cwe,
@@ -13040,7 +13120,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13040
13120
  line: call.location.line,
13041
13121
  confidence,
13042
13122
  method: call.method_name,
13043
- argPositions: pattern.arg_positions
13123
+ argPositions: pattern.arg_positions,
13124
+ class: simpleClass
13044
13125
  });
13045
13126
  }
13046
13127
  }
@@ -6317,6 +6317,12 @@ function resolveReceiverType(receiver, context) {
6317
6317
  return { simpleName: null, fqn: null };
6318
6318
  }
6319
6319
  if (receiver === "super") return { simpleName: null, fqn: null };
6320
+ const ctorMatch = receiver.match(/^new\s+([A-Za-z_$][\w$.]*)\s*[<(]/);
6321
+ if (ctorMatch) {
6322
+ const ctorClass = ctorMatch[1];
6323
+ const simple = ctorClass.includes(".") ? ctorClass.substring(ctorClass.lastIndexOf(".") + 1) : ctorClass;
6324
+ return resolveFqn(simple, context);
6325
+ }
6320
6326
  const declaredType = context.localVarTypes.get(receiver) ?? context.paramTypes.get(receiver) ?? context.fieldTypes.get(receiver);
6321
6327
  if (declaredType) {
6322
6328
  return resolveFqn(stripGenerics(declaredType), context);
@@ -10043,12 +10049,25 @@ function loadSinkConfigs(configs) {
10043
10049
  }
10044
10050
  return { sinks, sanitizers };
10045
10051
  }
10046
- function createTaintConfig(sourceContents, sinkContents) {
10052
+ function loadSinkSemanticsConfigs(configs) {
10053
+ const entries = [];
10054
+ for (const config of configs) {
10055
+ if (config.sinks) {
10056
+ entries.push(...config.sinks);
10057
+ }
10058
+ }
10059
+ return entries;
10060
+ }
10061
+ function createTaintConfig(sourceContents, sinkContents, sinkSemanticsContents = []) {
10047
10062
  const sourceConfigs = sourceContents.map((c) => parseConfig(c));
10048
10063
  const sinkConfigs = sinkContents.map((c) => parseConfig(c));
10064
+ const sinkSemanticsConfigs = sinkSemanticsContents.map(
10065
+ (c) => parseConfig(c)
10066
+ );
10049
10067
  const sources = loadSourceConfigs(sourceConfigs);
10050
10068
  const { sinks, sanitizers } = loadSinkConfigs(sinkConfigs);
10051
- return { sources, sinks, sanitizers };
10069
+ const sinkSemantics = loadSinkSemanticsConfigs(sinkSemanticsConfigs);
10070
+ return { sources, sinks, sanitizers, sinkSemantics };
10052
10071
  }
10053
10072
  var DEFAULT_SOURCES = [
10054
10073
  // HTTP Sources (Servlet API)
@@ -10767,6 +10786,14 @@ var DEFAULT_SINKS = [
10767
10786
  // reflection remains a downstream concern of body-writing sinks.
10768
10787
  { method: "setHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10769
10788
  { method: "addHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10789
+ // Cookie constructor + addCookie — HTTP response splitting via cookie
10790
+ // name/value (CWE-113). Reflects the #189 Sprint 92 V02SetCookie fixture
10791
+ // where `res.addCookie(new Cookie(name, req.getParameter("v")))` allows
10792
+ // CRLF injection into the Set-Cookie header. Both the ctor arg[1] (value)
10793
+ // and the addCookie arg[0] (Cookie handle) are flagged so intermediate-var
10794
+ // and inline-ctor shapes both surface.
10795
+ { method: "Cookie", class: "constructor", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0, 1] },
10796
+ { method: "addCookie", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0] },
10770
10797
  // Note: `sendRedirect` is primarily classified as `ssrf` / open-redirect
10771
10798
  // (CWE-601) further down — see entry near line 1195. CRLF via Location
10772
10799
  // header is a secondary concern; keeping the canonical SSRF entry avoids
@@ -12206,11 +12233,62 @@ var DEFAULT_SANITIZERS = [
12206
12233
  { method: "UUID", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12207
12234
  { method: "Decimal", class: "decimal", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] }
12208
12235
  ];
12236
+ var DEFAULT_SINK_SEMANTICS = [
12237
+ {
12238
+ signature: "Jedis#executeCommand",
12239
+ real_class: "db_protocol",
12240
+ overrides: ["command_injection", "code_injection"],
12241
+ note: "Redis wire-protocol serialization, not OS exec"
12242
+ },
12243
+ {
12244
+ signature: "Connection#executeCommand",
12245
+ real_class: "db_protocol",
12246
+ overrides: ["command_injection", "code_injection"],
12247
+ note: "Jedis abstract Connection base"
12248
+ },
12249
+ {
12250
+ signature: "JedisCluster#executeCommand",
12251
+ real_class: "db_protocol",
12252
+ overrides: ["command_injection", "code_injection"],
12253
+ note: "Jedis cluster client"
12254
+ },
12255
+ {
12256
+ signature: "Func1#exec",
12257
+ real_class: "functional_dispatch",
12258
+ overrides: ["command_injection", "code_injection"],
12259
+ note: "RxJava functional dispatch, not OS exec"
12260
+ },
12261
+ {
12262
+ signature: "Action0#call",
12263
+ real_class: "functional_dispatch",
12264
+ overrides: ["command_injection"],
12265
+ note: "RxJava Action0 dispatch"
12266
+ },
12267
+ {
12268
+ signature: "Action1#call",
12269
+ real_class: "functional_dispatch",
12270
+ overrides: ["command_injection"],
12271
+ note: "RxJava Action1 dispatch"
12272
+ },
12273
+ {
12274
+ signature: "Unsafe#defineAnonymousClass",
12275
+ real_class: "jdk_internal",
12276
+ overrides: ["code_injection"],
12277
+ note: "sun.misc.Unsafe JDK-internal reflective bridge"
12278
+ },
12279
+ {
12280
+ signature: "MethodHandle#invokeExact",
12281
+ real_class: "jdk_internal",
12282
+ overrides: ["code_injection"],
12283
+ note: "java.lang.invoke.MethodHandle \u2014 JDK-internal"
12284
+ }
12285
+ ];
12209
12286
  function getDefaultConfig() {
12210
12287
  return {
12211
12288
  sources: DEFAULT_SOURCES,
12212
12289
  sinks: DEFAULT_SINKS,
12213
- sanitizers: DEFAULT_SANITIZERS
12290
+ sanitizers: DEFAULT_SANITIZERS,
12291
+ sinkSemantics: DEFAULT_SINK_SEMANTICS
12214
12292
  };
12215
12293
  }
12216
12294
 
@@ -12967,6 +13045,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12967
13045
  const confidence = calculateSinkConfidence(call, pattern);
12968
13046
  const existing = sinkMap.get(key);
12969
13047
  if (!existing || confidence > existing.confidence) {
13048
+ const receiverType = call.receiver_type;
13049
+ const simpleClass = receiverType ? receiverType.split(".").pop() || void 0 : void 0;
12970
13050
  sinkMap.set(key, {
12971
13051
  type: pattern.type,
12972
13052
  cwe: pattern.cwe,
@@ -12974,7 +13054,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12974
13054
  line: call.location.line,
12975
13055
  confidence,
12976
13056
  method: call.method_name,
12977
- argPositions: pattern.arg_positions
13057
+ argPositions: pattern.arg_positions,
13058
+ class: simpleClass
12978
13059
  });
12979
13060
  }
12980
13061
  }