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.
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +11 -4
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.js +10 -5
- package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.js +88 -0
- package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
- package/dist/analysis/passes/weak-crypto-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-crypto-pass.js +24 -5
- package/dist/analysis/passes/weak-crypto-pass.js.map +1 -1
- package/dist/analysis/passes/weak-hash-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-hash-pass.js +117 -5
- package/dist/analysis/passes/weak-hash-pass.js.map +1 -1
- package/dist/browser/circle-ir.js +128 -12
- package/dist/core/circle-ir-core.cjs +11 -4
- package/dist/core/circle-ir-core.js +11 -4
- package/package.json +1 -1
|
@@ -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)
|
|
11740
|
-
//
|
|
11741
|
-
|
|
11742
|
-
|
|
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
|
-
|
|
29145
|
-
const
|
|
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 =
|
|
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
|
|
29548
|
-
|
|
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)
|
|
11122
|
-
//
|
|
11123
|
-
|
|
11124
|
-
|
|
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)
|
|
11056
|
-
//
|
|
11057
|
-
|
|
11058
|
-
|
|
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.
|
|
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",
|