circle-ir 3.83.0 → 3.85.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.
@@ -13208,6 +13208,55 @@ function isSafeGoExecCommandCall(call, pattern, language) {
13208
13208
  if (SHELL_PROGRAMS.has(program)) return false;
13209
13209
  return true;
13210
13210
  }
13211
+ function isSafeRustCommandCall(call, pattern, language) {
13212
+ if (language !== "rust") return false;
13213
+ if (pattern.type !== "command_injection") return false;
13214
+ if (pattern.class !== void 0 && pattern.class !== "Command") return false;
13215
+ const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
13216
+ "sh",
13217
+ "bash",
13218
+ "zsh",
13219
+ "dash",
13220
+ "ash",
13221
+ "ksh",
13222
+ "cmd",
13223
+ "cmd.exe",
13224
+ "powershell",
13225
+ "pwsh",
13226
+ "powershell.exe",
13227
+ "pwsh.exe"
13228
+ ]);
13229
+ const PROGRAM_RE = /\bCommand\s*::\s*new\s*\(\s*(?:r?"([^"]*)"|'([^']*)')/;
13230
+ const extractProgram = (text) => {
13231
+ const m = PROGRAM_RE.exec(text);
13232
+ if (!m) return null;
13233
+ const lit = m[1] ?? m[2] ?? "";
13234
+ return lit.split("/").pop() ?? lit;
13235
+ };
13236
+ if (pattern.method === "new") {
13237
+ const programArg = call.arguments.find((a) => a.position === 0);
13238
+ if (!programArg) return false;
13239
+ let program;
13240
+ if (programArg.literal !== null && programArg.literal !== void 0) {
13241
+ program = String(programArg.literal).split("/").pop() ?? String(programArg.literal);
13242
+ } else {
13243
+ const expr = (programArg.expression ?? "").trim();
13244
+ if (!(expr.startsWith('"') || expr.startsWith("'"))) {
13245
+ return false;
13246
+ }
13247
+ const stripped = expr.slice(1, -1);
13248
+ program = stripped.split("/").pop() ?? stripped;
13249
+ }
13250
+ return !SHELL_PROGRAMS.has(program);
13251
+ }
13252
+ if (pattern.method === "arg" || pattern.method === "args" || pattern.method === "spawn" || pattern.method === "output") {
13253
+ const receiverText = call.receiver ?? "";
13254
+ const program = extractProgram(receiverText);
13255
+ if (program === null) return false;
13256
+ return !SHELL_PROGRAMS.has(program);
13257
+ }
13258
+ return false;
13259
+ }
13211
13260
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
13212
13261
  function argIsClassLiteral(call, position) {
13213
13262
  const arg = call.arguments.find((a) => a.position === position);
@@ -13230,6 +13279,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13230
13279
  if (isSafeGoExecCommandCall(call, pattern, language)) {
13231
13280
  continue;
13232
13281
  }
13282
+ if (isSafeRustCommandCall(call, pattern, language)) {
13283
+ continue;
13284
+ }
13233
13285
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
13234
13286
  continue;
13235
13287
  }
@@ -22755,6 +22807,14 @@ var LanguageSourcesPass = class {
22755
22807
  additionalSanitizers.push(...findGoMapAllowlistGuardSanitizers(code));
22756
22808
  additionalSanitizers.push(...findGoHtmlTemplateImportSanitizers(code));
22757
22809
  }
22810
+ if (language === "python") {
22811
+ additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
22812
+ additionalSanitizers.push(...findPythonRangeCheckGuardSanitizers(code));
22813
+ }
22814
+ if (language === "rust") {
22815
+ additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
22816
+ additionalSanitizers.push(...findRustCanonicalizeGuardSanitizers(code));
22817
+ }
22758
22818
  attachSourceLineCode(additionalSources, additionalSinks, code);
22759
22819
  return { additionalSources, additionalSinks, additionalSanitizers, pyTaintedVars, pySanitizedVars, jsTaintedVars };
22760
22820
  }
@@ -23751,6 +23811,178 @@ function findGoHtmlTemplateImportSanitizers(code) {
23751
23811
  }
23752
23812
  return sanitizers;
23753
23813
  }
23814
+ function findPythonNetlocAllowlistGuardSanitizers(code) {
23815
+ const sanitizers = [];
23816
+ const lines = code.split("\n");
23817
+ const guardOpen = /^(\s*)if\s+.+?\s+not\s+in\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*$/;
23818
+ const allowlistName = /^(?:[A-Z][A-Z0-9_]+|.*?(allowed|accepted|whitelist|permitted|valid|approved).*)$/i;
23819
+ const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
23820
+ for (let i2 = 0; i2 < lines.length; i2++) {
23821
+ const m = guardOpen.exec(lines[i2]);
23822
+ if (!m) continue;
23823
+ const guardIndent = m[1].length;
23824
+ const allowName = m[2];
23825
+ if (!allowlistName.test(allowName)) continue;
23826
+ let bodyHasTerminator = false;
23827
+ let blockEnd = -1;
23828
+ const maxScan = Math.min(lines.length, i2 + 26);
23829
+ for (let j = i2 + 1; j < maxScan; j++) {
23830
+ const line = lines[j];
23831
+ if (line.trim() === "") continue;
23832
+ const indent = line.length - line.trimStart().length;
23833
+ if (indent <= guardIndent) {
23834
+ blockEnd = j - 1;
23835
+ break;
23836
+ }
23837
+ if (terminator.test(line)) bodyHasTerminator = true;
23838
+ }
23839
+ if (blockEnd === -1) blockEnd = Math.min(lines.length - 1, i2 + 25);
23840
+ if (!bodyHasTerminator) continue;
23841
+ for (let l = blockEnd + 2; l <= lines.length; l++) {
23842
+ sanitizers.push({
23843
+ type: "python_netloc_allowlist_guard",
23844
+ method: "if",
23845
+ line: l,
23846
+ sanitizes: [
23847
+ "open_redirect",
23848
+ "ssrf",
23849
+ "path_traversal",
23850
+ "external_taint_escape"
23851
+ ]
23852
+ });
23853
+ }
23854
+ }
23855
+ return sanitizers;
23856
+ }
23857
+ function findPythonRangeCheckGuardSanitizers(code) {
23858
+ const sanitizers = [];
23859
+ const lines = code.split("\n");
23860
+ const rangeGuard = /^(\s*)if\s+([A-Za-z_][A-Za-z0-9_]*)\s*[<>]=?\s*(?:\d+|-?\d+\.?\d*|[A-Z][A-Z0-9_]+)\s*(?:(?:or|and)\s+\2\s*[<>]=?\s*(?:\d+|-?\d+\.?\d*|[A-Z][A-Z0-9_]+)\s*)?:\s*$/;
23861
+ const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
23862
+ for (let i2 = 0; i2 < lines.length; i2++) {
23863
+ const m = rangeGuard.exec(lines[i2]);
23864
+ if (!m) continue;
23865
+ const guardIndent = m[1].length;
23866
+ let bodyHasTerminator = false;
23867
+ let blockEnd = -1;
23868
+ const maxScan = Math.min(lines.length, i2 + 26);
23869
+ for (let j = i2 + 1; j < maxScan; j++) {
23870
+ const line = lines[j];
23871
+ if (line.trim() === "") continue;
23872
+ const indent = line.length - line.trimStart().length;
23873
+ if (indent <= guardIndent) {
23874
+ blockEnd = j - 1;
23875
+ break;
23876
+ }
23877
+ if (terminator.test(line)) bodyHasTerminator = true;
23878
+ }
23879
+ if (blockEnd === -1) blockEnd = Math.min(lines.length - 1, i2 + 25);
23880
+ if (!bodyHasTerminator) continue;
23881
+ for (let l = blockEnd + 2; l <= lines.length; l++) {
23882
+ sanitizers.push({
23883
+ type: "python_range_check_guard",
23884
+ method: "if",
23885
+ line: l,
23886
+ sanitizes: ["xss", "external_taint_escape"]
23887
+ });
23888
+ }
23889
+ }
23890
+ return sanitizers;
23891
+ }
23892
+ function findRustSetAllowlistGuardSanitizers(code) {
23893
+ const sanitizers = [];
23894
+ const lines = code.split("\n");
23895
+ const guardOpen = /^\s*if\s+!\s*([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*(?:contains|contains_key)\s*\(/;
23896
+ const allowlistName = /^(?:[A-Z][A-Z0-9_]+|.*?(allowed|accepted|whitelist|permitted|valid|approved).*)$/i;
23897
+ const terminator = /\b(return|Err\s*\(|panic!\s*\(|HttpResponse::(?:Forbidden|BadRequest|Unauthorized))/;
23898
+ for (let i2 = 0; i2 < lines.length; i2++) {
23899
+ const m = guardOpen.exec(lines[i2]);
23900
+ if (!m) continue;
23901
+ const setName = m[1];
23902
+ if (!allowlistName.test(setName)) continue;
23903
+ let depth = 0;
23904
+ for (const ch of lines[i2]) {
23905
+ if (ch === "{") depth++;
23906
+ else if (ch === "}") depth--;
23907
+ }
23908
+ if (depth <= 0) continue;
23909
+ let closeLine = -1;
23910
+ let bodyHasTerminator = false;
23911
+ const maxScan = Math.min(lines.length, i2 + 26);
23912
+ for (let j = i2 + 1; j < maxScan; j++) {
23913
+ const line = lines[j];
23914
+ if (terminator.test(line)) bodyHasTerminator = true;
23915
+ for (const ch of line) {
23916
+ if (ch === "{") depth++;
23917
+ else if (ch === "}") depth--;
23918
+ }
23919
+ if (depth === 0) {
23920
+ closeLine = j;
23921
+ break;
23922
+ }
23923
+ }
23924
+ if (closeLine === -1 || !bodyHasTerminator) continue;
23925
+ for (let l = closeLine + 2; l <= lines.length; l++) {
23926
+ sanitizers.push({
23927
+ type: "rust_set_allowlist_guard",
23928
+ method: "if",
23929
+ line: l,
23930
+ sanitizes: [
23931
+ "ssrf",
23932
+ "open_redirect",
23933
+ "command_injection",
23934
+ "external_taint_escape"
23935
+ ]
23936
+ });
23937
+ }
23938
+ }
23939
+ return sanitizers;
23940
+ }
23941
+ function findRustCanonicalizeGuardSanitizers(code) {
23942
+ const sanitizers = [];
23943
+ const lines = code.split("\n");
23944
+ const guardOpen = /^\s*if\s+!\s*[A-Za-z_][\w?.()&]*\.starts_with\s*\(/;
23945
+ const terminator = /\b(return|Err\s*\(|panic!\s*\(|HttpResponse::(?:Forbidden|BadRequest|Unauthorized|NotFound))/;
23946
+ for (let i2 = 0; i2 < lines.length; i2++) {
23947
+ if (!guardOpen.test(lines[i2])) continue;
23948
+ let depth = 0;
23949
+ for (const ch of lines[i2]) {
23950
+ if (ch === "{") depth++;
23951
+ else if (ch === "}") depth--;
23952
+ }
23953
+ if (depth <= 0) continue;
23954
+ let closeLine = -1;
23955
+ let bodyHasTerminator = false;
23956
+ const maxScan = Math.min(lines.length, i2 + 26);
23957
+ for (let j = i2 + 1; j < maxScan; j++) {
23958
+ const line = lines[j];
23959
+ if (terminator.test(line)) bodyHasTerminator = true;
23960
+ for (const ch of line) {
23961
+ if (ch === "{") depth++;
23962
+ else if (ch === "}") depth--;
23963
+ }
23964
+ if (depth === 0) {
23965
+ closeLine = j;
23966
+ break;
23967
+ }
23968
+ }
23969
+ if (closeLine === -1 || !bodyHasTerminator) continue;
23970
+ for (let l = closeLine + 2; l <= lines.length; l++) {
23971
+ sanitizers.push({
23972
+ type: "rust_canonicalize_guard",
23973
+ method: "if",
23974
+ line: l,
23975
+ sanitizes: [
23976
+ "path_traversal",
23977
+ "xss",
23978
+ "ssrf",
23979
+ "external_taint_escape"
23980
+ ]
23981
+ });
23982
+ }
23983
+ }
23984
+ return sanitizers;
23985
+ }
23754
23986
 
23755
23987
  // src/analysis/passes/sink-filter-pass.ts
23756
23988
  var JS_XSS_SANITIZERS = [
@@ -28613,6 +28845,11 @@ var TEST_FILENAME_RE = /(?:\.(?:test|spec)\.[cm]?[jt]sx?|_test\.go|_test\.py|Tes
28613
28845
  function isTestFile(file) {
28614
28846
  return TEST_PATH_RE3.test(file) || TEST_FILENAME_RE.test(file);
28615
28847
  }
28848
+ var GENERATED_PATH_RE = /(?:^|[\\/])(?:gen|generated|build[\\/]generated|src[\\/](?:main|test)[\\/]generated|target[\\/]generated-sources|target[\\/]generated-test-sources|node_modules[\\/]\.cache)(?:[\\/]|$)/i;
28849
+ var GENERATED_FILENAME_RE = /__[ch]\.java$|\.pb\.go$|_pb2\.py$|\.generated\.[cm]?[jt]sx?$/i;
28850
+ function isGeneratedFile(file) {
28851
+ return GENERATED_PATH_RE.test(file) || GENERATED_FILENAME_RE.test(file);
28852
+ }
28616
28853
  var PROVIDER_PATTERNS = [
28617
28854
  {
28618
28855
  name: "AWS access key",
@@ -28779,6 +29016,117 @@ function shannonEntropy(s) {
28779
29016
  return h;
28780
29017
  }
28781
29018
  var CREDENTIAL_NAME_RE = /(?:key|secret|token|password|passwd|credential|api[_-]?key)/i;
29019
+ function findAnnotationLineRanges(code) {
29020
+ const lines = code.split("\n");
29021
+ const inAnnotation = /* @__PURE__ */ new Set();
29022
+ const OPEN_RE = /(?:@[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*\s*\(|#\[)/g;
29023
+ for (let i2 = 0; i2 < lines.length; i2++) {
29024
+ OPEN_RE.lastIndex = 0;
29025
+ let m;
29026
+ while ((m = OPEN_RE.exec(lines[i2])) !== null) {
29027
+ const isRustAttr = m[0].startsWith("#[");
29028
+ const openCh = isRustAttr ? "[" : "(";
29029
+ const closeCh = isRustAttr ? "]" : ")";
29030
+ let depth = 1;
29031
+ let li = i2;
29032
+ let col = m.index + m[0].length;
29033
+ let lineBudget = 200;
29034
+ inAnnotation.add(li + 1);
29035
+ while (depth > 0 && li < lines.length && lineBudget > 0) {
29036
+ const ln = lines[li];
29037
+ let inStr = null;
29038
+ while (col < ln.length && depth > 0) {
29039
+ const ch = ln[col];
29040
+ if (inStr !== null) {
29041
+ if (ch === "\\") {
29042
+ col += 2;
29043
+ continue;
29044
+ }
29045
+ if (ch === inStr) inStr = null;
29046
+ } else if (ch === '"' || ch === "'" || ch === "`") {
29047
+ inStr = ch;
29048
+ } else if (ch === openCh) {
29049
+ depth++;
29050
+ } else if (ch === closeCh) {
29051
+ depth--;
29052
+ }
29053
+ col++;
29054
+ }
29055
+ if (depth > 0) {
29056
+ li++;
29057
+ col = 0;
29058
+ lineBudget--;
29059
+ if (li < lines.length) inAnnotation.add(li + 1);
29060
+ }
29061
+ }
29062
+ }
29063
+ }
29064
+ return inAnnotation;
29065
+ }
29066
+ function findStringArrayLineRanges(code) {
29067
+ const lines = code.split("\n");
29068
+ const inArray = /* @__PURE__ */ new Set();
29069
+ const OPEN_RE = /=\s*([{\[])/g;
29070
+ const STR_LITERAL_COUNT_RE = /(["'`])(?:\\.|(?!\1).)*\1/g;
29071
+ for (let i2 = 0; i2 < lines.length; i2++) {
29072
+ OPEN_RE.lastIndex = 0;
29073
+ let m;
29074
+ while ((m = OPEN_RE.exec(lines[i2])) !== null) {
29075
+ const openCh = m[1];
29076
+ const closeCh = openCh === "{" ? "}" : "]";
29077
+ let depth = 1;
29078
+ let li = i2;
29079
+ let col = m.index + m[0].length;
29080
+ let lineBudget = 500;
29081
+ const spanLines = [li + 1];
29082
+ let spanText = "";
29083
+ while (depth > 0 && li < lines.length && lineBudget > 0) {
29084
+ const ln = lines[li];
29085
+ let inStr = null;
29086
+ const start2 = col;
29087
+ while (col < ln.length && depth > 0) {
29088
+ const ch = ln[col];
29089
+ if (inStr !== null) {
29090
+ if (ch === "\\") {
29091
+ col += 2;
29092
+ continue;
29093
+ }
29094
+ if (ch === inStr) inStr = null;
29095
+ } else if (ch === '"' || ch === "'" || ch === "`") {
29096
+ inStr = ch;
29097
+ } else if (ch === openCh) {
29098
+ depth++;
29099
+ } else if (ch === closeCh) {
29100
+ depth--;
29101
+ }
29102
+ col++;
29103
+ }
29104
+ spanText += ln.substring(start2, col) + "\n";
29105
+ if (depth > 0) {
29106
+ li++;
29107
+ col = 0;
29108
+ lineBudget--;
29109
+ if (li < lines.length) spanLines.push(li + 1);
29110
+ }
29111
+ }
29112
+ STR_LITERAL_COUNT_RE.lastIndex = 0;
29113
+ let strCount = 0;
29114
+ while (STR_LITERAL_COUNT_RE.exec(spanText) !== null) {
29115
+ strCount++;
29116
+ if (strCount >= 3) break;
29117
+ }
29118
+ if (strCount >= 3) {
29119
+ for (const ln of spanLines) inArray.add(ln);
29120
+ }
29121
+ }
29122
+ }
29123
+ return inArray;
29124
+ }
29125
+ var FIELD_ASSIGN_RE = /(?:^|[\s,(])([A-Za-z_$][\w$]*)\s*[:=]\s*["'`]/;
29126
+ function extractEnclosingFieldName(lineText) {
29127
+ const m = FIELD_ASSIGN_RE.exec(lineText);
29128
+ return m ? m[1] : null;
29129
+ }
28782
29130
  var TEST_CALL_RE = /\b(?:expect|assert|describe|it|test)\s*\(/;
28783
29131
  var COMMENT_EXAMPLE_RE = /(?:\/\/|#)\s*(?:example|sample|test|fixture)/i;
28784
29132
  var ScanSecretsPass = class {
@@ -28786,7 +29134,7 @@ var ScanSecretsPass = class {
28786
29134
  category = "security";
28787
29135
  run(ctx) {
28788
29136
  const file = ctx.graph.ir.meta.file;
28789
- if (isTestFile(file)) {
29137
+ if (isTestFile(file) || isGeneratedFile(file)) {
28790
29138
  return { providerFindings: 0, entropyFindings: 0 };
28791
29139
  }
28792
29140
  const lines = ctx.code.split("\n");
@@ -28798,6 +29146,8 @@ var ScanSecretsPass = class {
28798
29146
  seen.add(`${f.line}:${f.rule_id}`);
28799
29147
  }
28800
29148
  }
29149
+ const annotationLines = findAnnotationLineRanges(ctx.code);
29150
+ const arrayLines = findStringArrayLineRanges(ctx.code);
28801
29151
  let providerFindings = 0;
28802
29152
  let entropyFindings = 0;
28803
29153
  for (let i2 = 0; i2 < lines.length; i2++) {
@@ -28858,11 +29208,14 @@ var ScanSecretsPass = class {
28858
29208
  const lineNum = i2 + 1;
28859
29209
  if (TEST_CALL_RE.test(lineText)) continue;
28860
29210
  if (COMMENT_EXAMPLE_RE.test(lineText)) continue;
29211
+ if (annotationLines.has(lineNum)) continue;
29212
+ if (arrayLines.has(lineNum)) continue;
28861
29213
  STRING_LITERAL_RE.lastIndex = 0;
28862
29214
  let match;
28863
29215
  while ((match = STRING_LITERAL_RE.exec(lineText)) !== null) {
28864
29216
  const value = match[2];
28865
29217
  if (!this.isCandidate(value)) continue;
29218
+ if (value.length < 32) continue;
28866
29219
  if (!this.passesEntropyGate(value, lineText)) continue;
28867
29220
  const key = `${lineNum}:hardcoded-credential-entropy`;
28868
29221
  if (seen.has(key)) continue;
@@ -28900,17 +29253,25 @@ var ScanSecretsPass = class {
28900
29253
  return true;
28901
29254
  }
28902
29255
  /**
28903
- * Shannon-entropy gate. Base64-shaped strings need higher entropy than
28904
- * hex-shaped (hex alphabet is 4 bits/char by construction). When the
28905
- * surrounding line contains a credential-shaped variable name, both
28906
- * thresholds drop by 0.2 bits/char.
29256
+ * Shannon-entropy gate (#125 Gate 4 REQUIRED field-name match).
29257
+ *
29258
+ * The entropy layer emits ONLY when the enclosing assignment LHS
29259
+ * identifier matches a credential keyword (password / secret / token /
29260
+ * api_key / etc.). Without this requirement, the layer flagged every
29261
+ * high-entropy string — attribution keys, base64 resource blobs, public
29262
+ * encoding alphabets — as credentials. Provider patterns (Layer 1) and
29263
+ * named-credential matcher (Layer 1b) remain the recall safety net for
29264
+ * credentials that don't fit the `FIELD = "..."` shape.
29265
+ *
29266
+ * Base64-shaped strings need higher entropy than hex-shaped (hex alphabet
29267
+ * is 4 bits/char by construction).
28907
29268
  */
28908
29269
  passesEntropyGate(value, lineText) {
29270
+ const fieldName = extractEnclosingFieldName(lineText);
29271
+ if (fieldName === null || !CREDENTIAL_NAME_RE.test(fieldName)) return false;
28909
29272
  const isHex = HEXISH_RE.test(value);
28910
- const boost = CREDENTIAL_NAME_RE.test(lineText) ? 0.2 : 0;
28911
- const threshold = isHex ? 3.5 - boost : 4.3 - boost;
28912
- const h = shannonEntropy(value);
28913
- return h >= threshold;
29273
+ const threshold = isHex ? 3.3 : 4.1;
29274
+ return shannonEntropy(value) >= threshold;
28914
29275
  }
28915
29276
  };
28916
29277
 
@@ -12503,6 +12503,55 @@ function isSafeGoExecCommandCall(call, pattern, language) {
12503
12503
  if (SHELL_PROGRAMS.has(program)) return false;
12504
12504
  return true;
12505
12505
  }
12506
+ function isSafeRustCommandCall(call, pattern, language) {
12507
+ if (language !== "rust") return false;
12508
+ if (pattern.type !== "command_injection") return false;
12509
+ if (pattern.class !== void 0 && pattern.class !== "Command") return false;
12510
+ const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
12511
+ "sh",
12512
+ "bash",
12513
+ "zsh",
12514
+ "dash",
12515
+ "ash",
12516
+ "ksh",
12517
+ "cmd",
12518
+ "cmd.exe",
12519
+ "powershell",
12520
+ "pwsh",
12521
+ "powershell.exe",
12522
+ "pwsh.exe"
12523
+ ]);
12524
+ const PROGRAM_RE = /\bCommand\s*::\s*new\s*\(\s*(?:r?"([^"]*)"|'([^']*)')/;
12525
+ const extractProgram = (text) => {
12526
+ const m = PROGRAM_RE.exec(text);
12527
+ if (!m) return null;
12528
+ const lit = m[1] ?? m[2] ?? "";
12529
+ return lit.split("/").pop() ?? lit;
12530
+ };
12531
+ if (pattern.method === "new") {
12532
+ const programArg = call.arguments.find((a) => a.position === 0);
12533
+ if (!programArg) return false;
12534
+ let program;
12535
+ if (programArg.literal !== null && programArg.literal !== void 0) {
12536
+ program = String(programArg.literal).split("/").pop() ?? String(programArg.literal);
12537
+ } else {
12538
+ const expr = (programArg.expression ?? "").trim();
12539
+ if (!(expr.startsWith('"') || expr.startsWith("'"))) {
12540
+ return false;
12541
+ }
12542
+ const stripped = expr.slice(1, -1);
12543
+ program = stripped.split("/").pop() ?? stripped;
12544
+ }
12545
+ return !SHELL_PROGRAMS.has(program);
12546
+ }
12547
+ if (pattern.method === "arg" || pattern.method === "args" || pattern.method === "spawn" || pattern.method === "output") {
12548
+ const receiverText = call.receiver ?? "";
12549
+ const program = extractProgram(receiverText);
12550
+ if (program === null) return false;
12551
+ return !SHELL_PROGRAMS.has(program);
12552
+ }
12553
+ return false;
12554
+ }
12506
12555
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
12507
12556
  function argIsClassLiteral(call, position) {
12508
12557
  const arg = call.arguments.find((a) => a.position === position);
@@ -12525,6 +12574,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12525
12574
  if (isSafeGoExecCommandCall(call, pattern, language)) {
12526
12575
  continue;
12527
12576
  }
12577
+ if (isSafeRustCommandCall(call, pattern, language)) {
12578
+ continue;
12579
+ }
12528
12580
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
12529
12581
  continue;
12530
12582
  }
@@ -12437,6 +12437,55 @@ function isSafeGoExecCommandCall(call, pattern, language) {
12437
12437
  if (SHELL_PROGRAMS.has(program)) return false;
12438
12438
  return true;
12439
12439
  }
12440
+ function isSafeRustCommandCall(call, pattern, language) {
12441
+ if (language !== "rust") return false;
12442
+ if (pattern.type !== "command_injection") return false;
12443
+ if (pattern.class !== void 0 && pattern.class !== "Command") return false;
12444
+ const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
12445
+ "sh",
12446
+ "bash",
12447
+ "zsh",
12448
+ "dash",
12449
+ "ash",
12450
+ "ksh",
12451
+ "cmd",
12452
+ "cmd.exe",
12453
+ "powershell",
12454
+ "pwsh",
12455
+ "powershell.exe",
12456
+ "pwsh.exe"
12457
+ ]);
12458
+ const PROGRAM_RE = /\bCommand\s*::\s*new\s*\(\s*(?:r?"([^"]*)"|'([^']*)')/;
12459
+ const extractProgram = (text) => {
12460
+ const m = PROGRAM_RE.exec(text);
12461
+ if (!m) return null;
12462
+ const lit = m[1] ?? m[2] ?? "";
12463
+ return lit.split("/").pop() ?? lit;
12464
+ };
12465
+ if (pattern.method === "new") {
12466
+ const programArg = call.arguments.find((a) => a.position === 0);
12467
+ if (!programArg) return false;
12468
+ let program;
12469
+ if (programArg.literal !== null && programArg.literal !== void 0) {
12470
+ program = String(programArg.literal).split("/").pop() ?? String(programArg.literal);
12471
+ } else {
12472
+ const expr = (programArg.expression ?? "").trim();
12473
+ if (!(expr.startsWith('"') || expr.startsWith("'"))) {
12474
+ return false;
12475
+ }
12476
+ const stripped = expr.slice(1, -1);
12477
+ program = stripped.split("/").pop() ?? stripped;
12478
+ }
12479
+ return !SHELL_PROGRAMS.has(program);
12480
+ }
12481
+ if (pattern.method === "arg" || pattern.method === "args" || pattern.method === "spawn" || pattern.method === "output") {
12482
+ const receiverText = call.receiver ?? "";
12483
+ const program = extractProgram(receiverText);
12484
+ if (program === null) return false;
12485
+ return !SHELL_PROGRAMS.has(program);
12486
+ }
12487
+ return false;
12488
+ }
12440
12489
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
12441
12490
  function argIsClassLiteral(call, position) {
12442
12491
  const arg = call.arguments.find((a) => a.position === position);
@@ -12459,6 +12508,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12459
12508
  if (isSafeGoExecCommandCall(call, pattern, language)) {
12460
12509
  continue;
12461
12510
  }
12511
+ if (isSafeRustCommandCall(call, pattern, language)) {
12512
+ continue;
12513
+ }
12462
12514
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
12463
12515
  continue;
12464
12516
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circle-ir",
3
- "version": "3.83.0",
3
+ "version": "3.85.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",