cognium-dev 3.141.0 → 3.146.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 (2) hide show
  1. package/dist/cli.js +531 -20
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -5668,6 +5668,12 @@ function resolveReceiverType(receiver, context) {
5668
5668
  }
5669
5669
  if (receiver === "super")
5670
5670
  return { simpleName: null, fqn: null };
5671
+ const ctorMatch = receiver.match(/^new\s+([A-Za-z_$][\w$.]*)\s*[<(]/);
5672
+ if (ctorMatch) {
5673
+ const ctorClass = ctorMatch[1];
5674
+ const simple = ctorClass.includes(".") ? ctorClass.substring(ctorClass.lastIndexOf(".") + 1) : ctorClass;
5675
+ return resolveFqn(simple, context);
5676
+ }
5671
5677
  const declaredType = context.localVarTypes.get(receiver) ?? context.paramTypes.get(receiver) ?? context.fieldTypes.get(receiver);
5672
5678
  if (declaredType) {
5673
5679
  return resolveFqn(stripGenerics(declaredType), context);
@@ -10732,6 +10738,8 @@ var DEFAULT_SINKS = [
10732
10738
  { method: "sendError", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
10733
10739
  { method: "setHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10734
10740
  { method: "addHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10741
+ { method: "Cookie", class: "constructor", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0, 1] },
10742
+ { method: "addCookie", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0] },
10735
10743
  { method: "setContentType", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [0] },
10736
10744
  { method: "setAttribute", class: "PageContext", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
10737
10745
  { method: "addAttribute", class: "Model", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
@@ -11668,11 +11676,62 @@ var DEFAULT_SANITIZERS = [
11668
11676
  { method: "UUID", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
11669
11677
  { method: "Decimal", class: "decimal", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] }
11670
11678
  ];
11679
+ var DEFAULT_SINK_SEMANTICS = [
11680
+ {
11681
+ signature: "Jedis#executeCommand",
11682
+ real_class: "db_protocol",
11683
+ overrides: ["command_injection", "code_injection"],
11684
+ note: "Redis wire-protocol serialization, not OS exec"
11685
+ },
11686
+ {
11687
+ signature: "Connection#executeCommand",
11688
+ real_class: "db_protocol",
11689
+ overrides: ["command_injection", "code_injection"],
11690
+ note: "Jedis abstract Connection base"
11691
+ },
11692
+ {
11693
+ signature: "JedisCluster#executeCommand",
11694
+ real_class: "db_protocol",
11695
+ overrides: ["command_injection", "code_injection"],
11696
+ note: "Jedis cluster client"
11697
+ },
11698
+ {
11699
+ signature: "Func1#exec",
11700
+ real_class: "functional_dispatch",
11701
+ overrides: ["command_injection", "code_injection"],
11702
+ note: "RxJava functional dispatch, not OS exec"
11703
+ },
11704
+ {
11705
+ signature: "Action0#call",
11706
+ real_class: "functional_dispatch",
11707
+ overrides: ["command_injection"],
11708
+ note: "RxJava Action0 dispatch"
11709
+ },
11710
+ {
11711
+ signature: "Action1#call",
11712
+ real_class: "functional_dispatch",
11713
+ overrides: ["command_injection"],
11714
+ note: "RxJava Action1 dispatch"
11715
+ },
11716
+ {
11717
+ signature: "Unsafe#defineAnonymousClass",
11718
+ real_class: "jdk_internal",
11719
+ overrides: ["code_injection"],
11720
+ note: "sun.misc.Unsafe JDK-internal reflective bridge"
11721
+ },
11722
+ {
11723
+ signature: "MethodHandle#invokeExact",
11724
+ real_class: "jdk_internal",
11725
+ overrides: ["code_injection"],
11726
+ note: "java.lang.invoke.MethodHandle — JDK-internal"
11727
+ }
11728
+ ];
11671
11729
  function getDefaultConfig() {
11672
11730
  return {
11673
11731
  sources: DEFAULT_SOURCES,
11674
11732
  sinks: DEFAULT_SINKS,
11675
- sanitizers: DEFAULT_SANITIZERS
11733
+ sanitizers: DEFAULT_SANITIZERS,
11734
+ sinkSemantics: DEFAULT_SINK_SEMANTICS
11676
11735
  };
11677
11736
  }
11678
11737
  var DEFAULT_HEADER_RULES = [
@@ -12001,7 +12060,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
12001
12060
  severity: "medium",
12002
12061
  line: paramLine,
12003
12062
  confidence: param.type ? 0.7 : 0.5,
12004
- in_method: method.name
12063
+ in_method: method.name,
12064
+ ...language === "java" ? { variable: param.name } : {}
12005
12065
  });
12006
12066
  }
12007
12067
  }
@@ -12499,7 +12559,19 @@ function isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines) {
12499
12559
  return false;
12500
12560
  }
12501
12561
  var TEMPLATE_LITERAL_RECEIVER_RE = /^Template\(\s*(?:"[^"\\]*"|'[^'\\]*')\s*\)$/;
12502
- function isSafeJinjaRenderCall(call, pattern, language) {
12562
+ var JINJA_AUTOESCAPE_TRUE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*True\b/;
12563
+ var JINJA_SELECT_AUTOESCAPE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*select_autoescape\s*\(/;
12564
+ var JINJA_AUTOESCAPE_FALSE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*False\b/;
12565
+ function fileHasSafeJinjaEnvironment(sourceLines) {
12566
+ if (!sourceLines || sourceLines.length === 0)
12567
+ return false;
12568
+ const text = sourceLines.join(`
12569
+ `);
12570
+ if (JINJA_AUTOESCAPE_FALSE_RE.test(text))
12571
+ return false;
12572
+ return JINJA_AUTOESCAPE_TRUE_RE.test(text) || JINJA_SELECT_AUTOESCAPE_RE.test(text);
12573
+ }
12574
+ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
12503
12575
  if (language !== "python")
12504
12576
  return false;
12505
12577
  if (pattern.type !== "xss" && pattern.type !== "code_injection")
@@ -12515,7 +12587,10 @@ function isSafeJinjaRenderCall(call, pattern, language) {
12515
12587
  }
12516
12588
  if (method === "render") {
12517
12589
  const receiver = (call.receiver ?? "").trim();
12518
- return TEMPLATE_LITERAL_RECEIVER_RE.test(receiver);
12590
+ if (TEMPLATE_LITERAL_RECEIVER_RE.test(receiver))
12591
+ return true;
12592
+ if (fileHasSafeJinjaEnvironment(sourceLines))
12593
+ return true;
12519
12594
  }
12520
12595
  return false;
12521
12596
  }
@@ -12563,7 +12638,7 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12563
12638
  if (isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines)) {
12564
12639
  continue;
12565
12640
  }
12566
- if (isSafeJinjaRenderCall(call, pattern, language)) {
12641
+ if (isSafeJinjaRenderCall(call, pattern, language, sourceLines)) {
12567
12642
  continue;
12568
12643
  }
12569
12644
  const location = formatCallLocation(call);
@@ -12571,6 +12646,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12571
12646
  const confidence = calculateSinkConfidence(call, pattern);
12572
12647
  const existing = sinkMap.get(key);
12573
12648
  if (!existing || confidence > existing.confidence) {
12649
+ const receiverType = call.receiver_type;
12650
+ const simpleClass = receiverType ? receiverType.split(".").pop() || undefined : undefined;
12574
12651
  sinkMap.set(key, {
12575
12652
  type: pattern.type,
12576
12653
  cwe: pattern.cwe,
@@ -12578,7 +12655,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12578
12655
  line: call.location.line,
12579
12656
  confidence,
12580
12657
  method: call.method_name,
12581
- argPositions: pattern.arg_positions
12658
+ argPositions: pattern.arg_positions,
12659
+ class: simpleClass
12582
12660
  });
12583
12661
  }
12584
12662
  }
@@ -13478,12 +13556,12 @@ function formatCallCode(call) {
13478
13556
  // ../circle-ir/dist/analysis/findings.js
13479
13557
  function canSourceReachSink(sourceType, sinkType) {
13480
13558
  const sourceToSinkMapping = {
13481
- 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"],
13559
+ 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"],
13482
13560
  http_body: ["sql_injection", "command_injection", "deserialization", "xxe", "xss", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment", "open_redirect", "trust_boundary"],
13483
13561
  http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect", "trust_boundary"],
13484
13562
  http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect", "trust_boundary"],
13485
13563
  http_path: ["path_traversal", "sql_injection", "ssrf", "mybatis_mapper_call", "open_redirect", "trust_boundary"],
13486
- http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect", "trust_boundary"],
13564
+ http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect", "trust_boundary", "deserialization"],
13487
13565
  io_input: ["command_injection", "path_traversal", "deserialization", "xxe", "code_injection", "xss", "ssrf"],
13488
13566
  env_input: ["command_injection", "path_traversal"],
13489
13567
  db_input: ["xss", "sql_injection"],
@@ -13496,6 +13574,15 @@ function canSourceReachSink(sourceType, sinkType) {
13496
13574
  const validSinks = sourceToSinkMapping[sourceType];
13497
13575
  return validSinks ? validSinks.includes(sinkType) : false;
13498
13576
  }
13577
+ function sourceSemanticsAllowed(source, sinkType) {
13578
+ if (source.constant === true) {
13579
+ return false;
13580
+ }
13581
+ if (source.spi === true) {
13582
+ return sinkType === "code_injection";
13583
+ }
13584
+ return true;
13585
+ }
13499
13586
 
13500
13587
  // ../circle-ir/dist/analysis/findings-instrumentation.js
13501
13588
  var instrumentEnabled = false;
@@ -22781,6 +22868,8 @@ class LanguageSourcesPass {
22781
22868
  additionalSanitizers.push(...findJsSsrfAllowlistGuardSanitizers(code));
22782
22869
  additionalSanitizers.push(...findJsArgvFormExecSanitizers(code));
22783
22870
  additionalSanitizers.push(...findJsParameterizedSqlSanitizers(code));
22871
+ additionalSanitizers.push(...findJsPathResolveStartsWithGuardSanitizers(code));
22872
+ additionalSanitizers.push(...findJsCommandAllowlistGuardSanitizers(code));
22784
22873
  for (const finding of findJsPatternFindings(code, graph.ir.meta.file)) {
22785
22874
  ctx.addFinding(finding);
22786
22875
  }
@@ -23602,6 +23691,68 @@ function buildRustTaintedVars(sourceCode, seedVars) {
23602
23691
  }
23603
23692
  return derived;
23604
23693
  }
23694
+ function buildJavaTaintedVars(sourceCode, seedVars) {
23695
+ const derived = new Map;
23696
+ const knownTainted = new Set(seedVars);
23697
+ const lines = sourceCode.split(`
23698
+ `);
23699
+ 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*$/;
23700
+ const assignRe = /^\s*([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
23701
+ const JAVA_KEYWORDS = new Set([
23702
+ "if",
23703
+ "else",
23704
+ "while",
23705
+ "for",
23706
+ "do",
23707
+ "switch",
23708
+ "case",
23709
+ "return",
23710
+ "throw",
23711
+ "try",
23712
+ "catch",
23713
+ "finally",
23714
+ "new",
23715
+ "this",
23716
+ "super",
23717
+ "break",
23718
+ "continue",
23719
+ "default",
23720
+ "class",
23721
+ "interface",
23722
+ "enum"
23723
+ ]);
23724
+ let changed = true;
23725
+ let guard = 0;
23726
+ while (changed && guard < lines.length + 2) {
23727
+ changed = false;
23728
+ guard++;
23729
+ for (let i2 = 0;i2 < lines.length; i2++) {
23730
+ const line = lines[i2];
23731
+ const trimmed = line.trimStart();
23732
+ if (trimmed.startsWith("//") || trimmed.startsWith("*"))
23733
+ continue;
23734
+ const declMatch = declRe.exec(line);
23735
+ const assignMatch = !declMatch ? assignRe.exec(line) : null;
23736
+ const m = declMatch ?? assignMatch;
23737
+ if (!m)
23738
+ continue;
23739
+ const lhs = m[1];
23740
+ const rhs = m[2];
23741
+ if (JAVA_KEYWORDS.has(lhs))
23742
+ continue;
23743
+ if (knownTainted.has(lhs))
23744
+ continue;
23745
+ const escaped = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23746
+ const ref = [...knownTainted].some((v) => new RegExp(`(?<![\\p{L}\\p{N}_])${escaped(v)}(?![\\p{L}\\p{N}_])`, "u").test(rhs));
23747
+ if (ref) {
23748
+ derived.set(lhs, i2 + 1);
23749
+ knownTainted.add(lhs);
23750
+ changed = true;
23751
+ }
23752
+ }
23753
+ }
23754
+ return derived;
23755
+ }
23605
23756
  var BASH_POSITIONAL_PARAMS = new Set(["1", "2", "3", "4", "5", "6", "7", "8", "9", "@", "*"]);
23606
23757
  var BASH_UNTRUSTED_ENV_PATTERNS = [
23607
23758
  /^USER_INPUT$/i,
@@ -23982,7 +24133,7 @@ function findBashRealpathPrefixGuardSanitizers(code) {
23982
24133
  const caseOpen = /^\s*case\s+"?\$\{?\w+\}?"?\s+in\b/;
23983
24134
  const esacClose = /^\s*esac\b/;
23984
24135
  const armOpener = /^\s*([^)\s][^)]*?)\)/;
23985
- const prefixArm = /^(?:"\$\{?\w+\}?"|"[^"]*"|\/[\w\-./]+|\$\{?\w+\}?|[\w\-./]+)(?:\/|\*)/;
24136
+ const prefixArm = /^(?:"\$\{?\w+\}?"|"[^"]*"|\/[\w\-./]+|\$\{?\w+\}?|https?:\/\/[\w\-.]+|[\w\-./]+)(?:\/|\*)/;
23986
24137
  const catchAllArm = /^(?:\*|\\\*)$/;
23987
24138
  let i2 = 0;
23988
24139
  while (i2 < lines.length) {
@@ -24838,6 +24989,126 @@ function findJsParameterizedSqlSanitizers(code) {
24838
24989
  }
24839
24990
  return sanitizers;
24840
24991
  }
24992
+ function findJsPathResolveStartsWithGuardSanitizers(code) {
24993
+ const sanitizers = [];
24994
+ const lines = code.split(`
24995
+ `);
24996
+ const resolveDeclRe = /\b(?:const|let|var)\s+([A-Za-z_]\w*)\s*=\s*(?:(?:node:)?path|nodePath|Path)\s*\.\s*(?:resolve|join)\s*\(\s*([A-Za-z_]\w*(?:\.\w+)*)\s*,/;
24997
+ const startsWithGuardRe = (varName, rootName) => new RegExp(`if\\s*\\(\\s*!\\s*${varName}\\s*\\.\\s*startsWith\\s*\\(\\s*${rootName}\\b`);
24998
+ const terminatorRe = /\b(?:throw|return|res\s*\.\s*status\s*\([^)]*\)\s*\.\s*(?:send|end|json))/;
24999
+ const candidates = [];
25000
+ for (let i2 = 0;i2 < lines.length; i2++) {
25001
+ const m = resolveDeclRe.exec(lines[i2]);
25002
+ if (!m)
25003
+ continue;
25004
+ candidates.push({ line: i2 + 1, fullVar: m[1], rootVar: m[2] });
25005
+ }
25006
+ if (candidates.length === 0)
25007
+ return sanitizers;
25008
+ for (const c of candidates) {
25009
+ const guardRe = startsWithGuardRe(c.fullVar, c.rootVar);
25010
+ let guardLine = -1;
25011
+ for (let l = c.line;l < Math.min(lines.length, c.line + 6); l++) {
25012
+ if (!guardRe.test(lines[l]))
25013
+ continue;
25014
+ if (terminatorRe.test(lines[l]) || l + 1 < lines.length && terminatorRe.test(lines[l + 1])) {
25015
+ guardLine = l + 1;
25016
+ break;
25017
+ }
25018
+ }
25019
+ if (guardLine < 0)
25020
+ continue;
25021
+ const varRefRe = new RegExp(`\\b${c.fullVar}\\b`);
25022
+ sanitizers.push({
25023
+ type: "js_path_resolve_startswith_guard",
25024
+ method: "startsWith",
25025
+ line: c.line,
25026
+ sanitizes: ["path_traversal", "external_taint_escape"]
25027
+ });
25028
+ for (let l = c.line;l < lines.length; l++) {
25029
+ if (!varRefRe.test(lines[l]))
25030
+ continue;
25031
+ sanitizers.push({
25032
+ type: "js_path_resolve_startswith_guard",
25033
+ method: "startsWith",
25034
+ line: l + 1,
25035
+ sanitizes: ["path_traversal", "external_taint_escape"]
25036
+ });
25037
+ }
25038
+ }
25039
+ return sanitizers;
25040
+ }
25041
+ function findJsCommandAllowlistGuardSanitizers(code) {
25042
+ const sanitizers = [];
25043
+ const lines = code.split(`
25044
+ `);
25045
+ const shoutyCase = /^[A-Z][A-Z0-9_]+$/;
25046
+ const allowlistTokens = /(?:allowed|accepted|whitelist|permitted|valid|approved|cmds|commands|tools)/i;
25047
+ const isAllowlistName = (n) => shoutyCase.test(n) || allowlistTokens.test(n);
25048
+ const guardHas = /\bif\s*\(\s*!\s*([A-Za-z_]\w*)\s*\.\s*(?:has|includes)\s*\(\s*([A-Za-z_]\w*)\s*\)\s*\)/;
25049
+ const guardIndexOf = /\bif\s*\(\s*([A-Za-z_]\w*)\s*\.\s*indexOf\s*\(\s*([A-Za-z_]\w*)\s*\)\s*(?:<\s*0|===\s*-1|==\s*-1)\s*\)/;
25050
+ const terminator = /\b(?:return|throw|res\s*\.\s*status\s*\([^)]*\)\s*\.\s*(?:send|end|json))/;
25051
+ for (let i2 = 0;i2 < lines.length; i2++) {
25052
+ const line = lines[i2];
25053
+ let m = guardHas.exec(line);
25054
+ let allow = null;
25055
+ let guardedVar = null;
25056
+ if (m) {
25057
+ allow = m[1];
25058
+ guardedVar = m[2];
25059
+ } else {
25060
+ m = guardIndexOf.exec(line);
25061
+ if (m) {
25062
+ allow = m[1];
25063
+ guardedVar = m[2];
25064
+ }
25065
+ }
25066
+ if (!allow || !guardedVar)
25067
+ continue;
25068
+ if (!isAllowlistName(allow))
25069
+ continue;
25070
+ let bodyHasTerminator = terminator.test(line);
25071
+ let blockEnd = i2;
25072
+ if (!bodyHasTerminator) {
25073
+ let braceDepth = 0;
25074
+ let started = false;
25075
+ const maxScan = Math.min(lines.length, i2 + 26);
25076
+ for (let j = i2;j < maxScan; j++) {
25077
+ const ln = lines[j];
25078
+ for (const ch of ln) {
25079
+ if (ch === "{") {
25080
+ braceDepth++;
25081
+ started = true;
25082
+ } else if (ch === "}") {
25083
+ braceDepth--;
25084
+ if (started && braceDepth === 0) {
25085
+ blockEnd = j;
25086
+ break;
25087
+ }
25088
+ }
25089
+ }
25090
+ if (started && j > i2 && terminator.test(ln))
25091
+ bodyHasTerminator = true;
25092
+ if (started && braceDepth === 0 && blockEnd !== i2)
25093
+ break;
25094
+ }
25095
+ if (!started || !bodyHasTerminator)
25096
+ continue;
25097
+ }
25098
+ const varRefRe = new RegExp(`\\b${guardedVar}\\b`);
25099
+ for (let l = blockEnd + 1;l < lines.length; l++) {
25100
+ if (!varRefRe.test(lines[l]))
25101
+ continue;
25102
+ sanitizers.push({
25103
+ type: "js_command_allowlist_guard",
25104
+ method: "if",
25105
+ line: l + 1,
25106
+ sanitizes: ["command_injection", "external_taint_escape"]
25107
+ });
25108
+ }
25109
+ }
25110
+ return sanitizers;
25111
+ }
24841
25112
  function findJavaPathNormalizeStartsWithGuardSanitizers(code) {
24842
25113
  const sanitizers = [];
24843
25114
  const lines = code.split(`
@@ -24915,10 +25186,13 @@ function findJavaArgvFormExecSanitizers(code) {
24915
25186
  `);
24916
25187
  const argvExecRe = /\.\s*exec\s*\(\s*new\s+String\s*\[\s*\]\s*\{/;
24917
25188
  const argvPbRe = /\bnew\s+ProcessBuilder\s*\(\s*new\s+String\s*\[\s*\]\s*\{/;
25189
+ 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;
24918
25190
  for (let i2 = 0;i2 < lines.length; i2++) {
24919
25191
  const text = lines[i2];
24920
25192
  if (!argvExecRe.test(text) && !argvPbRe.test(text))
24921
25193
  continue;
25194
+ if (shellInStringRe.test(text))
25195
+ continue;
24922
25196
  sanitizers.push({
24923
25197
  type: "java_argv_form_exec",
24924
25198
  method: "exec",
@@ -27069,6 +27343,20 @@ function findPythonMongoengineWhereNosqlInjectionFindings(code, file) {
27069
27343
  }
27070
27344
  return findings;
27071
27345
  }
27346
+ function hasHostAllowlistBeforeSink(taintedVars, lines, sinkLineIdx) {
27347
+ for (let i2 = 0;i2 < sinkLineIdx; i2++) {
27348
+ const t = lines[i2];
27349
+ for (const v of taintedVars) {
27350
+ const containsRe = new RegExp(`\\.\\s*contains\\s*\\(\\s*${v}\\s*\\.\\s*getHost\\s*\\(\\s*\\)\\s*\\)`);
27351
+ if (containsRe.test(t))
27352
+ return true;
27353
+ const equalsRe = new RegExp(`\\b${v}\\s*\\.\\s*getHost\\s*\\(\\s*\\)\\s*\\.\\s*equals(?:IgnoreCase)?\\s*\\(\\s*"[^"]+"\\s*\\)`);
27354
+ if (equalsRe.test(t))
27355
+ return true;
27356
+ }
27357
+ }
27358
+ return false;
27359
+ }
27072
27360
  function findJavaUrlOpenStreamSsrfFindings(code, file) {
27073
27361
  const findings = [];
27074
27362
  if (typeof code !== "string" || code.length === 0)
@@ -27133,6 +27421,8 @@ function findJavaUrlOpenStreamSsrfFindings(code, file) {
27133
27421
  }
27134
27422
  if (!tainted)
27135
27423
  continue;
27424
+ if (hasHostAllowlistBeforeSink(taintedVars, lines, i2))
27425
+ continue;
27136
27426
  const key = `${i2 + 1}:${op}`;
27137
27427
  if (seen.has(key))
27138
27428
  continue;
@@ -28443,6 +28733,94 @@ function findJsTemplateInjectionSstiFindings(code, file) {
28443
28733
  return findings;
28444
28734
  }
28445
28735
 
28736
+ // ../circle-ir/dist/analysis/passes/source-semantics-pass.js
28737
+ var DEMO_PATH_RE = /(?:^|\/)(?:demo|example|examples|samples|integration-tests|integration_tests)(?:\/|$)/i;
28738
+ 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*$/;
28739
+ var STATIC_FINAL_RE = /^\s*(?:public\s+|private\s+|protected\s+)?static\s+final\s+/;
28740
+ var ENUM_CONST_REF_RE = /=\s*[A-Z][A-Za-z0-9_]*\.[A-Z][A-Z0-9_]*\s*;?\s*$/;
28741
+ function isConstantSource(code) {
28742
+ if (!code)
28743
+ return false;
28744
+ if (CONST_STRING_ASSIGN_RE.test(code))
28745
+ return true;
28746
+ if (STATIC_FINAL_RE.test(code)) {
28747
+ const rhs = code.split("=").slice(1).join("=").trim();
28748
+ if (rhs.length === 0)
28749
+ return false;
28750
+ if (/^"[^"]*"\s*;?\s*$/.test(rhs))
28751
+ return true;
28752
+ if (/^-?\d+(?:\.\d+)?[fFdDlL]?\s*;?\s*$/.test(rhs))
28753
+ return true;
28754
+ if (/^(?:true|false)\s*;?\s*$/.test(rhs))
28755
+ return true;
28756
+ if (/^[A-Za-z_][\w.]*\s*;?\s*$/.test(rhs))
28757
+ return true;
28758
+ return false;
28759
+ }
28760
+ if (ENUM_CONST_REF_RE.test(code))
28761
+ return true;
28762
+ return false;
28763
+ }
28764
+ var SERVICE_LOADER_RE = /\bServiceLoader\.(?:load|loadInstalled|stream)\s*\(/;
28765
+ var CLASS_FOR_NAME_RE = /\bClass\.forName\s*\(/;
28766
+ var META_INF_SERVICES_RE = /getResources?\s*\(\s*"META-INF\/services\//;
28767
+ var SPI_WINDOW = 30;
28768
+ function isSpiSource(source, lines) {
28769
+ const code = source.code;
28770
+ if (!code)
28771
+ return false;
28772
+ if (SERVICE_LOADER_RE.test(code))
28773
+ return true;
28774
+ if (CLASS_FOR_NAME_RE.test(code)) {
28775
+ const start2 = Math.max(0, source.line - 1 - SPI_WINDOW);
28776
+ const end = Math.min(lines.length, source.line - 1 + SPI_WINDOW + 1);
28777
+ for (let i2 = start2;i2 < end; i2++) {
28778
+ if (META_INF_SERVICES_RE.test(lines[i2]))
28779
+ return true;
28780
+ }
28781
+ }
28782
+ return false;
28783
+ }
28784
+ function isDemoPathFile(file) {
28785
+ if (!file)
28786
+ return false;
28787
+ return DEMO_PATH_RE.test(file);
28788
+ }
28789
+
28790
+ class SourceSemanticsPass {
28791
+ name = "source-semantics";
28792
+ category = "security";
28793
+ run(ctx) {
28794
+ const { graph, code } = ctx;
28795
+ const sources = graph.ir.taint.sources;
28796
+ if (sources.length === 0) {
28797
+ return { constantCount: 0, spiCount: 0, demoPathCount: 0 };
28798
+ }
28799
+ const file = graph.ir.meta.file;
28800
+ const demoPath = isDemoPathFile(file);
28801
+ const lines = code.split(`
28802
+ `);
28803
+ let constantCount = 0;
28804
+ let spiCount = 0;
28805
+ let demoPathCount = 0;
28806
+ for (const source of sources) {
28807
+ if (isConstantSource(source.code)) {
28808
+ source.constant = true;
28809
+ constantCount++;
28810
+ }
28811
+ if (isSpiSource(source, lines)) {
28812
+ source.spi = true;
28813
+ spiCount++;
28814
+ }
28815
+ if (demoPath) {
28816
+ source.demoPath = true;
28817
+ demoPathCount++;
28818
+ }
28819
+ }
28820
+ return { constantCount, spiCount, demoPathCount };
28821
+ }
28822
+ }
28823
+
28446
28824
  // ../circle-ir/dist/analysis/passes/sink-filter-pass.js
28447
28825
  var JS_XSS_SANITIZERS = [
28448
28826
  /\bDOMPurify\.sanitize\s*\(/,
@@ -28500,6 +28878,16 @@ var COMPILED_TEMPLATE_TYPES = new Set([
28500
28878
  "VelocityTemplate",
28501
28879
  "BeetlTemplate"
28502
28880
  ]);
28881
+ var TEMPLATE_ENGINE_LITERAL_TARGETS = new Set([
28882
+ "VelocityEngine",
28883
+ "Velocity",
28884
+ "TemplateEngine",
28885
+ "SpringTemplateEngine",
28886
+ "Configuration",
28887
+ "PebbleEngine",
28888
+ "Pebble",
28889
+ "Handlebars"
28890
+ ]);
28503
28891
  var REFLECTION_LITERAL_METHODS = new Set([
28504
28892
  "forName",
28505
28893
  "loadClass",
@@ -28532,7 +28920,7 @@ var PROTOCOL_CLIENT_PACKAGES = [
28532
28920
  ];
28533
28921
  var OS_EXEC_RECEIVER_RE = /\b(?:Runtime|ProcessBuilder|DefaultExecutor|Executor|Exec|Launcher|ProcStarter|ProcessExecutor|RuntimeUtil)\s*[.(]/;
28534
28922
  var PROCESS_BUILDER_ARGV_FORM_RE = /\bnew\s+ProcessBuilder\s*\(\s*(?:Arrays\.asList\b|List\.of\b|Collections\.singletonList\b|new\s+ArrayList\b|new\s+String\s*\[\s*\]\s*\{|"[^"]*"\s*,)/;
28535
- var JAVA_THROW_STATEMENT_RE = /^\s*throw\s+new\s+\w+(?:Exception|Error)\b/;
28923
+ var JAVA_THROW_STATEMENT_RE = /^\s*throw\s+new\s+\w+(?:Exception|Error)\b[^;]*;\s*(?:\/\/.*|\/\*.*)?$/;
28536
28924
  var JEXL_ENGINE_TYPES = new Set(["JexlEngine", "Jexl", "JxltEngine"]);
28537
28925
  var JEXL_EXPRESSION_TYPE_RE = /(?:Jexl)?(?:Expression|Script|Template)(?:Script)?$/;
28538
28926
  var TEMPLATE_COMPILE_RECEIVER_TYPES = new Set([
@@ -28958,6 +29346,23 @@ class SinkFilterPass {
28958
29346
  const recvType = resolveJavaReceiverType(receiver, sink.line, sourceLines);
28959
29347
  if (recvType && COMPILED_TEMPLATE_TYPES.has(recvType))
28960
29348
  return false;
29349
+ if (recvType && TEMPLATE_ENGINE_LITERAL_TARGETS.has(recvType)) {
29350
+ const args2 = extractJavaCallArgs(method, sinkLineText);
29351
+ if (args2 !== null && args2.length >= 1 && isJavaLiteralOrAnnotationAccessor(args2[0])) {
29352
+ return false;
29353
+ }
29354
+ }
29355
+ }
29356
+ if (method === "evaluate" && receiver) {
29357
+ const recvType = resolveJavaReceiverType(receiver, sink.line, sourceLines);
29358
+ if (recvType && TEMPLATE_ENGINE_LITERAL_TARGETS.has(recvType)) {
29359
+ const args2 = extractJavaCallArgs(method, sinkLineText);
29360
+ if (args2 !== null && args2.length >= 1) {
29361
+ const last = args2[args2.length - 1] ?? "";
29362
+ if (isJavaLiteralOrAnnotationAccessor(last))
29363
+ return false;
29364
+ }
29365
+ }
28961
29366
  }
28962
29367
  if (method && REFLECTION_LITERAL_METHODS.has(method)) {
28963
29368
  const args2 = extractJavaCallArgs(method, sinkLineText);
@@ -29024,7 +29429,7 @@ class SinkFilterPass {
29024
29429
  filtered = filtered.filter((sink) => {
29025
29430
  if (sink.type !== "command_injection")
29026
29431
  return true;
29027
- if (sink.method !== "ProcessBuilder")
29432
+ if (sink.method !== "ProcessBuilder" && sink.method !== "start")
29028
29433
  return true;
29029
29434
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
29030
29435
  if (!/\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText))
@@ -29861,6 +30266,54 @@ function filterSanitizedSinks(sinks, sanitizers, calls) {
29861
30266
  });
29862
30267
  }
29863
30268
 
30269
+ // ../circle-ir/dist/analysis/passes/sink-semantics-pass.js
30270
+ function buildRegistry(entries) {
30271
+ const registry = new Map;
30272
+ for (const entry of entries) {
30273
+ const existing = registry.get(entry.signature);
30274
+ if (existing) {
30275
+ for (const t of entry.overrides)
30276
+ existing.add(t);
30277
+ } else {
30278
+ registry.set(entry.signature, new Set(entry.overrides));
30279
+ }
30280
+ }
30281
+ return registry;
30282
+ }
30283
+
30284
+ class SinkSemanticsPass {
30285
+ name = "sink-semantics";
30286
+ category = "security";
30287
+ run(ctx) {
30288
+ const { graph, config } = ctx;
30289
+ const entries = config.sinkSemantics ?? [];
30290
+ if (entries.length === 0) {
30291
+ return { droppedCount: 0, registrySize: 0 };
30292
+ }
30293
+ const registry = buildRegistry(entries);
30294
+ const sinks = graph.ir.taint.sinks;
30295
+ let droppedCount = 0;
30296
+ const kept = sinks.filter((sink) => {
30297
+ if (!sink.class || !sink.method)
30298
+ return true;
30299
+ const signature = `${sink.class}#${sink.method}`;
30300
+ const overrides = registry.get(signature);
30301
+ if (!overrides)
30302
+ return true;
30303
+ if (overrides.has(sink.type)) {
30304
+ droppedCount++;
30305
+ return false;
30306
+ }
30307
+ return true;
30308
+ });
30309
+ if (droppedCount > 0) {
30310
+ sinks.length = 0;
30311
+ sinks.push(...kept);
30312
+ }
30313
+ return { droppedCount, registrySize: registry.size };
30314
+ }
30315
+ }
30316
+
29864
30317
  // ../circle-ir/dist/analysis/passes/taint-propagation-pass.js
29865
30318
  class TaintPropagationPass {
29866
30319
  name = "taint-propagation";
@@ -30555,6 +31008,27 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
30555
31008
  }
30556
31009
  }
30557
31010
  }
31011
+ if (language === "java" && typeof code === "string" && sourcesWithVar.length > 0) {
31012
+ const seedVars = new Set(sourcesWithVar.map((s) => s.variable));
31013
+ const derived = buildJavaTaintedVars(code, seedVars);
31014
+ if (derived.size > 0) {
31015
+ let anchor = sourcesWithVar[0];
31016
+ for (const s of sourcesWithVar) {
31017
+ if (s.line < anchor.line)
31018
+ anchor = s;
31019
+ }
31020
+ const existingVars = new Set(sourcesWithVar.map((s) => s.variable));
31021
+ for (const [varName] of derived) {
31022
+ if (!varName || existingVars.has(varName))
31023
+ continue;
31024
+ sourcesWithVar.push({
31025
+ ...anchor,
31026
+ variable: varName
31027
+ });
31028
+ existingVars.add(varName);
31029
+ }
31030
+ }
31031
+ }
30558
31032
  const reCache = new Map;
30559
31033
  for (const s of sourcesWithVar) {
30560
31034
  if (reCache.has(s.variable))
@@ -30589,6 +31063,8 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
30589
31063
  const re = reCache.get(source.variable);
30590
31064
  if (!re || !re.test(expr))
30591
31065
  continue;
31066
+ if (!sourceSemanticsAllowed(source, sink.type))
31067
+ continue;
30592
31068
  if (flows.some((f) => f.source_line === source.line && f.sink_line === sink.line && f.sink_type === sink.type))
30593
31069
  continue;
30594
31070
  if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
@@ -30616,8 +31092,6 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
30616
31092
  }
30617
31093
  const sourcesByLine = new Map;
30618
31094
  for (const s of sources) {
30619
- if (s.variable && s.variable.length > 0)
30620
- continue;
30621
31095
  const arr = sourcesByLine.get(s.line) ?? [];
30622
31096
  arr.push(s);
30623
31097
  sourcesByLine.set(s.line, arr);
@@ -30631,6 +31105,20 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
30631
31105
  for (const source of colocSources) {
30632
31106
  if (!canSourceReachSink(source.type, sink.type))
30633
31107
  continue;
31108
+ if (!sourceSemanticsAllowed(source, sink.type))
31109
+ continue;
31110
+ const sourceVar = source.variable;
31111
+ if (sourceVar && sourceVar.length > 0) {
31112
+ const sinkCode = sink.code;
31113
+ if (!sinkCode) {
31114
+ continue;
31115
+ }
31116
+ const assignMatch = sinkCode.match(/^\s*(?:[A-Za-z_][\w.<>[\]\s,?]*\s+)?[A-Za-z_]\w*\s*=(?!=)\s*/);
31117
+ const rhs = assignMatch ? sinkCode.slice(assignMatch[0].length) : sinkCode;
31118
+ if (new RegExp(`\\b${sourceVar}\\b`).test(rhs)) {
31119
+ continue;
31120
+ }
31121
+ }
30634
31122
  if (source.type === "file_input" && sink.type === "path_traversal" && sink.method && source.location.includes(`${sink.method}(`)) {
30635
31123
  continue;
30636
31124
  }
@@ -31219,7 +31707,9 @@ var TIER_1_METHOD_ANNOTATIONS = new Set([
31219
31707
  "DELETE",
31220
31708
  "PATCH",
31221
31709
  "HEAD",
31222
- "OPTIONS"
31710
+ "OPTIONS",
31711
+ "DataBoundConstructor",
31712
+ "DataBoundSetter"
31223
31713
  ]);
31224
31714
  var TIER_1_CLASS_ANNOTATIONS = new Set([
31225
31715
  "RestController",
@@ -31244,7 +31734,13 @@ var TIER_1_BY_SUPERTYPE = new Map([
31244
31734
  ["ChannelInboundHandler", new Set(["channelRead", "channelReadComplete"])],
31245
31735
  ["ChannelInboundHandlerAdapter", new Set(["channelRead", "channelReadComplete"])],
31246
31736
  ["ChannelDuplexHandler", new Set(["channelRead", "channelReadComplete"])],
31247
- ["NettyRequestProcessor", new Set(["process"])]
31737
+ ["NettyRequestProcessor", new Set(["process"])],
31738
+ ["Converter", new Set(["marshal", "unmarshal"])],
31739
+ ["SingleValueConverter", new Set(["fromString", "toString"])],
31740
+ ["ConverterMatcher", new Set(["marshal", "unmarshal"])],
31741
+ ["AbstractReflectionConverter", new Set(["marshal", "unmarshal", "doMarshal", "doUnmarshal"])],
31742
+ ["AbstractSingleValueConverter", new Set(["fromString", "toString"])],
31743
+ ["AbstractCollectionConverter", new Set(["marshal", "unmarshal"])]
31248
31744
  ]);
31249
31745
  var TIER_3_CLASS_SUFFIXES = [
31250
31746
  "Util",
@@ -35754,6 +36250,14 @@ function isProtocolMandatedCryptoFile(file, code) {
35754
36250
  }
35755
36251
 
35756
36252
  // ../circle-ir/dist/analysis/passes/scan-secrets-pass.js
36253
+ function applyDemoDowngrade(demoPath, severity, level) {
36254
+ if (!demoPath)
36255
+ return { severity, level };
36256
+ if (severity === "high") {
36257
+ return { severity: "low", level: "note" };
36258
+ }
36259
+ return { severity, level };
36260
+ }
35757
36261
  var TEST_PATH_RE3 = /(?:^|[\\/])(?:test|tests|spec|specs|__tests?__|__mocks?__|fixtures?|testdata)(?:[\\/]|$)/i;
35758
36262
  var TEST_FILENAME_RE = /(?:\.(?:test|spec)\.[cm]?[jt]sx?|_test\.go|_test\.py|Test\.java|Tests\.java)$/i;
35759
36263
  function isTestFile(file) {
@@ -36104,6 +36608,7 @@ class ScanSecretsPass {
36104
36608
  if (isTestFile(file) || isGeneratedFile(file)) {
36105
36609
  return { providerFindings: 0, entropyFindings: 0 };
36106
36610
  }
36611
+ const demoPath = DEMO_PATH_RE.test(file);
36107
36612
  const lines = ctx.code.split(`
36108
36613
  `);
36109
36614
  const prior = ctx.getFindings?.() ?? [];
@@ -36134,14 +36639,15 @@ class ScanSecretsPass {
36134
36639
  if (seen.has(key))
36135
36640
  continue;
36136
36641
  seen.add(key);
36642
+ const dg = applyDemoDowngrade(demoPath, pattern.severity, pattern.level);
36137
36643
  ctx.addFinding({
36138
36644
  id: `hardcoded-credential-${file}-${lineNum}`,
36139
36645
  pass: this.name,
36140
36646
  category: this.category,
36141
36647
  rule_id: "hardcoded-credential",
36142
36648
  cwe: "CWE-798",
36143
- severity: pattern.severity,
36144
- level: pattern.level,
36649
+ severity: dg.severity,
36650
+ level: dg.level,
36145
36651
  message: `Hardcoded credential: ${pattern.name} detected`,
36146
36652
  file,
36147
36653
  line: lineNum,
@@ -36163,14 +36669,15 @@ class ScanSecretsPass {
36163
36669
  if (seen.has(key))
36164
36670
  continue;
36165
36671
  seen.add(key);
36672
+ const dg = applyDemoDowngrade(demoPath, "high", "error");
36166
36673
  ctx.addFinding({
36167
36674
  id: `hardcoded-credential-${file}-${lineNum}`,
36168
36675
  pass: this.name,
36169
36676
  category: this.category,
36170
36677
  rule_id: "hardcoded-credential",
36171
36678
  cwe: "CWE-798",
36172
- severity: "high",
36173
- level: "error",
36679
+ severity: dg.severity,
36680
+ level: dg.level,
36174
36681
  message: `Hardcoded credential: \`${hit.name}\` assigned a literal value`,
36175
36682
  file,
36176
36683
  line: lineNum,
@@ -40791,7 +41298,11 @@ async function analyze(code, filePath, language, options = {}) {
40791
41298
  pipeline.add(new TaintMatcherPass);
40792
41299
  pipeline.add(new ConstantPropagationPass(tree));
40793
41300
  pipeline.add(new LanguageSourcesPass);
41301
+ if (!disabledPasses.has("source-semantics"))
41302
+ pipeline.add(new SourceSemanticsPass);
40794
41303
  pipeline.add(new SinkFilterPass);
41304
+ if (!disabledPasses.has("sink-semantics"))
41305
+ pipeline.add(new SinkSemanticsPass);
40795
41306
  pipeline.add(new TaintPropagationPass);
40796
41307
  pipeline.add(new InterproceduralPass({
40797
41308
  enableEntryPointGate: options.enableEntryPointGate ?? true
@@ -41719,7 +42230,7 @@ var colors = {
41719
42230
  };
41720
42231
 
41721
42232
  // src/version.ts
41722
- var version = "3.141.0";
42233
+ var version = "3.146.0";
41723
42234
 
41724
42235
  // src/formatters.ts
41725
42236
  var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "3.141.0",
3
+ "version": "3.146.0",
4
4
  "description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@cognium/project-profile-detect": "^1.1.0",
69
- "circle-ir": "^3.141.0"
69
+ "circle-ir": "^3.146.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/node": "^25.5.0",