circle-ir 3.77.0 → 3.80.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.
@@ -11736,10 +11736,17 @@ var DEFAULT_SINKS = [
11736
11736
  // These patterns are detected by call-site literal inspection, not taint flow,
11737
11737
  // so they are NOT registered here as sinks (they could never match a "tainted
11738
11738
  // value flowing into a sink" because the bad value is a hard-coded constant).
11739
- // Trust Boundary (CWE-501) - using untrusted data as session attribute NAME
11740
- // The vulnerability is attacker controlling which key to use, not the value
11741
- { method: "setAttribute", class: "HttpSession", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [0] },
11742
- { method: "putValue", class: "HttpSession", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [0] },
11739
+ // Trust Boundary (CWE-501) tainted VALUE crossing into shared session
11740
+ // state. OWASP/CWE-501 treats `session.setAttribute("k", taintedValue)` as
11741
+ // the violation: untrusted data enters server-side state where downstream
11742
+ // code reads it as if trusted. Both arg positions are flagged so either a
11743
+ // tainted key (rare) or tainted value (the OWASP shape, 83 cases) trips
11744
+ // the sink. (cognium-dev #117)
11745
+ { method: "setAttribute", class: "HttpSession", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [0, 1] },
11746
+ { method: "putValue", class: "HttpSession", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [0, 1] },
11747
+ // ServletContext + request scopes — same trust-boundary semantics.
11748
+ { method: "setAttribute", class: "ServletContext", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [0, 1] },
11749
+ { method: "setAttribute", class: "HttpServletRequest", type: "trust_boundary", cwe: "CWE-501", severity: "low", arg_positions: [0, 1] },
11743
11750
  // Additional XSS patterns (JDOM/XML output)
11744
11751
  { method: "outputElementContent", class: "XMLOutputter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11745
11752
  { method: "output", class: "XMLOutputter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
@@ -28683,6 +28690,23 @@ var PROVIDER_PATTERNS = [
28683
28690
  fix: "Revoke the npm token at https://www.npmjs.com/settings/<user>/tokens and load from environment."
28684
28691
  }
28685
28692
  ];
28693
+ var CRED_KEYWORD_RE = /\b([A-Za-z_$][\w$]*?(?:password|passwd|secret|api[_-]?key|auth[_-]?token|private[_-]?key|access[_-]?key)[\w$]*?)\s*[:=]\s*["'`]([^"'`\s$][^"'`\n]{2,})["'`]/i;
28694
+ var CRED_DYNAMIC_VALUE_RE = /\$\{|process\.env|os\.environ|os\.Getenv|System\.getenv/;
28695
+ var CRED_FUNCTION_DECL_RE = /\b(?:function|func|def|fn)\s+\w+\s*\(/;
28696
+ var CRED_COMPARISON_RE = /(?:===?|!==?|>=|<=|<>)\s*["'`]/;
28697
+ function isLikelyCredentialAssignment(line) {
28698
+ if (CRED_FUNCTION_DECL_RE.test(line)) return null;
28699
+ if (CRED_COMPARISON_RE.test(line)) return null;
28700
+ const m = line.match(CRED_KEYWORD_RE);
28701
+ if (!m) return null;
28702
+ const name2 = m[1];
28703
+ const value = m[2];
28704
+ if (PLACEHOLDER_RE.test(value)) return null;
28705
+ if (CRED_DYNAMIC_VALUE_RE.test(value)) return null;
28706
+ if (value.length < 3) return null;
28707
+ if (isAllSameChar(value)) return null;
28708
+ return { name: name2, value };
28709
+ }
28686
28710
  var STRING_LITERAL_RE = /(["'`])((?:\\.|(?!\1).){8,200})\1/g;
28687
28711
  var BASE64ISH_RE = /^[A-Za-z0-9+/=_-]+$/;
28688
28712
  var HEXISH_RE = /^[a-fA-F0-9]+$/;
@@ -28774,6 +28798,31 @@ var ScanSecretsPass = class {
28774
28798
  break;
28775
28799
  }
28776
28800
  }
28801
+ for (let i2 = 0; i2 < lines.length; i2++) {
28802
+ const lineText = lines[i2];
28803
+ const lineNum = i2 + 1;
28804
+ const hit = isLikelyCredentialAssignment(lineText);
28805
+ if (!hit) continue;
28806
+ const key = `${lineNum}:hardcoded-credential`;
28807
+ if (seen.has(key)) continue;
28808
+ seen.add(key);
28809
+ ctx.addFinding({
28810
+ id: `hardcoded-credential-${file}-${lineNum}`,
28811
+ pass: this.name,
28812
+ category: this.category,
28813
+ rule_id: "hardcoded-credential",
28814
+ cwe: "CWE-798",
28815
+ severity: "high",
28816
+ level: "error",
28817
+ message: `Hardcoded credential: \`${hit.name}\` assigned a literal value`,
28818
+ file,
28819
+ line: lineNum,
28820
+ snippet: lineText.trim().substring(0, 120),
28821
+ fix: "Move the credential to an environment variable or secrets manager; never commit live secrets to source control.",
28822
+ evidence: { kind: "named-credential", name: hit.name }
28823
+ });
28824
+ providerFindings += 1;
28825
+ }
28777
28826
  for (let i2 = 0; i2 < lines.length; i2++) {
28778
28827
  const lineText = lines[i2];
28779
28828
  const lineNum = i2 + 1;
@@ -29141,8 +29190,10 @@ var InsecureCookiePass = class {
29141
29190
  }
29142
29191
  // ---------------- Java ----------------
29143
29192
  detectJavaCookieCtor(call, hasSetSecureTrue, hasSetHttpOnlyTrue) {
29144
- if (call.method_name !== "Cookie") return null;
29145
- const looksLikeCtor = call.is_constructor || !call.receiver && call.receiver_type === "Cookie" || (call.resolution?.target ?? "").endsWith(".<init>");
29193
+ const method = call.method_name ?? "";
29194
+ const isCookieCtor = method === "Cookie" || method.endsWith(".Cookie");
29195
+ if (!isCookieCtor) return null;
29196
+ const looksLikeCtor = call.is_constructor || !call.receiver && (call.receiver_type === "Cookie" || (call.receiver_type ?? "").endsWith(".Cookie")) || (call.resolution?.target ?? "").endsWith(".<init>");
29146
29197
  if (!looksLikeCtor) return null;
29147
29198
  if (call.arguments.length < 2) return null;
29148
29199
  const missingSecure = !hasSetSecureTrue;
@@ -29202,6 +29253,17 @@ var COMMONS_DIGEST_METHODS = /* @__PURE__ */ new Set([
29202
29253
  "sha",
29203
29254
  "shaHex"
29204
29255
  ]);
29256
+ var COMMONS_DIGEST_GETTERS = {
29257
+ getMd2Digest: "md2",
29258
+ getMd5Digest: "md5",
29259
+ getSha1Digest: "sha1",
29260
+ getShaDigest: "sha1"
29261
+ };
29262
+ var COMMONS_ALGO_CONSTANTS = {
29263
+ "MessageDigestAlgorithms.MD2": "md2",
29264
+ "MessageDigestAlgorithms.MD5": "md5",
29265
+ "MessageDigestAlgorithms.SHA_1": "sha1"
29266
+ };
29205
29267
  var PY_HASHLIB_WEAK = /* @__PURE__ */ new Set(["md5", "sha1", "md4", "md2", "new"]);
29206
29268
  function stripQuotes4(s) {
29207
29269
  const trimmed = s.trim();
@@ -29217,15 +29279,58 @@ function literalAlgo(call, position) {
29217
29279
  const cleaned = stripQuotes4(raw).toLowerCase();
29218
29280
  return cleaned || null;
29219
29281
  }
29282
+ function resolveJavaAlgo(call, position, constProp, javaBindings) {
29283
+ const arg = call.arguments.find((a) => a.position === position);
29284
+ if (!arg) return null;
29285
+ if (arg.literal) {
29286
+ const cleaned = stripQuotes4(arg.literal).toLowerCase();
29287
+ if (cleaned) return cleaned;
29288
+ }
29289
+ const expr = (arg.expression ?? "").trim();
29290
+ if (expr.startsWith('"') || expr.startsWith("`") || expr.startsWith("'")) {
29291
+ const cleaned = stripQuotes4(expr).toLowerCase();
29292
+ if (cleaned) return cleaned;
29293
+ }
29294
+ if (COMMONS_ALGO_CONSTANTS[expr]) return COMMONS_ALGO_CONSTANTS[expr];
29295
+ const tail = expr.split(".").slice(-2).join(".");
29296
+ if (COMMONS_ALGO_CONSTANTS[tail]) return COMMONS_ALGO_CONSTANTS[tail];
29297
+ if (arg.variable && constProp) {
29298
+ const sym = constProp.symbols?.get(arg.variable);
29299
+ if (sym && sym.type === "string" && typeof sym.value === "string") {
29300
+ const cleaned = stripQuotes4(sym.value).toLowerCase();
29301
+ if (cleaned) return cleaned;
29302
+ }
29303
+ }
29304
+ if (arg.variable) {
29305
+ const bound = javaBindings.get(arg.variable);
29306
+ if (bound) {
29307
+ const cleaned = stripQuotes4(bound).toLowerCase();
29308
+ if (cleaned) return cleaned;
29309
+ }
29310
+ }
29311
+ return null;
29312
+ }
29313
+ function scanJavaStringBindings(code) {
29314
+ const out2 = /* @__PURE__ */ new Map();
29315
+ if (!code) return out2;
29316
+ const re = /^[ \t]*(?:(?:public|private|protected|static|final|volatile)\s+){0,5}String\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*("[^"]*")\s*;/gm;
29317
+ let m;
29318
+ while ((m = re.exec(code)) !== null) {
29319
+ if (m[1] && m[2]) out2.set(m[1], m[2]);
29320
+ }
29321
+ return out2;
29322
+ }
29220
29323
  var WeakHashPass = class {
29221
29324
  name = "weak-hash";
29222
29325
  category = "security";
29223
29326
  run(ctx) {
29224
- const { graph, language } = ctx;
29327
+ const { graph, language, code } = ctx;
29225
29328
  const file = graph.ir.meta.file;
29226
29329
  const findings = [];
29330
+ const constProp = ctx.hasResult("constant-propagation") ? ctx.getResult("constant-propagation") : null;
29331
+ const javaBindings = language === "java" ? scanJavaStringBindings(code) : /* @__PURE__ */ new Map();
29227
29332
  for (const call of graph.ir.calls) {
29228
- const detection = this.detect(call, language);
29333
+ const detection = this.detect(call, language, constProp, javaBindings);
29229
29334
  if (!detection) continue;
29230
29335
  const { algorithm, api } = detection;
29231
29336
  const line = call.location.line;
@@ -29247,12 +29352,12 @@ var WeakHashPass = class {
29247
29352
  }
29248
29353
  return { findings };
29249
29354
  }
29250
- detect(call, language) {
29355
+ detect(call, language, constProp, javaBindings) {
29251
29356
  const method = call.method_name;
29252
29357
  const receiver = call.receiver ?? "";
29253
29358
  if (language === "java") {
29254
29359
  if (method === "getInstance" && (receiver === "MessageDigest" || receiver.endsWith(".MessageDigest"))) {
29255
- const algo = literalAlgo(call, 0);
29360
+ const algo = resolveJavaAlgo(call, 0, constProp, javaBindings);
29256
29361
  if (algo && WEAK_HASH_NAMES.has(algo)) {
29257
29362
  return { algorithm: algo, api: "MessageDigest.getInstance" };
29258
29363
  }
@@ -29262,6 +29367,9 @@ var WeakHashPass = class {
29262
29367
  const normalized = algoFromMethod === "sha" ? "sha1" : algoFromMethod;
29263
29368
  return { algorithm: normalized, api: `DigestUtils.${method}` };
29264
29369
  }
29370
+ if (COMMONS_DIGEST_GETTERS[method] && (receiver === "DigestUtils" || receiver.endsWith(".DigestUtils"))) {
29371
+ return { algorithm: COMMONS_DIGEST_GETTERS[method], api: `DigestUtils.${method}` };
29372
+ }
29265
29373
  return null;
29266
29374
  }
29267
29375
  if (language === "python") {
@@ -29544,8 +29652,9 @@ var WeakCryptoPass = class {
29544
29652
  const receiver = call.receiver ?? "";
29545
29653
  const out2 = [];
29546
29654
  if (language === "java") {
29547
- const isCipherFactory = method === "getInstance" && (receiver === "Cipher" || receiver.endsWith(".Cipher") || receiver === "KeyGenerator" || receiver.endsWith(".KeyGenerator"));
29548
- if (isCipherFactory) {
29655
+ const isCipherInstance = method === "getInstance" && (receiver === "Cipher" || receiver.endsWith(".Cipher"));
29656
+ const isKeyGenInstance = method === "getInstance" && (receiver === "KeyGenerator" || receiver.endsWith(".KeyGenerator"));
29657
+ if (isCipherInstance) {
29549
29658
  const spec = literalAlgo2(call, 0);
29550
29659
  if (spec) {
29551
29660
  const { weakBase, ecb } = classifyJavaCipherSpec(spec);
@@ -29553,6 +29662,13 @@ var WeakCryptoPass = class {
29553
29662
  if (weakBase) out2.push({ issue: "weak-cipher", detail: weakBase, api });
29554
29663
  if (ecb) out2.push({ issue: "ecb-mode", detail: spec, api });
29555
29664
  }
29665
+ } else if (isKeyGenInstance) {
29666
+ const spec = literalAlgo2(call, 0);
29667
+ if (spec) {
29668
+ const { weakBase } = classifyJavaCipherSpec(spec);
29669
+ const api = `${receiver}.getInstance`;
29670
+ if (weakBase) out2.push({ issue: "weak-cipher", detail: weakBase, api });
29671
+ }
29556
29672
  }
29557
29673
  if (method === "IvParameterSpec" && isJavaCtor(call, "IvParameterSpec")) {
29558
29674
  const ivDetail = detectStaticIvJava(call);
@@ -11118,10 +11118,17 @@ var DEFAULT_SINKS = [
11118
11118
  // These patterns are detected by call-site literal inspection, not taint flow,
11119
11119
  // so they are NOT registered here as sinks (they could never match a "tainted
11120
11120
  // value flowing into a sink" because the bad value is a hard-coded constant).
11121
- // Trust Boundary (CWE-501) - using untrusted data as session attribute NAME
11122
- // The vulnerability is attacker controlling which key to use, not the value
11123
- { method: "setAttribute", class: "HttpSession", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [0] },
11124
- { method: "putValue", class: "HttpSession", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [0] },
11121
+ // Trust Boundary (CWE-501) tainted VALUE crossing into shared session
11122
+ // state. OWASP/CWE-501 treats `session.setAttribute("k", taintedValue)` as
11123
+ // the violation: untrusted data enters server-side state where downstream
11124
+ // code reads it as if trusted. Both arg positions are flagged so either a
11125
+ // tainted key (rare) or tainted value (the OWASP shape, 83 cases) trips
11126
+ // the sink. (cognium-dev #117)
11127
+ { method: "setAttribute", class: "HttpSession", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [0, 1] },
11128
+ { method: "putValue", class: "HttpSession", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [0, 1] },
11129
+ // ServletContext + request scopes — same trust-boundary semantics.
11130
+ { method: "setAttribute", class: "ServletContext", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [0, 1] },
11131
+ { method: "setAttribute", class: "HttpServletRequest", type: "trust_boundary", cwe: "CWE-501", severity: "low", arg_positions: [0, 1] },
11125
11132
  // Additional XSS patterns (JDOM/XML output)
11126
11133
  { method: "outputElementContent", class: "XMLOutputter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11127
11134
  { method: "output", class: "XMLOutputter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
@@ -11052,10 +11052,17 @@ var DEFAULT_SINKS = [
11052
11052
  // These patterns are detected by call-site literal inspection, not taint flow,
11053
11053
  // so they are NOT registered here as sinks (they could never match a "tainted
11054
11054
  // value flowing into a sink" because the bad value is a hard-coded constant).
11055
- // Trust Boundary (CWE-501) - using untrusted data as session attribute NAME
11056
- // The vulnerability is attacker controlling which key to use, not the value
11057
- { method: "setAttribute", class: "HttpSession", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [0] },
11058
- { method: "putValue", class: "HttpSession", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [0] },
11055
+ // Trust Boundary (CWE-501) tainted VALUE crossing into shared session
11056
+ // state. OWASP/CWE-501 treats `session.setAttribute("k", taintedValue)` as
11057
+ // the violation: untrusted data enters server-side state where downstream
11058
+ // code reads it as if trusted. Both arg positions are flagged so either a
11059
+ // tainted key (rare) or tainted value (the OWASP shape, 83 cases) trips
11060
+ // the sink. (cognium-dev #117)
11061
+ { method: "setAttribute", class: "HttpSession", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [0, 1] },
11062
+ { method: "putValue", class: "HttpSession", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [0, 1] },
11063
+ // ServletContext + request scopes — same trust-boundary semantics.
11064
+ { method: "setAttribute", class: "ServletContext", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [0, 1] },
11065
+ { method: "setAttribute", class: "HttpServletRequest", type: "trust_boundary", cwe: "CWE-501", severity: "low", arg_positions: [0, 1] },
11059
11066
  // Additional XSS patterns (JDOM/XML output)
11060
11067
  { method: "outputElementContent", class: "XMLOutputter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11061
11068
  { method: "output", class: "XMLOutputter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circle-ir",
3
- "version": "3.77.0",
3
+ "version": "3.80.0",
4
4
  "description": "High-performance Static Application Security Testing (SAST) library for detecting security vulnerabilities through taint analysis",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",