circle-ir 3.141.0 → 3.144.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/configs/sink-semantics.json +53 -0
  2. package/dist/analysis/config-loader.d.ts +24 -2
  3. package/dist/analysis/config-loader.d.ts.map +1 -1
  4. package/dist/analysis/config-loader.js +92 -2
  5. package/dist/analysis/config-loader.js.map +1 -1
  6. package/dist/analysis/findings.d.ts +32 -0
  7. package/dist/analysis/findings.d.ts.map +1 -1
  8. package/dist/analysis/findings.js +52 -2
  9. package/dist/analysis/findings.js.map +1 -1
  10. package/dist/analysis/passes/language-sources-pass.d.ts +19 -0
  11. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  12. package/dist/analysis/passes/language-sources-pass.js +133 -3
  13. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  14. package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
  15. package/dist/analysis/passes/scan-secrets-pass.js +37 -4
  16. package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
  17. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  18. package/dist/analysis/passes/sink-filter-pass.js +10 -1
  19. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  20. package/dist/analysis/passes/sink-semantics-pass.d.ts +52 -0
  21. package/dist/analysis/passes/sink-semantics-pass.d.ts.map +1 -0
  22. package/dist/analysis/passes/sink-semantics-pass.js +100 -0
  23. package/dist/analysis/passes/sink-semantics-pass.js.map +1 -0
  24. package/dist/analysis/passes/source-semantics-pass.d.ts +66 -0
  25. package/dist/analysis/passes/source-semantics-pass.d.ts.map +1 -0
  26. package/dist/analysis/passes/source-semantics-pass.js +165 -0
  27. package/dist/analysis/passes/source-semantics-pass.js.map +1 -0
  28. package/dist/analysis/passes/taint-propagation-pass.js +91 -10
  29. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  30. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  31. package/dist/analysis/taint-matcher.js +55 -3
  32. package/dist/analysis/taint-matcher.js.map +1 -1
  33. package/dist/analyzer.d.ts.map +1 -1
  34. package/dist/analyzer.js +14 -0
  35. package/dist/analyzer.js.map +1 -1
  36. package/dist/browser/circle-ir.js +363 -15
  37. package/dist/core/circle-ir-core.cjs +112 -8
  38. package/dist/core/circle-ir-core.js +112 -8
  39. package/dist/core/extractors/calls.js +14 -0
  40. package/dist/core/extractors/calls.js.map +1 -1
  41. package/dist/types/config.d.ts +47 -0
  42. package/dist/types/config.d.ts.map +1 -1
  43. package/dist/types/index.d.ts +43 -0
  44. package/dist/types/index.d.ts.map +1 -1
  45. package/package.json +1 -1
@@ -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 = [
@@ -13204,7 +13269,20 @@ function findSources(calls, types, patterns, sourceLines, language) {
13204
13269
  line: paramLine,
13205
13270
  confidence: param.type ? 0.7 : 0.5,
13206
13271
  // Lower confidence for untyped params
13207
- in_method: method.name
13272
+ in_method: method.name,
13273
+ // cognium-dev #220 — expose the parameter name to variable-scan flow
13274
+ // detection so uses of the param (including through concat-derived
13275
+ // aliases via `buildJavaTaintedVars`) can be bridged to sinks.
13276
+ //
13277
+ // Java-only: the Python/Rust alias-expansion branches in
13278
+ // taint-propagation-pass.ts use the earliest sourcesWithVar entry
13279
+ // as the anchor for synthetic derived sources. Adding
13280
+ // interprocedural_param to sourcesWithVar in those languages
13281
+ // would cause the anchor to become the low-confidence
13282
+ // interprocedural_param at the method decl line instead of the
13283
+ // real HTTP source, breaking downstream sink-type filters
13284
+ // (regressed #78, #92.1, #105 FP-31, #215 recall).
13285
+ ...language === "java" ? { variable: param.name } : {}
13208
13286
  });
13209
13287
  }
13210
13288
  }
@@ -13668,7 +13746,16 @@ function isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines) {
13668
13746
  return false;
13669
13747
  }
13670
13748
  var TEMPLATE_LITERAL_RECEIVER_RE = /^Template\(\s*(?:"[^"\\]*"|'[^'\\]*')\s*\)$/;
13671
- function isSafeJinjaRenderCall(call, pattern, language) {
13749
+ var JINJA_AUTOESCAPE_TRUE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*True\b/;
13750
+ var JINJA_SELECT_AUTOESCAPE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*select_autoescape\s*\(/;
13751
+ var JINJA_AUTOESCAPE_FALSE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*False\b/;
13752
+ function fileHasSafeJinjaEnvironment(sourceLines) {
13753
+ if (!sourceLines || sourceLines.length === 0) return false;
13754
+ const text = sourceLines.join("\n");
13755
+ if (JINJA_AUTOESCAPE_FALSE_RE.test(text)) return false;
13756
+ return JINJA_AUTOESCAPE_TRUE_RE.test(text) || JINJA_SELECT_AUTOESCAPE_RE.test(text);
13757
+ }
13758
+ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
13672
13759
  if (language !== "python") return false;
13673
13760
  if (pattern.type !== "xss" && pattern.type !== "code_injection") return false;
13674
13761
  const method = call.method_name;
@@ -13682,7 +13769,8 @@ function isSafeJinjaRenderCall(call, pattern, language) {
13682
13769
  }
13683
13770
  if (method === "render") {
13684
13771
  const receiver = (call.receiver ?? "").trim();
13685
- return TEMPLATE_LITERAL_RECEIVER_RE.test(receiver);
13772
+ if (TEMPLATE_LITERAL_RECEIVER_RE.test(receiver)) return true;
13773
+ if (fileHasSafeJinjaEnvironment(sourceLines)) return true;
13686
13774
  }
13687
13775
  return false;
13688
13776
  }
@@ -13730,7 +13818,7 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13730
13818
  if (isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines)) {
13731
13819
  continue;
13732
13820
  }
13733
- if (isSafeJinjaRenderCall(call, pattern, language)) {
13821
+ if (isSafeJinjaRenderCall(call, pattern, language, sourceLines)) {
13734
13822
  continue;
13735
13823
  }
13736
13824
  const location = formatCallLocation(call);
@@ -13738,6 +13826,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13738
13826
  const confidence = calculateSinkConfidence(call, pattern);
13739
13827
  const existing = sinkMap.get(key);
13740
13828
  if (!existing || confidence > existing.confidence) {
13829
+ const receiverType = call.receiver_type;
13830
+ const simpleClass = receiverType ? receiverType.split(".").pop() || void 0 : void 0;
13741
13831
  sinkMap.set(key, {
13742
13832
  type: pattern.type,
13743
13833
  cwe: pattern.cwe,
@@ -13745,7 +13835,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13745
13835
  line: call.location.line,
13746
13836
  confidence,
13747
13837
  method: call.method_name,
13748
- argPositions: pattern.arg_positions
13838
+ argPositions: pattern.arg_positions,
13839
+ class: simpleClass
13749
13840
  });
13750
13841
  }
13751
13842
  }
@@ -14700,12 +14791,20 @@ function canSourceReachSink(sourceType, sinkType) {
14700
14791
  // flow detector silently dropped `http_* → trust_boundary` co-located
14701
14792
  // flows like `req.getSession().setAttribute("u", req.getParameter("u"))`
14702
14793
  // (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"],
14794
+ // deserialization added Sprint 93 (#189): SnakeYAML/Jackson/etc. sinks
14795
+ // like `new Yaml().load(req.getParameter("y"))` are real RCE gadget chains
14796
+ // (CWE-502). The reach map previously restricted deserialization to
14797
+ // http_body so http_param/http_query request-derived values feeding
14798
+ // Yaml.load, ObjectInputStream ctor, XMLDecoder ctor, etc. silently
14799
+ // dropped their inline-colocation flow. Typed-overload FPs are gated by
14800
+ // the sink pattern's `safe_if_class_literal_at` flag (Jackson readValue,
14801
+ // Yaml.loadAs, Gson.fromJson) so the wider reach does not regress.
14802
+ 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
14803
  http_body: ["sql_injection", "command_injection", "deserialization", "xxe", "xss", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment", "open_redirect", "trust_boundary"],
14705
14804
  http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect", "trust_boundary"],
14706
14805
  http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect", "trust_boundary"],
14707
14806
  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"],
14807
+ http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect", "trust_boundary", "deserialization"],
14709
14808
  // ssrf added Sprint 57 #200: bash CGI/webhook handlers and scripts that
14710
14809
  // take a URL on stdin or as a positional CLI arg (`curl "$1"`,
14711
14810
  // `wget "$(read line)"`) and curl/wget it server-side are textbook SSRF
@@ -14727,6 +14826,15 @@ function canSourceReachSink(sourceType, sinkType) {
14727
14826
  const validSinks = sourceToSinkMapping[sourceType];
14728
14827
  return validSinks ? validSinks.includes(sinkType) : false;
14729
14828
  }
14829
+ function sourceSemanticsAllowed(source, sinkType) {
14830
+ if (source.constant === true) {
14831
+ return false;
14832
+ }
14833
+ if (source.spi === true) {
14834
+ return sinkType === "code_injection";
14835
+ }
14836
+ return true;
14837
+ }
14730
14838
 
14731
14839
  // src/analysis/findings-instrumentation.ts
14732
14840
  var instrumentEnabled = false;
@@ -24649,6 +24757,65 @@ function buildRustTaintedVars(sourceCode, seedVars) {
24649
24757
  }
24650
24758
  return derived;
24651
24759
  }
24760
+ function buildJavaTaintedVars(sourceCode, seedVars) {
24761
+ const derived = /* @__PURE__ */ new Map();
24762
+ const knownTainted = new Set(seedVars);
24763
+ const lines = sourceCode.split("\n");
24764
+ const declRe = /^\s*(?:public|private|protected|static|final|volatile|transient|\s)*\s*(?:[A-Za-z_][\w.]*(?:\s*<[^>]*>)?(?:\s*\[\s*\])*)\s+([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
24765
+ const assignRe = /^\s*([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
24766
+ const JAVA_KEYWORDS = /* @__PURE__ */ new Set([
24767
+ "if",
24768
+ "else",
24769
+ "while",
24770
+ "for",
24771
+ "do",
24772
+ "switch",
24773
+ "case",
24774
+ "return",
24775
+ "throw",
24776
+ "try",
24777
+ "catch",
24778
+ "finally",
24779
+ "new",
24780
+ "this",
24781
+ "super",
24782
+ "break",
24783
+ "continue",
24784
+ "default",
24785
+ "class",
24786
+ "interface",
24787
+ "enum"
24788
+ ]);
24789
+ let changed = true;
24790
+ let guard = 0;
24791
+ while (changed && guard < lines.length + 2) {
24792
+ changed = false;
24793
+ guard++;
24794
+ for (let i2 = 0; i2 < lines.length; i2++) {
24795
+ const line = lines[i2];
24796
+ const trimmed = line.trimStart();
24797
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
24798
+ const declMatch = declRe.exec(line);
24799
+ const assignMatch = !declMatch ? assignRe.exec(line) : null;
24800
+ const m = declMatch ?? assignMatch;
24801
+ if (!m) continue;
24802
+ const lhs = m[1];
24803
+ const rhs = m[2];
24804
+ if (JAVA_KEYWORDS.has(lhs)) continue;
24805
+ if (knownTainted.has(lhs)) continue;
24806
+ const escaped = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24807
+ const ref = [...knownTainted].some(
24808
+ (v) => new RegExp(`(?<![\\p{L}\\p{N}_])${escaped(v)}(?![\\p{L}\\p{N}_])`, "u").test(rhs)
24809
+ );
24810
+ if (ref) {
24811
+ derived.set(lhs, i2 + 1);
24812
+ knownTainted.add(lhs);
24813
+ changed = true;
24814
+ }
24815
+ }
24816
+ }
24817
+ return derived;
24818
+ }
24652
24819
  var BASH_UNTRUSTED_ENV_PATTERNS = [
24653
24820
  /^USER_INPUT$/i,
24654
24821
  /^QUERY_STRING$/i,
@@ -25006,7 +25173,7 @@ function findBashRealpathPrefixGuardSanitizers(code) {
25006
25173
  const caseOpen = /^\s*case\s+"?\$\{?\w+\}?"?\s+in\b/;
25007
25174
  const esacClose = /^\s*esac\b/;
25008
25175
  const armOpener = /^\s*([^)\s][^)]*?)\)/;
25009
- const prefixArm = /^(?:"\$\{?\w+\}?"|"[^"]*"|\/[\w\-./]+|\$\{?\w+\}?|[\w\-./]+)(?:\/|\*)/;
25176
+ const prefixArm = /^(?:"\$\{?\w+\}?"|"[^"]*"|\/[\w\-./]+|\$\{?\w+\}?|https?:\/\/[\w\-.]+|[\w\-./]+)(?:\/|\*)/;
25010
25177
  const catchAllArm = /^(?:\*|\\\*)$/;
25011
25178
  let i2 = 0;
25012
25179
  while (i2 < lines.length) {
@@ -25831,9 +25998,11 @@ function findJavaArgvFormExecSanitizers(code) {
25831
25998
  const lines = code.split("\n");
25832
25999
  const argvExecRe = /\.\s*exec\s*\(\s*new\s+String\s*\[\s*\]\s*\{/;
25833
26000
  const argvPbRe = /\bnew\s+ProcessBuilder\s*\(\s*new\s+String\s*\[\s*\]\s*\{/;
26001
+ const shellInStringRe = /new\s+String\s*\[\s*\]\s*\{\s*"(?:\/(?:usr\/)?bin\/(?:sh|bash|zsh|ksh|dash)|(?:sh|bash|zsh|ksh|dash)|cmd(?:\.exe)?|powershell(?:\.exe)?|pwsh)"\s*,\s*"(?:-c|\/c|-Command|-command)"/i;
25834
26002
  for (let i2 = 0; i2 < lines.length; i2++) {
25835
26003
  const text = lines[i2];
25836
26004
  if (!argvExecRe.test(text) && !argvPbRe.test(text)) continue;
26005
+ if (shellInStringRe.test(text)) continue;
25837
26006
  sanitizers.push({
25838
26007
  type: "java_argv_form_exec",
25839
26008
  method: "exec",
@@ -27770,6 +27939,22 @@ function findPythonMongoengineWhereNosqlInjectionFindings(code, file) {
27770
27939
  }
27771
27940
  return findings;
27772
27941
  }
27942
+ function hasHostAllowlistBeforeSink(taintedVars, lines, sinkLineIdx) {
27943
+ for (let i2 = 0; i2 < sinkLineIdx; i2++) {
27944
+ const t = lines[i2];
27945
+ for (const v of taintedVars) {
27946
+ const containsRe = new RegExp(
27947
+ `\\.\\s*contains\\s*\\(\\s*${v}\\s*\\.\\s*getHost\\s*\\(\\s*\\)\\s*\\)`
27948
+ );
27949
+ if (containsRe.test(t)) return true;
27950
+ const equalsRe = new RegExp(
27951
+ `\\b${v}\\s*\\.\\s*getHost\\s*\\(\\s*\\)\\s*\\.\\s*equals(?:IgnoreCase)?\\s*\\(\\s*"[^"]+"\\s*\\)`
27952
+ );
27953
+ if (equalsRe.test(t)) return true;
27954
+ }
27955
+ }
27956
+ return false;
27957
+ }
27773
27958
  function findJavaUrlOpenStreamSsrfFindings(code, file) {
27774
27959
  const findings = [];
27775
27960
  if (typeof code !== "string" || code.length === 0) return findings;
@@ -27825,6 +28010,7 @@ function findJavaUrlOpenStreamSsrfFindings(code, file) {
27825
28010
  }
27826
28011
  }
27827
28012
  if (!tainted) continue;
28013
+ if (hasHostAllowlistBeforeSink(taintedVars, lines, i2)) continue;
27828
28014
  const key = `${i2 + 1}:${op}`;
27829
28015
  if (seen.has(key)) continue;
27830
28016
  seen.add(key);
@@ -28967,6 +29153,80 @@ function findJsTemplateInjectionSstiFindings(code, file) {
28967
29153
  return findings;
28968
29154
  }
28969
29155
 
29156
+ // src/analysis/passes/source-semantics-pass.ts
29157
+ var DEMO_PATH_RE = /(?:^|\/)(?:demo|example|examples|samples|integration-tests|integration_tests)(?:\/|$)/i;
29158
+ 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*$/;
29159
+ var STATIC_FINAL_RE = /^\s*(?:public\s+|private\s+|protected\s+)?static\s+final\s+/;
29160
+ var ENUM_CONST_REF_RE = /=\s*[A-Z][A-Za-z0-9_]*\.[A-Z][A-Z0-9_]*\s*;?\s*$/;
29161
+ function isConstantSource(code) {
29162
+ if (!code) return false;
29163
+ if (CONST_STRING_ASSIGN_RE.test(code)) return true;
29164
+ if (STATIC_FINAL_RE.test(code)) {
29165
+ const rhs = code.split("=").slice(1).join("=").trim();
29166
+ if (rhs.length === 0) return false;
29167
+ if (/^"[^"]*"\s*;?\s*$/.test(rhs)) return true;
29168
+ if (/^-?\d+(?:\.\d+)?[fFdDlL]?\s*;?\s*$/.test(rhs)) return true;
29169
+ if (/^(?:true|false)\s*;?\s*$/.test(rhs)) return true;
29170
+ if (/^[A-Za-z_][\w.]*\s*;?\s*$/.test(rhs)) return true;
29171
+ return false;
29172
+ }
29173
+ if (ENUM_CONST_REF_RE.test(code)) return true;
29174
+ return false;
29175
+ }
29176
+ var SERVICE_LOADER_RE = /\bServiceLoader\.(?:load|loadInstalled|stream)\s*\(/;
29177
+ var CLASS_FOR_NAME_RE = /\bClass\.forName\s*\(/;
29178
+ var META_INF_SERVICES_RE = /getResources?\s*\(\s*"META-INF\/services\//;
29179
+ var SPI_WINDOW = 30;
29180
+ function isSpiSource(source, lines) {
29181
+ const code = source.code;
29182
+ if (!code) return false;
29183
+ if (SERVICE_LOADER_RE.test(code)) return true;
29184
+ if (CLASS_FOR_NAME_RE.test(code)) {
29185
+ const start2 = Math.max(0, source.line - 1 - SPI_WINDOW);
29186
+ const end = Math.min(lines.length, source.line - 1 + SPI_WINDOW + 1);
29187
+ for (let i2 = start2; i2 < end; i2++) {
29188
+ if (META_INF_SERVICES_RE.test(lines[i2])) return true;
29189
+ }
29190
+ }
29191
+ return false;
29192
+ }
29193
+ function isDemoPathFile(file) {
29194
+ if (!file) return false;
29195
+ return DEMO_PATH_RE.test(file);
29196
+ }
29197
+ var SourceSemanticsPass = class {
29198
+ name = "source-semantics";
29199
+ category = "security";
29200
+ run(ctx) {
29201
+ const { graph, code } = ctx;
29202
+ const sources = graph.ir.taint.sources;
29203
+ if (sources.length === 0) {
29204
+ return { constantCount: 0, spiCount: 0, demoPathCount: 0 };
29205
+ }
29206
+ const file = graph.ir.meta.file;
29207
+ const demoPath = isDemoPathFile(file);
29208
+ const lines = code.split("\n");
29209
+ let constantCount = 0;
29210
+ let spiCount = 0;
29211
+ let demoPathCount = 0;
29212
+ for (const source of sources) {
29213
+ if (isConstantSource(source.code)) {
29214
+ source.constant = true;
29215
+ constantCount++;
29216
+ }
29217
+ if (isSpiSource(source, lines)) {
29218
+ source.spi = true;
29219
+ spiCount++;
29220
+ }
29221
+ if (demoPath) {
29222
+ source.demoPath = true;
29223
+ demoPathCount++;
29224
+ }
29225
+ }
29226
+ return { constantCount, spiCount, demoPathCount };
29227
+ }
29228
+ };
29229
+
28970
29230
  // src/analysis/passes/sink-filter-pass.ts
28971
29231
  var JS_XSS_SANITIZERS = [
28972
29232
  /\bDOMPurify\.sanitize\s*\(/,
@@ -29514,7 +29774,7 @@ var SinkFilterPass = class {
29514
29774
  const sourceLines = ctx.code.split("\n");
29515
29775
  filtered = filtered.filter((sink) => {
29516
29776
  if (sink.type !== "command_injection") return true;
29517
- if (sink.method !== "ProcessBuilder") return true;
29777
+ if (sink.method !== "ProcessBuilder" && sink.method !== "start") return true;
29518
29778
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
29519
29779
  if (!/\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText)) return true;
29520
29780
  if (PROCESS_BUILDER_ARGV_FORM_RE.test(sinkLineText)) return false;
@@ -30258,6 +30518,50 @@ function filterSanitizedSinks(sinks, sanitizers, calls) {
30258
30518
  });
30259
30519
  }
30260
30520
 
30521
+ // src/analysis/passes/sink-semantics-pass.ts
30522
+ function buildRegistry(entries) {
30523
+ const registry = /* @__PURE__ */ new Map();
30524
+ for (const entry of entries) {
30525
+ const existing = registry.get(entry.signature);
30526
+ if (existing) {
30527
+ for (const t of entry.overrides) existing.add(t);
30528
+ } else {
30529
+ registry.set(entry.signature, new Set(entry.overrides));
30530
+ }
30531
+ }
30532
+ return registry;
30533
+ }
30534
+ var SinkSemanticsPass = class {
30535
+ name = "sink-semantics";
30536
+ category = "security";
30537
+ run(ctx) {
30538
+ const { graph, config } = ctx;
30539
+ const entries = config.sinkSemantics ?? [];
30540
+ if (entries.length === 0) {
30541
+ return { droppedCount: 0, registrySize: 0 };
30542
+ }
30543
+ const registry = buildRegistry(entries);
30544
+ const sinks = graph.ir.taint.sinks;
30545
+ let droppedCount = 0;
30546
+ const kept = sinks.filter((sink) => {
30547
+ if (!sink.class || !sink.method) return true;
30548
+ const signature = `${sink.class}#${sink.method}`;
30549
+ const overrides = registry.get(signature);
30550
+ if (!overrides) return true;
30551
+ if (overrides.has(sink.type)) {
30552
+ droppedCount++;
30553
+ return false;
30554
+ }
30555
+ return true;
30556
+ });
30557
+ if (droppedCount > 0) {
30558
+ sinks.length = 0;
30559
+ sinks.push(...kept);
30560
+ }
30561
+ return { droppedCount, registrySize: registry.size };
30562
+ }
30563
+ };
30564
+
30261
30565
  // src/analysis/passes/taint-propagation-pass.ts
30262
30566
  var TaintPropagationPass = class {
30263
30567
  name = "taint-propagation";
@@ -30887,6 +31191,25 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
30887
31191
  }
30888
31192
  }
30889
31193
  }
31194
+ if (language === "java" && typeof code === "string" && sourcesWithVar.length > 0) {
31195
+ const seedVars = new Set(sourcesWithVar.map((s) => s.variable));
31196
+ const derived = buildJavaTaintedVars(code, seedVars);
31197
+ if (derived.size > 0) {
31198
+ let anchor = sourcesWithVar[0];
31199
+ for (const s of sourcesWithVar) {
31200
+ if (s.line < anchor.line) anchor = s;
31201
+ }
31202
+ const existingVars = new Set(sourcesWithVar.map((s) => s.variable));
31203
+ for (const [varName] of derived) {
31204
+ if (!varName || existingVars.has(varName)) continue;
31205
+ sourcesWithVar.push({
31206
+ ...anchor,
31207
+ variable: varName
31208
+ });
31209
+ existingVars.add(varName);
31210
+ }
31211
+ }
31212
+ }
30890
31213
  const reCache = /* @__PURE__ */ new Map();
30891
31214
  for (const s of sourcesWithVar) {
30892
31215
  if (reCache.has(s.variable)) continue;
@@ -30916,6 +31239,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
30916
31239
  }
30917
31240
  const re = reCache.get(source.variable);
30918
31241
  if (!re || !re.test(expr)) continue;
31242
+ if (!sourceSemanticsAllowed(source, sink.type)) continue;
30919
31243
  if (flows.some(
30920
31244
  (f) => f.source_line === source.line && f.sink_line === sink.line && f.sink_type === sink.type
30921
31245
  )) continue;
@@ -30944,7 +31268,6 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
30944
31268
  }
30945
31269
  const sourcesByLine = /* @__PURE__ */ new Map();
30946
31270
  for (const s of sources) {
30947
- if (s.variable && s.variable.length > 0) continue;
30948
31271
  const arr = sourcesByLine.get(s.line) ?? [];
30949
31272
  arr.push(s);
30950
31273
  sourcesByLine.set(s.line, arr);
@@ -30955,6 +31278,19 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
30955
31278
  if (!colocSources || colocSources.length === 0) continue;
30956
31279
  for (const source of colocSources) {
30957
31280
  if (!canSourceReachSink(source.type, sink.type)) continue;
31281
+ if (!sourceSemanticsAllowed(source, sink.type)) continue;
31282
+ const sourceVar = source.variable;
31283
+ if (sourceVar && sourceVar.length > 0) {
31284
+ const sinkCode = sink.code;
31285
+ if (!sinkCode) {
31286
+ continue;
31287
+ }
31288
+ const assignMatch = sinkCode.match(/^\s*(?:[A-Za-z_][\w.<>[\]\s,?]*\s+)?[A-Za-z_]\w*\s*=(?!=)\s*/);
31289
+ const rhs = assignMatch ? sinkCode.slice(assignMatch[0].length) : sinkCode;
31290
+ if (new RegExp(`\\b${sourceVar}\\b`).test(rhs)) {
31291
+ continue;
31292
+ }
31293
+ }
30958
31294
  if (source.type === "file_input" && sink.type === "path_traversal" && sink.method && source.location.includes(`${sink.method}(`)) {
30959
31295
  continue;
30960
31296
  }
@@ -35059,6 +35395,13 @@ function isProtocolMandatedCryptoFile(file, code) {
35059
35395
  }
35060
35396
 
35061
35397
  // src/analysis/passes/scan-secrets-pass.ts
35398
+ function applyDemoDowngrade(demoPath, severity, level) {
35399
+ if (!demoPath) return { severity, level };
35400
+ if (severity === "high") {
35401
+ return { severity: "low", level: "note" };
35402
+ }
35403
+ return { severity, level };
35404
+ }
35062
35405
  var TEST_PATH_RE3 = /(?:^|[\\/])(?:test|tests|spec|specs|__tests?__|__mocks?__|fixtures?|testdata)(?:[\\/]|$)/i;
35063
35406
  var TEST_FILENAME_RE = /(?:\.(?:test|spec)\.[cm]?[jt]sx?|_test\.go|_test\.py|Test\.java|Tests\.java)$/i;
35064
35407
  function isTestFile(file) {
@@ -35374,6 +35717,7 @@ var ScanSecretsPass = class {
35374
35717
  if (isTestFile(file) || isGeneratedFile(file)) {
35375
35718
  return { providerFindings: 0, entropyFindings: 0 };
35376
35719
  }
35720
+ const demoPath = DEMO_PATH_RE.test(file);
35377
35721
  const lines = ctx.code.split("\n");
35378
35722
  const prior = ctx.getFindings?.() ?? [];
35379
35723
  const seen = /* @__PURE__ */ new Set();
@@ -35400,14 +35744,15 @@ var ScanSecretsPass = class {
35400
35744
  const key = `${lineNum}:hardcoded-credential`;
35401
35745
  if (seen.has(key)) continue;
35402
35746
  seen.add(key);
35747
+ const dg = applyDemoDowngrade(demoPath, pattern.severity, pattern.level);
35403
35748
  ctx.addFinding({
35404
35749
  id: `hardcoded-credential-${file}-${lineNum}`,
35405
35750
  pass: this.name,
35406
35751
  category: this.category,
35407
35752
  rule_id: "hardcoded-credential",
35408
35753
  cwe: "CWE-798",
35409
- severity: pattern.severity,
35410
- level: pattern.level,
35754
+ severity: dg.severity,
35755
+ level: dg.level,
35411
35756
  message: `Hardcoded credential: ${pattern.name} detected`,
35412
35757
  file,
35413
35758
  line: lineNum,
@@ -35427,14 +35772,15 @@ var ScanSecretsPass = class {
35427
35772
  const key = `${lineNum}:hardcoded-credential`;
35428
35773
  if (seen.has(key)) continue;
35429
35774
  seen.add(key);
35775
+ const dg = applyDemoDowngrade(demoPath, "high", "error");
35430
35776
  ctx.addFinding({
35431
35777
  id: `hardcoded-credential-${file}-${lineNum}`,
35432
35778
  pass: this.name,
35433
35779
  category: this.category,
35434
35780
  rule_id: "hardcoded-credential",
35435
35781
  cwe: "CWE-798",
35436
- severity: "high",
35437
- level: "error",
35782
+ severity: dg.severity,
35783
+ level: dg.level,
35438
35784
  message: `Hardcoded credential: \`${hit.name}\` assigned a literal value`,
35439
35785
  file,
35440
35786
  line: lineNum,
@@ -39748,7 +40094,9 @@ async function analyze(code, filePath, language, options = {}) {
39748
40094
  pipeline.add(new TaintMatcherPass());
39749
40095
  pipeline.add(new ConstantPropagationPass(tree));
39750
40096
  pipeline.add(new LanguageSourcesPass());
40097
+ if (!disabledPasses.has("source-semantics")) pipeline.add(new SourceSemanticsPass());
39751
40098
  pipeline.add(new SinkFilterPass());
40099
+ if (!disabledPasses.has("sink-semantics")) pipeline.add(new SinkSemanticsPass());
39752
40100
  pipeline.add(new TaintPropagationPass());
39753
40101
  pipeline.add(new InterproceduralPass({
39754
40102
  enableEntryPointGate: options.enableEntryPointGate ?? true