circle-ir 3.140.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 +295 -17
  33. package/dist/core/circle-ir-core.cjs +142 -11
  34. package/dist/core/circle-ir-core.js +142 -11
  35. package/dist/core/extractors/calls.js +104 -18
  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);
@@ -6347,19 +6353,43 @@ function resolveReceiverType(receiver, context) {
6347
6353
  return resolveFqn(simple, context);
6348
6354
  }
6349
6355
  }
6350
- const chained = receiver.match(/^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\s*\([^()]*\)$/);
6351
- if (chained) {
6352
- const varName = chained[1];
6353
- const methodName = chained[2];
6354
- const varType = context.localVarTypes.get(varName) ?? context.paramTypes.get(varName) ?? context.fieldTypes.get(varName);
6355
- if (varType) {
6356
- const returnType = JAVA_CHAINED_FACTORY_RETURN_TYPES[stripGenerics(varType)]?.[methodName];
6356
+ const chain = splitChainedReceiver(receiver);
6357
+ if (chain) {
6358
+ const prefixType = resolveReceiverType(chain.prefix, context);
6359
+ if (prefixType.simpleName) {
6360
+ const returnType = JAVA_CHAINED_FACTORY_RETURN_TYPES[prefixType.simpleName]?.[chain.methodName];
6357
6361
  if (returnType) return resolveFqn(returnType, context);
6358
6362
  }
6359
6363
  }
6360
6364
  return { simpleName: null, fqn: null };
6361
6365
  }
6366
+ function splitChainedReceiver(receiver) {
6367
+ if (!receiver.endsWith(")")) return null;
6368
+ let depth = 0;
6369
+ let openIdx = -1;
6370
+ for (let i2 = receiver.length - 1; i2 >= 0; i2--) {
6371
+ const c = receiver[i2];
6372
+ if (c === ")") depth++;
6373
+ else if (c === "(") {
6374
+ depth--;
6375
+ if (depth === 0) {
6376
+ openIdx = i2;
6377
+ break;
6378
+ }
6379
+ }
6380
+ }
6381
+ if (openIdx <= 0) return null;
6382
+ const beforeParen = receiver.substring(0, openIdx);
6383
+ const dotIdx = beforeParen.lastIndexOf(".");
6384
+ if (dotIdx <= 0) return null;
6385
+ const methodName = beforeParen.substring(dotIdx + 1).trim();
6386
+ if (!/^[A-Za-z_$][\w$]*$/.test(methodName)) return null;
6387
+ const prefix = beforeParen.substring(0, dotIdx).trim();
6388
+ if (!prefix) return null;
6389
+ return { prefix, methodName };
6390
+ }
6362
6391
  var JAVA_CHAINED_FACTORY_RETURN_TYPES = {
6392
+ // Servlet API (Sprint 91 / #117)
6363
6393
  HttpServletRequest: {
6364
6394
  getSession: "HttpSession",
6365
6395
  getServletContext: "ServletContext",
@@ -6370,6 +6400,32 @@ var JAVA_CHAINED_FACTORY_RETURN_TYPES = {
6370
6400
  },
6371
6401
  ServletContext: {
6372
6402
  getRequestDispatcher: "RequestDispatcher"
6403
+ },
6404
+ // JAXP DOM / SAX / XPath / Transformer / StAX factories (Sprint 92 / #189)
6405
+ DocumentBuilderFactory: {
6406
+ newInstance: "DocumentBuilderFactory",
6407
+ newDocumentBuilder: "DocumentBuilder"
6408
+ },
6409
+ SAXParserFactory: {
6410
+ newInstance: "SAXParserFactory",
6411
+ newSAXParser: "SAXParser"
6412
+ },
6413
+ SAXParser: {
6414
+ getXMLReader: "XMLReader"
6415
+ },
6416
+ XPathFactory: {
6417
+ newInstance: "XPathFactory",
6418
+ newXPath: "XPath"
6419
+ },
6420
+ TransformerFactory: {
6421
+ newInstance: "TransformerFactory",
6422
+ newTransformer: "Transformer"
6423
+ },
6424
+ XMLInputFactory: {
6425
+ newInstance: "XMLInputFactory",
6426
+ newFactory: "XMLInputFactory",
6427
+ createXMLStreamReader: "XMLStreamReader",
6428
+ createXMLEventReader: "XMLEventReader"
6373
6429
  }
6374
6430
  };
6375
6431
  function resolveFqn(simpleName, context) {
@@ -11401,6 +11457,14 @@ var DEFAULT_SINKS = [
11401
11457
  // reflection remains a downstream concern of body-writing sinks.
11402
11458
  { method: "setHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
11403
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] },
11404
11468
  // Note: `sendRedirect` is primarily classified as `ssrf` / open-redirect
11405
11469
  // (CWE-601) further down — see entry near line 1195. CRLF via Location
11406
11470
  // header is a secondary concern; keeping the canonical SSRF entry avoids
@@ -12840,11 +12904,62 @@ var DEFAULT_SANITIZERS = [
12840
12904
  { method: "UUID", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12841
12905
  { method: "Decimal", class: "decimal", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] }
12842
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
+ ];
12843
12957
  function getDefaultConfig() {
12844
12958
  return {
12845
12959
  sources: DEFAULT_SOURCES,
12846
12960
  sinks: DEFAULT_SINKS,
12847
- sanitizers: DEFAULT_SANITIZERS
12961
+ sanitizers: DEFAULT_SANITIZERS,
12962
+ sinkSemantics: DEFAULT_SINK_SEMANTICS
12848
12963
  };
12849
12964
  }
12850
12965
  var DEFAULT_HEADER_RULES = [
@@ -13688,6 +13803,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13688
13803
  const confidence = calculateSinkConfidence(call, pattern);
13689
13804
  const existing = sinkMap.get(key);
13690
13805
  if (!existing || confidence > existing.confidence) {
13806
+ const receiverType = call.receiver_type;
13807
+ const simpleClass = receiverType ? receiverType.split(".").pop() || void 0 : void 0;
13691
13808
  sinkMap.set(key, {
13692
13809
  type: pattern.type,
13693
13810
  cwe: pattern.cwe,
@@ -13695,7 +13812,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13695
13812
  line: call.location.line,
13696
13813
  confidence,
13697
13814
  method: call.method_name,
13698
- argPositions: pattern.arg_positions
13815
+ argPositions: pattern.arg_positions,
13816
+ class: simpleClass
13699
13817
  });
13700
13818
  }
13701
13819
  }
@@ -14650,12 +14768,20 @@ function canSourceReachSink(sourceType, sinkType) {
14650
14768
  // flow detector silently dropped `http_* → trust_boundary` co-located
14651
14769
  // flows like `req.getSession().setAttribute("u", req.getParameter("u"))`
14652
14770
  // (0% recall on OWASP Java trustbound category).
14653
- 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"],
14654
14780
  http_body: ["sql_injection", "command_injection", "deserialization", "xxe", "xss", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment", "open_redirect", "trust_boundary"],
14655
14781
  http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect", "trust_boundary"],
14656
14782
  http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect", "trust_boundary"],
14657
14783
  http_path: ["path_traversal", "sql_injection", "ssrf", "mybatis_mapper_call", "open_redirect", "trust_boundary"],
14658
- 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"],
14659
14785
  // ssrf added Sprint 57 #200: bash CGI/webhook handlers and scripts that
14660
14786
  // take a URL on stdin or as a positional CLI arg (`curl "$1"`,
14661
14787
  // `wget "$(read line)"`) and curl/wget it server-side are textbook SSRF
@@ -14677,6 +14803,15 @@ function canSourceReachSink(sourceType, sinkType) {
14677
14803
  const validSinks = sourceToSinkMapping[sourceType];
14678
14804
  return validSinks ? validSinks.includes(sinkType) : false;
14679
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
+ }
14680
14815
 
14681
14816
  // src/analysis/findings-instrumentation.ts
14682
14817
  var instrumentEnabled = false;
@@ -28917,6 +29052,80 @@ function findJsTemplateInjectionSstiFindings(code, file) {
28917
29052
  return findings;
28918
29053
  }
28919
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
+
28920
29129
  // src/analysis/passes/sink-filter-pass.ts
28921
29130
  var JS_XSS_SANITIZERS = [
28922
29131
  /\bDOMPurify\.sanitize\s*\(/,
@@ -29464,7 +29673,7 @@ var SinkFilterPass = class {
29464
29673
  const sourceLines = ctx.code.split("\n");
29465
29674
  filtered = filtered.filter((sink) => {
29466
29675
  if (sink.type !== "command_injection") return true;
29467
- if (sink.method !== "ProcessBuilder") return true;
29676
+ if (sink.method !== "ProcessBuilder" && sink.method !== "start") return true;
29468
29677
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
29469
29678
  if (!/\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText)) return true;
29470
29679
  if (PROCESS_BUILDER_ARGV_FORM_RE.test(sinkLineText)) return false;
@@ -30208,6 +30417,50 @@ function filterSanitizedSinks(sinks, sanitizers, calls) {
30208
30417
  });
30209
30418
  }
30210
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
+
30211
30464
  // src/analysis/passes/taint-propagation-pass.ts
30212
30465
  var TaintPropagationPass = class {
30213
30466
  name = "taint-propagation";
@@ -30866,6 +31119,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
30866
31119
  }
30867
31120
  const re = reCache.get(source.variable);
30868
31121
  if (!re || !re.test(expr)) continue;
31122
+ if (!sourceSemanticsAllowed(source, sink.type)) continue;
30869
31123
  if (flows.some(
30870
31124
  (f) => f.source_line === source.line && f.sink_line === sink.line && f.sink_type === sink.type
30871
31125
  )) continue;
@@ -30894,7 +31148,6 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
30894
31148
  }
30895
31149
  const sourcesByLine = /* @__PURE__ */ new Map();
30896
31150
  for (const s of sources) {
30897
- if (s.variable && s.variable.length > 0) continue;
30898
31151
  const arr = sourcesByLine.get(s.line) ?? [];
30899
31152
  arr.push(s);
30900
31153
  sourcesByLine.set(s.line, arr);
@@ -30905,6 +31158,19 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
30905
31158
  if (!colocSources || colocSources.length === 0) continue;
30906
31159
  for (const source of colocSources) {
30907
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
+ }
30908
31174
  if (source.type === "file_input" && sink.type === "path_traversal" && sink.method && source.location.includes(`${sink.method}(`)) {
30909
31175
  continue;
30910
31176
  }
@@ -35009,6 +35275,13 @@ function isProtocolMandatedCryptoFile(file, code) {
35009
35275
  }
35010
35276
 
35011
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
+ }
35012
35285
  var TEST_PATH_RE3 = /(?:^|[\\/])(?:test|tests|spec|specs|__tests?__|__mocks?__|fixtures?|testdata)(?:[\\/]|$)/i;
35013
35286
  var TEST_FILENAME_RE = /(?:\.(?:test|spec)\.[cm]?[jt]sx?|_test\.go|_test\.py|Test\.java|Tests\.java)$/i;
35014
35287
  function isTestFile(file) {
@@ -35324,6 +35597,7 @@ var ScanSecretsPass = class {
35324
35597
  if (isTestFile(file) || isGeneratedFile(file)) {
35325
35598
  return { providerFindings: 0, entropyFindings: 0 };
35326
35599
  }
35600
+ const demoPath = DEMO_PATH_RE.test(file);
35327
35601
  const lines = ctx.code.split("\n");
35328
35602
  const prior = ctx.getFindings?.() ?? [];
35329
35603
  const seen = /* @__PURE__ */ new Set();
@@ -35350,14 +35624,15 @@ var ScanSecretsPass = class {
35350
35624
  const key = `${lineNum}:hardcoded-credential`;
35351
35625
  if (seen.has(key)) continue;
35352
35626
  seen.add(key);
35627
+ const dg = applyDemoDowngrade(demoPath, pattern.severity, pattern.level);
35353
35628
  ctx.addFinding({
35354
35629
  id: `hardcoded-credential-${file}-${lineNum}`,
35355
35630
  pass: this.name,
35356
35631
  category: this.category,
35357
35632
  rule_id: "hardcoded-credential",
35358
35633
  cwe: "CWE-798",
35359
- severity: pattern.severity,
35360
- level: pattern.level,
35634
+ severity: dg.severity,
35635
+ level: dg.level,
35361
35636
  message: `Hardcoded credential: ${pattern.name} detected`,
35362
35637
  file,
35363
35638
  line: lineNum,
@@ -35377,14 +35652,15 @@ var ScanSecretsPass = class {
35377
35652
  const key = `${lineNum}:hardcoded-credential`;
35378
35653
  if (seen.has(key)) continue;
35379
35654
  seen.add(key);
35655
+ const dg = applyDemoDowngrade(demoPath, "high", "error");
35380
35656
  ctx.addFinding({
35381
35657
  id: `hardcoded-credential-${file}-${lineNum}`,
35382
35658
  pass: this.name,
35383
35659
  category: this.category,
35384
35660
  rule_id: "hardcoded-credential",
35385
35661
  cwe: "CWE-798",
35386
- severity: "high",
35387
- level: "error",
35662
+ severity: dg.severity,
35663
+ level: dg.level,
35388
35664
  message: `Hardcoded credential: \`${hit.name}\` assigned a literal value`,
35389
35665
  file,
35390
35666
  line: lineNum,
@@ -39698,7 +39974,9 @@ async function analyze(code, filePath, language, options = {}) {
39698
39974
  pipeline.add(new TaintMatcherPass());
39699
39975
  pipeline.add(new ConstantPropagationPass(tree));
39700
39976
  pipeline.add(new LanguageSourcesPass());
39977
+ if (!disabledPasses.has("source-semantics")) pipeline.add(new SourceSemanticsPass());
39701
39978
  pipeline.add(new SinkFilterPass());
39979
+ if (!disabledPasses.has("sink-semantics")) pipeline.add(new SinkSemanticsPass());
39702
39980
  pipeline.add(new TaintPropagationPass());
39703
39981
  pipeline.add(new InterproceduralPass({
39704
39982
  enableEntryPointGate: options.enableEntryPointGate ?? true