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.
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +291 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts +12 -4
- package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.js +207 -9
- package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +96 -0
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/browser/circle-ir.js +370 -9
- package/dist/core/circle-ir-core.cjs +52 -0
- package/dist/core/circle-ir-core.js +52 -0
- package/package.json +1 -1
|
@@ -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
|
|
28904
|
-
*
|
|
28905
|
-
*
|
|
28906
|
-
*
|
|
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
|
|
28911
|
-
|
|
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.
|
|
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",
|