circle-ir 3.80.0 → 3.82.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 (39) hide show
  1. package/configs/sinks/xss.yaml +2 -1
  2. package/dist/analysis/config-loader.d.ts.map +1 -1
  3. package/dist/analysis/config-loader.js +26 -4
  4. package/dist/analysis/config-loader.js.map +1 -1
  5. package/dist/analysis/passes/_credential-helpers.d.ts +40 -0
  6. package/dist/analysis/passes/_credential-helpers.d.ts.map +1 -0
  7. package/dist/analysis/passes/_credential-helpers.js +152 -0
  8. package/dist/analysis/passes/_credential-helpers.js.map +1 -0
  9. package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts +42 -0
  10. package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts.map +1 -0
  11. package/dist/analysis/passes/cleartext-credential-transport-pass.js +196 -0
  12. package/dist/analysis/passes/cleartext-credential-transport-pass.js.map +1 -0
  13. package/dist/analysis/passes/info-disclosure-stacktrace-pass.d.ts +48 -0
  14. package/dist/analysis/passes/info-disclosure-stacktrace-pass.d.ts.map +1 -0
  15. package/dist/analysis/passes/info-disclosure-stacktrace-pass.js +222 -0
  16. package/dist/analysis/passes/info-disclosure-stacktrace-pass.js.map +1 -0
  17. package/dist/analysis/passes/plaintext-password-storage-pass.d.ts +47 -0
  18. package/dist/analysis/passes/plaintext-password-storage-pass.d.ts.map +1 -0
  19. package/dist/analysis/passes/plaintext-password-storage-pass.js +159 -0
  20. package/dist/analysis/passes/plaintext-password-storage-pass.js.map +1 -0
  21. package/dist/analysis/passes/unrestricted-file-upload-pass.d.ts +46 -0
  22. package/dist/analysis/passes/unrestricted-file-upload-pass.d.ts.map +1 -0
  23. package/dist/analysis/passes/unrestricted-file-upload-pass.js +193 -0
  24. package/dist/analysis/passes/unrestricted-file-upload-pass.js.map +1 -0
  25. package/dist/analysis/passes/weak-password-encoding-pass.d.ts +40 -0
  26. package/dist/analysis/passes/weak-password-encoding-pass.d.ts.map +1 -0
  27. package/dist/analysis/passes/weak-password-encoding-pass.js +157 -0
  28. package/dist/analysis/passes/weak-password-encoding-pass.js.map +1 -0
  29. package/dist/analysis/passes/weak-password-hash-pass.d.ts +49 -0
  30. package/dist/analysis/passes/weak-password-hash-pass.d.ts.map +1 -0
  31. package/dist/analysis/passes/weak-password-hash-pass.js +225 -0
  32. package/dist/analysis/passes/weak-password-hash-pass.js.map +1 -0
  33. package/dist/analyzer.d.ts.map +1 -1
  34. package/dist/analyzer.js +18 -0
  35. package/dist/analyzer.js.map +1 -1
  36. package/dist/browser/circle-ir.js +912 -4
  37. package/dist/core/circle-ir-core.cjs +26 -4
  38. package/dist/core/circle-ir-core.js +26 -4
  39. package/package.json +1 -1
@@ -11377,7 +11377,12 @@ var DEFAULT_SINKS = [
11377
11377
  // Class-less XSS patterns for cases where receiver type is inferred
11378
11378
  { method: "println", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [0] },
11379
11379
  { method: "print", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [0] },
11380
- { method: "write", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [0] },
11380
+ // NOTE: the unscoped { method: 'write', type: 'xss' } entry was removed in
11381
+ // Sprint 28 (#110). It mistyped every non-XSS .write() across all languages
11382
+ // (fs.writeFile, open().write, bcrypt callbacks, credential file writes,
11383
+ // node ClientRequest.write, etc.) as xss. Real HTML writers are covered
11384
+ // by class-scoped entries: PrintWriter.write (line 843), ServletOutputStream.write
11385
+ // (line 849), JspWriter.write (xss.yaml), Response.write (nodejs.json).
11381
11386
  { method: "append", class: "StringBuilder", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [0] },
11382
11387
  { method: "append", class: "StringBuffer", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [0] },
11383
11388
  // Wiki/CMS XSS sinks (JSPWiki, Confluence, etc.)
@@ -12574,9 +12579,26 @@ var DEFAULT_SANITIZERS = [
12574
12579
  // JSON.parse (data is validated against JSON grammar, prevents XSS/code injection)
12575
12580
  { method: "parse", class: "JSON", removes: ["xss", "code_injection"] },
12576
12581
  // Type coercion (removes string-based injections)
12577
- { method: "parseInt", removes: ["sql_injection", "nosql_injection", "command_injection", "xss"] },
12578
- { method: "parseFloat", removes: ["sql_injection", "nosql_injection", "command_injection"] },
12579
- { method: "Number", removes: ["sql_injection", "nosql_injection", "command_injection"] },
12582
+ // Sprint 29 (#113): include external_taint_escape a numeric cast cannot
12583
+ // carry an unvalidated string payload across a function boundary.
12584
+ { method: "parseInt", removes: ["sql_injection", "nosql_injection", "command_injection", "xss", "external_taint_escape", "path_traversal", "code_injection"] },
12585
+ { method: "parseFloat", removes: ["sql_injection", "nosql_injection", "command_injection", "external_taint_escape", "path_traversal", "code_injection"] },
12586
+ { method: "Number", removes: ["sql_injection", "nosql_injection", "command_injection", "external_taint_escape", "path_traversal", "code_injection"] },
12587
+ // Sprint 29 (#113): bounds-clamp Math.min / Math.max — when used to bound
12588
+ // a numeric/size value (e.g. `Math.min(size, MAX_BYTES)`), the result is
12589
+ // safely bounded and cannot resource-exhaust downstream. Only suppress
12590
+ // external_taint_escape — these helpers do NOT sanitize string injection.
12591
+ { method: "min", class: "Math", removes: ["external_taint_escape"] },
12592
+ { method: "max", class: "Math", removes: ["external_taint_escape"] },
12593
+ // Sprint 29 (#113): allow-list / membership guards — when an external value
12594
+ // is tested against an allow-list (`ALLOWED.includes(x)`, `set.has(x)`,
12595
+ // `list.contains(x)`) before being forwarded, it cannot escape unbounded.
12596
+ // Only suppress `external_taint_escape`; real string-injection sinks should
12597
+ // still rely on their own escaping.
12598
+ { method: "includes", removes: ["external_taint_escape"] },
12599
+ { method: "has", removes: ["external_taint_escape"] },
12600
+ { method: "contains", removes: ["external_taint_escape"] },
12601
+ { method: "indexOf", removes: ["external_taint_escape"] },
12580
12602
  // Path sanitization
12581
12603
  { method: "basename", class: "path", removes: ["path_traversal"] },
12582
12604
  { method: "normalize", class: "path", removes: ["path_traversal"] },
@@ -29981,6 +30003,886 @@ var WeakRandomPass = class {
29981
30003
  }
29982
30004
  };
29983
30005
 
30006
+ // src/analysis/passes/_credential-helpers.ts
30007
+ var CRED_KEYWORD_RE2 = /(?:password|passwd|pwd|secret|api[_-]?key|auth[_-]?token|private[_-]?key|access[_-]?key|credential)/i;
30008
+ function isCredentialIdentifier(name2) {
30009
+ if (!name2) return false;
30010
+ if (name2.length < 3) return false;
30011
+ return CRED_KEYWORD_RE2.test(name2);
30012
+ }
30013
+ function argLooksLikeCredential(arg) {
30014
+ if (!arg) return false;
30015
+ if (arg.variable && isCredentialIdentifier(arg.variable)) return true;
30016
+ const expr = (arg.expression ?? "").trim();
30017
+ if (!expr) return false;
30018
+ const head = expr.split(/[.\s(]/, 1)[0] ?? "";
30019
+ return isCredentialIdentifier(head);
30020
+ }
30021
+ function stripQuotes6(s) {
30022
+ const t = s.trim();
30023
+ if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'") || t.startsWith("`") && t.endsWith("`")) {
30024
+ return t.slice(1, -1);
30025
+ }
30026
+ return t;
30027
+ }
30028
+ function literalAt(call, position) {
30029
+ const arg = call.arguments.find((a) => a.position === position);
30030
+ if (!arg) return null;
30031
+ const raw = arg.literal ?? arg.expression ?? "";
30032
+ const trimmed = raw.trim();
30033
+ if (trimmed.startsWith('"') || trimmed.startsWith("'") || trimmed.startsWith("`")) {
30034
+ return stripQuotes6(trimmed);
30035
+ }
30036
+ if (arg.literal) return stripQuotes6(arg.literal);
30037
+ return null;
30038
+ }
30039
+ function isHashFunctionCall(call) {
30040
+ const method = call.method_name ?? "";
30041
+ const receiver = call.receiver ?? "";
30042
+ const recvLower = receiver.toLowerCase();
30043
+ if (recvLower === "bcrypt" || recvLower.endsWith(".bcrypt")) {
30044
+ return method === "hashpw" || method === "hash" || method === "hashSync" || method === "GenerateFromPassword" || method === "generate_password_hash";
30045
+ }
30046
+ if (recvLower === "argon2" || recvLower.endsWith(".argon2")) {
30047
+ return method === "hash" || method === "Hash" || method === "PasswordHash";
30048
+ }
30049
+ if (recvLower === "hashlib") return true;
30050
+ if (recvLower === "passlib" || recvLower.includes("passlib.hash")) return true;
30051
+ if (method === "PBKDF2HMAC" || method === "derive") return true;
30052
+ if (recvLower === "crypto") {
30053
+ return method === "createHash" || method === "createHmac" || method === "pbkdf2" || method === "pbkdf2Sync" || method === "scrypt" || method === "scryptSync";
30054
+ }
30055
+ if (receiver === "MessageDigest" || receiver.endsWith(".MessageDigest")) {
30056
+ return method === "getInstance" || method === "update" || method === "digest";
30057
+ }
30058
+ if (receiver === "DigestUtils" || receiver.endsWith(".DigestUtils")) {
30059
+ return true;
30060
+ }
30061
+ if (receiver === "SecretKeyFactory" || receiver.endsWith(".SecretKeyFactory")) {
30062
+ return method === "getInstance" || method === "generateSecret";
30063
+ }
30064
+ if (method === "PBEKeySpec") return true;
30065
+ if (receiver === "md5" || receiver === "sha1" || receiver === "sha256" || receiver === "sha512" || receiver === "sha3" || receiver.endsWith("/md5") || receiver.endsWith("/sha1") || receiver.endsWith("/sha256") || receiver.endsWith("/sha512")) {
30066
+ return method === "New" || method === "Sum" || method === "New224" || method === "New384";
30067
+ }
30068
+ const m = method.toLowerCase();
30069
+ if (m === "hash" || m === "hashpw" || m === "hashsync" || m === "pbkdf2" || m === "pbkdf2sync" || m === "scrypt" || m === "scryptsync") return true;
30070
+ return false;
30071
+ }
30072
+ function priorHashOf(varName, priorCalls) {
30073
+ for (const c of priorCalls) {
30074
+ if (!isHashFunctionCall(c)) continue;
30075
+ for (const a of c.arguments) {
30076
+ if (a.variable === varName) return true;
30077
+ const head = (a.expression ?? "").trim().split(/[.\s(]/, 1)[0];
30078
+ if (head === varName) return true;
30079
+ }
30080
+ }
30081
+ return false;
30082
+ }
30083
+
30084
+ // src/analysis/passes/weak-password-hash-pass.ts
30085
+ var FAST_HASH_NAMES = /* @__PURE__ */ new Set([
30086
+ "sha224",
30087
+ "sha-224",
30088
+ "sha256",
30089
+ "sha-256",
30090
+ "sha384",
30091
+ "sha-384",
30092
+ "sha512",
30093
+ "sha-512",
30094
+ "sha3",
30095
+ "sha-3",
30096
+ "sha3-256",
30097
+ "sha3-512"
30098
+ // MD/SHA1 are covered by weak-hash; not duplicating here.
30099
+ ]);
30100
+ var BCRYPT_MIN_COST = 10;
30101
+ var PBKDF2_MIN_ITERATIONS = 1e5;
30102
+ function intLiteral(s) {
30103
+ if (s == null) return null;
30104
+ const t = s.trim();
30105
+ if (!/^-?\d+$/.test(t)) return null;
30106
+ const n = parseInt(t, 10);
30107
+ return Number.isFinite(n) ? n : null;
30108
+ }
30109
+ function pyKwargInt(call, name2) {
30110
+ for (const a of call.arguments) {
30111
+ const expr = (a.expression ?? "").trim();
30112
+ const m = expr.match(new RegExp(`^${name2}\\s*=\\s*(-?\\d+)$`));
30113
+ if (m) return parseInt(m[1], 10);
30114
+ }
30115
+ return null;
30116
+ }
30117
+ var WeakPasswordHashPass = class {
30118
+ name = "weak-password-hash";
30119
+ category = "security";
30120
+ run(ctx) {
30121
+ const { graph, language } = ctx;
30122
+ const file = graph.ir.meta.file;
30123
+ const findings = [];
30124
+ for (const call of graph.ir.calls) {
30125
+ const detection = this.detect(call, language);
30126
+ if (!detection) continue;
30127
+ const { kind, api } = detection;
30128
+ const line = call.location.line;
30129
+ findings.push({ line, language, kind, api });
30130
+ const message = kind === "fast-unsalted-hash" ? `Fast/unsalted hash \`${api}\` applied to a password. General-purpose hashes (SHA-256/512) are unsuitable for password storage.` : kind === "low-bcrypt-cost" ? `bcrypt called with insufficient cost factor (< ${BCRYPT_MIN_COST}).` : `PBKDF2 called with insufficient iteration count (< ${PBKDF2_MIN_ITERATIONS}).`;
30131
+ ctx.addFinding({
30132
+ id: `${this.name}-${file}-${line}`,
30133
+ pass: this.name,
30134
+ category: this.category,
30135
+ rule_id: this.name,
30136
+ cwe: "CWE-916",
30137
+ severity: "high",
30138
+ level: "warning",
30139
+ message,
30140
+ file,
30141
+ line,
30142
+ fix: "Use a memory-hard password-hashing function with appropriate cost: Argon2id (recommended), bcrypt (cost \u2265 12), scrypt, or PBKDF2 with \u2265 600k iterations.",
30143
+ evidence: { kind, api, language }
30144
+ });
30145
+ }
30146
+ return { findings };
30147
+ }
30148
+ detect(call, language) {
30149
+ const method = call.method_name ?? "";
30150
+ const receiver = call.receiver ?? "";
30151
+ const recvLower = receiver.toLowerCase();
30152
+ if (recvLower === "bcrypt" || recvLower.endsWith(".bcrypt")) {
30153
+ if (method === "gensalt") {
30154
+ const rounds = pyKwargInt(call, "rounds");
30155
+ if (rounds !== null && rounds < BCRYPT_MIN_COST) {
30156
+ return { kind: "low-bcrypt-cost", api: "bcrypt.gensalt" };
30157
+ }
30158
+ }
30159
+ if (method === "hash" || method === "hashSync") {
30160
+ const cost = intLiteral(literalAt(call, 1));
30161
+ if (cost !== null && cost < BCRYPT_MIN_COST) {
30162
+ return { kind: "low-bcrypt-cost", api: `bcrypt.${method}` };
30163
+ }
30164
+ }
30165
+ if (method === "GenerateFromPassword") {
30166
+ const arg1 = call.arguments.find((a) => a.position === 1);
30167
+ const expr = (arg1?.expression ?? "").trim();
30168
+ const n = intLiteral(expr);
30169
+ if (n !== null && n < BCRYPT_MIN_COST) {
30170
+ return { kind: "low-bcrypt-cost", api: "bcrypt.GenerateFromPassword" };
30171
+ }
30172
+ if (expr === "bcrypt.MinCost") {
30173
+ return { kind: "low-bcrypt-cost", api: "bcrypt.GenerateFromPassword" };
30174
+ }
30175
+ }
30176
+ }
30177
+ if (method === "PBKDF2HMAC") {
30178
+ const iters = pyKwargInt(call, "iterations");
30179
+ if (iters !== null && iters < PBKDF2_MIN_ITERATIONS) {
30180
+ return { kind: "low-pbkdf2-iterations", api: "PBKDF2HMAC" };
30181
+ }
30182
+ }
30183
+ if ((method === "pbkdf2" || method === "pbkdf2Sync") && (recvLower === "crypto" || recvLower.endsWith(".crypto"))) {
30184
+ const iters = intLiteral(literalAt(call, 2));
30185
+ if (iters !== null && iters < PBKDF2_MIN_ITERATIONS) {
30186
+ return { kind: "low-pbkdf2-iterations", api: `crypto.${method}` };
30187
+ }
30188
+ }
30189
+ if (method === "PBEKeySpec" && language === "java") {
30190
+ const iters = intLiteral(literalAt(call, 2));
30191
+ if (iters !== null && iters < PBKDF2_MIN_ITERATIONS) {
30192
+ return { kind: "low-pbkdf2-iterations", api: "PBEKeySpec" };
30193
+ }
30194
+ }
30195
+ if (language === "python" && (recvLower === "hashlib" || recvLower.endsWith(".hashlib"))) {
30196
+ if (FAST_HASH_NAMES.has(method.toLowerCase())) {
30197
+ if (argLooksLikeCredential(call.arguments.find((a) => a.position === 0))) {
30198
+ return { kind: "fast-unsalted-hash", api: `hashlib.${method}` };
30199
+ }
30200
+ }
30201
+ if (method === "new") {
30202
+ const algo = literalAt(call, 0)?.toLowerCase() ?? "";
30203
+ if (FAST_HASH_NAMES.has(algo) && argLooksLikeCredential(call.arguments.find((a) => a.position === 1))) {
30204
+ return { kind: "fast-unsalted-hash", api: `hashlib.new(${algo})` };
30205
+ }
30206
+ }
30207
+ }
30208
+ if ((language === "javascript" || language === "typescript") && method === "update") {
30209
+ const recvExpr = (receiver ?? "").toLowerCase();
30210
+ const hashLike = recvExpr.includes("hash") || recvExpr.includes("createhash") || recvExpr.includes("sha") || recvExpr.includes("md");
30211
+ if (hashLike && argLooksLikeCredential(call.arguments.find((a) => a.position === 0))) {
30212
+ return { kind: "fast-unsalted-hash", api: "crypto.createHash().update" };
30213
+ }
30214
+ }
30215
+ if (language === "java" && method === "update") {
30216
+ const recvName = (receiver ?? "").toLowerCase();
30217
+ const looksLikeDigest = recvName.includes("digest") || recvName.includes("md") || recvName.includes("hash");
30218
+ if (looksLikeDigest && argLooksLikeCredential(call.arguments.find((a) => a.position === 0))) {
30219
+ return { kind: "fast-unsalted-hash", api: "MessageDigest.update" };
30220
+ }
30221
+ }
30222
+ if (language === "go") {
30223
+ const isFastPkg = receiver === "sha256" || receiver === "sha512" || receiver === "sha3" || receiver === "sha224";
30224
+ if (isFastPkg && (method === "Sum256" || method === "Sum512" || method === "Sum224" || method === "Sum384" || method === "Sum")) {
30225
+ const expr = (call.arguments.find((a) => a.position === 0)?.expression ?? "").trim();
30226
+ const inner = expr.replace(/^\[\]byte\s*\(\s*/, "").replace(/\s*\)\s*$/, "");
30227
+ if (argLooksLikeCredential({ position: 0, expression: inner, variable: inner })) {
30228
+ return { kind: "fast-unsalted-hash", api: `${receiver}.${method}` };
30229
+ }
30230
+ }
30231
+ }
30232
+ return null;
30233
+ }
30234
+ };
30235
+
30236
+ // src/analysis/passes/weak-password-encoding-pass.ts
30237
+ function isBasicAuthContext(call, code) {
30238
+ const line = call.location.line;
30239
+ if (line < 1) return false;
30240
+ const lines = code.split("\n");
30241
+ const start2 = Math.max(0, line - 2);
30242
+ const end = Math.min(lines.length, line + 1);
30243
+ const window2 = lines.slice(start2, end).join("\n");
30244
+ return /["'`]Basic\s/i.test(window2);
30245
+ }
30246
+ var WeakPasswordEncodingPass = class {
30247
+ name = "weak-password-encoding";
30248
+ category = "security";
30249
+ run(ctx) {
30250
+ const { graph, language, code } = ctx;
30251
+ const file = graph.ir.meta.file;
30252
+ const findings = [];
30253
+ for (const call of graph.ir.calls) {
30254
+ const api = this.detect(call, language);
30255
+ if (!api) continue;
30256
+ if (isBasicAuthContext(call, code)) continue;
30257
+ const line = call.location.line;
30258
+ findings.push({ line, language, api });
30259
+ ctx.addFinding({
30260
+ id: `${this.name}-${file}-${line}`,
30261
+ pass: this.name,
30262
+ category: this.category,
30263
+ rule_id: this.name,
30264
+ cwe: "CWE-261",
30265
+ severity: "medium",
30266
+ level: "warning",
30267
+ message: `Credential encoded via \`${api}\` \u2014 encoding is NOT encryption. Base64/hex provide no confidentiality; anyone with the encoded value can decode it.`,
30268
+ file,
30269
+ line,
30270
+ fix: "For storage, use a password hash (Argon2id / bcrypt). For transport, use TLS. For symmetric secrecy, use authenticated encryption (AES-GCM).",
30271
+ evidence: { api, language }
30272
+ });
30273
+ }
30274
+ return { findings };
30275
+ }
30276
+ detect(call, language) {
30277
+ const method = call.method_name ?? "";
30278
+ const receiver = call.receiver ?? "";
30279
+ const recvLower = receiver.toLowerCase();
30280
+ const arg0 = call.arguments.find((a) => a.position === 0);
30281
+ if (language === "python") {
30282
+ if (recvLower === "base64" && (method === "b64encode" || method === "urlsafe_b64encode" || method === "standard_b64encode")) {
30283
+ if (argLooksLikeCredential(arg0)) return `base64.${method}`;
30284
+ }
30285
+ if (recvLower === "binascii" && method === "hexlify") {
30286
+ if (argLooksLikeCredential(arg0)) return "binascii.hexlify";
30287
+ }
30288
+ }
30289
+ if (language === "javascript" || language === "typescript") {
30290
+ if (method === "toString") {
30291
+ const encoding = literalAt(call, 0);
30292
+ if (encoding === "base64" || encoding === "hex" || encoding === "base64url") {
30293
+ const recv = (receiver ?? "").toLowerCase();
30294
+ if (recv.includes("buffer.from") && /(?:password|passwd|pwd|secret|api[_-]?key|auth[_-]?token|private[_-]?key|access[_-]?key|credential)/i.test(receiver ?? "")) {
30295
+ return `Buffer.from().toString('${encoding}')`;
30296
+ }
30297
+ }
30298
+ }
30299
+ if (method === "btoa" && receiver === "") {
30300
+ if (argLooksLikeCredential(arg0)) return "btoa";
30301
+ }
30302
+ }
30303
+ if (language === "java") {
30304
+ if (method === "encodeToString") {
30305
+ const recv = (receiver ?? "").toLowerCase();
30306
+ if (recv.includes("encoder") || recv.includes("base64")) {
30307
+ const expr = (arg0?.expression ?? "").trim();
30308
+ const head = expr.split(/[.\s(]/, 1)[0] ?? "";
30309
+ if (argLooksLikeCredential({ position: 0, expression: head, variable: head })) {
30310
+ return "Base64.encodeToString";
30311
+ }
30312
+ }
30313
+ }
30314
+ if (method === "encodeHexString" && (receiver === "Hex" || receiver.endsWith(".Hex"))) {
30315
+ const expr = (arg0?.expression ?? "").trim();
30316
+ const head = expr.split(/[.\s(]/, 1)[0] ?? "";
30317
+ if (argLooksLikeCredential({ position: 0, expression: head, variable: head })) {
30318
+ return "Hex.encodeHexString";
30319
+ }
30320
+ }
30321
+ }
30322
+ if (language === "go") {
30323
+ if (method === "EncodeToString") {
30324
+ const recv = (receiver ?? "").toLowerCase();
30325
+ if (recv.includes("base64") || recv.includes("hex") || recv.includes("encoding")) {
30326
+ const expr = (arg0?.expression ?? "").trim();
30327
+ const inner = expr.replace(/^\[\]byte\s*\(\s*/, "").replace(/\s*\)\s*$/, "");
30328
+ const head = inner.split(/[.\s(]/, 1)[0] ?? "";
30329
+ if (argLooksLikeCredential({ position: 0, expression: head, variable: head })) {
30330
+ return recv.includes("hex") ? "hex.EncodeToString" : "base64.EncodeToString";
30331
+ }
30332
+ }
30333
+ }
30334
+ }
30335
+ return null;
30336
+ }
30337
+ };
30338
+
30339
+ // src/analysis/passes/info-disclosure-stacktrace-pass.ts
30340
+ var RESPONSE_RECEIVER_RE = /^(res|response|w|writer|ctx|c)$/i;
30341
+ var LOGGER_RECEIVER_RE = /^(log|logger|slog|console|pino|winston|sentry)$/i;
30342
+ var RESPONSE_SEND_METHODS = /* @__PURE__ */ new Set([
30343
+ "send",
30344
+ "json",
30345
+ "write",
30346
+ "writeHead",
30347
+ "end",
30348
+ "sendFile",
30349
+ "println",
30350
+ "print",
30351
+ "getWriter",
30352
+ "Fprintln",
30353
+ "Fprintf",
30354
+ "Fprint"
30355
+ ]);
30356
+ function isExceptionExpression(expr) {
30357
+ if (!expr) return false;
30358
+ const e = expr.trim();
30359
+ return /\b(err|error|exc|exception|e|t|throwable)\.(stack|message|toString\(|getMessage\(|getStackTrace\(|getLocalizedMessage\(|getCause\()/i.test(e) || /\btraceback\.(format_exc|format_exception|print_exc)\b/i.test(e) || /\bdebug\.Stack\(\)/.test(e) || /\bstr\(\s*(err|error|exc|exception|e)\s*\)/i.test(e) || /\bString\(\s*(err|error|exc|exception|e)\s*\)/i.test(e);
30360
+ }
30361
+ function argIsException(arg) {
30362
+ if (!arg) return false;
30363
+ if (arg.variable && /^(err|error|exc|exception|e|t|throwable)$/i.test(arg.variable)) {
30364
+ return true;
30365
+ }
30366
+ return isExceptionExpression(arg.expression);
30367
+ }
30368
+ function detectJavaPrintStackTrace(call) {
30369
+ if (call.method_name !== "printStackTrace") return null;
30370
+ const rec = call.receiver ?? "";
30371
+ if (!/^(e|ex|exc|exception|err|error|t|throwable)$/i.test(rec)) return null;
30372
+ const arg0 = call.arguments.find((a) => a.position === 0);
30373
+ if (!arg0) return null;
30374
+ const expr = (arg0.expression ?? arg0.variable ?? "").trim();
30375
+ if (/\bresponse\.getWriter\(\)/.test(expr) || /\bresp\.getWriter\(\)/.test(expr) || /\bout\b/.test(expr) || // common name; conservative
30376
+ /\bgetWriter\(\)/.test(expr)) {
30377
+ return "e.printStackTrace(response.getWriter())";
30378
+ }
30379
+ return null;
30380
+ }
30381
+ function detectResponseLeakCall(call) {
30382
+ const method = call.method_name ?? "";
30383
+ const receiver = call.receiver ?? "";
30384
+ if (!RESPONSE_SEND_METHODS.has(method)) return null;
30385
+ if (LOGGER_RECEIVER_RE.test(receiver)) return null;
30386
+ const recTail = receiver.split(".").pop() ?? receiver;
30387
+ const recHead = receiver.split(".")[0] ?? receiver;
30388
+ if (!RESPONSE_RECEIVER_RE.test(recTail) && !RESPONSE_RECEIVER_RE.test(recHead)) {
30389
+ if (!/(?:^|[.\s])(res|response)\.(?:status|set|header|cookie)\b/i.test(receiver)) {
30390
+ return null;
30391
+ }
30392
+ }
30393
+ for (const a of call.arguments) {
30394
+ if (argIsException(a)) {
30395
+ return `${receiver || ""}${receiver ? "." : ""}${method}(${(a.expression ?? a.variable ?? "").trim()})`;
30396
+ }
30397
+ }
30398
+ return null;
30399
+ }
30400
+ function detectPythonTracebackReturn(ctx) {
30401
+ const out2 = [];
30402
+ const lines = ctx.code.split("\n");
30403
+ for (let i2 = 0; i2 < lines.length; i2++) {
30404
+ const ln = lines[i2] ?? "";
30405
+ if (/\breturn\s+traceback\.format_exc\s*\(\s*\)/.test(ln) || /\breturn\s+\{[^}]*traceback\.format_exc\s*\(\s*\)[^}]*\}/.test(ln) || /\bjsonify\s*\([^)]*traceback\.format_exc\s*\(\s*\)/.test(ln)) {
30406
+ out2.push({ line: i2 + 1, api: "return traceback.format_exc()" });
30407
+ continue;
30408
+ }
30409
+ if (/\breturn\s+(?:str|repr)\s*\(\s*(?:e|err|error|exc|exception)\s*\)/.test(ln)) {
30410
+ const start2 = Math.max(0, i2 - 8);
30411
+ const end = Math.min(lines.length, i2 + 2);
30412
+ const window2 = lines.slice(start2, end).join("\n");
30413
+ if (/@(?:app|router|blueprint)\.(?:route|get|post|put|delete|patch)\b/.test(window2)) {
30414
+ out2.push({ line: i2 + 1, api: "return str(e) in handler" });
30415
+ }
30416
+ }
30417
+ }
30418
+ return out2;
30419
+ }
30420
+ var InfoDisclosureStacktracePass = class {
30421
+ name = "info-disclosure-stacktrace";
30422
+ category = "security";
30423
+ run(ctx) {
30424
+ const { graph, language } = ctx;
30425
+ const file = graph.ir.meta.file;
30426
+ const findings = [];
30427
+ if (language === "python") {
30428
+ for (const f of detectPythonTracebackReturn(ctx)) {
30429
+ findings.push({ line: f.line, api: f.api, language });
30430
+ ctx.addFinding(this.makeFinding(file, f.line, f.api));
30431
+ }
30432
+ }
30433
+ for (const call of graph.ir.calls) {
30434
+ let api = null;
30435
+ if (language === "java") {
30436
+ api = detectJavaPrintStackTrace(call);
30437
+ if (!api) api = detectResponseLeakCall(call);
30438
+ } else if (language === "javascript" || language === "typescript") {
30439
+ api = detectResponseLeakCall(call);
30440
+ } else if (language === "go") {
30441
+ const method = call.method_name ?? "";
30442
+ const rec = call.receiver ?? "";
30443
+ if (rec === "http" && method === "Error") {
30444
+ const arg1 = call.arguments.find((a) => a.position === 1);
30445
+ if (argIsException(arg1)) api = "http.Error(w, err.Error())";
30446
+ } else if (rec === "fmt" && (method === "Fprintln" || method === "Fprintf" || method === "Fprint")) {
30447
+ const arg0 = call.arguments.find((a) => a.position === 0);
30448
+ if (arg0 && /^(w|writer|resp|response)$/i.test((arg0.variable ?? arg0.expression ?? "").trim())) {
30449
+ for (const a of call.arguments) {
30450
+ if (a.position === 0) continue;
30451
+ if (argIsException(a)) {
30452
+ api = `fmt.${method}(w, err)`;
30453
+ break;
30454
+ }
30455
+ }
30456
+ }
30457
+ } else {
30458
+ api = detectResponseLeakCall(call);
30459
+ }
30460
+ } else if (language === "python") {
30461
+ api = detectResponseLeakCall(call);
30462
+ }
30463
+ if (!api) continue;
30464
+ const line = call.location.line;
30465
+ findings.push({ line, api, language });
30466
+ ctx.addFinding(this.makeFinding(file, line, api));
30467
+ }
30468
+ return { findings };
30469
+ }
30470
+ makeFinding(file, line, api) {
30471
+ return {
30472
+ id: `${this.name}-${file}-${line}`,
30473
+ pass: this.name,
30474
+ category: this.category,
30475
+ rule_id: this.name,
30476
+ cwe: "CWE-209",
30477
+ severity: "medium",
30478
+ level: "warning",
30479
+ message: `Exception detail returned to client via \`${api}\`. Leaking stack traces / exception messages reveals framework internals, file paths, and class names \u2014 useful reconnaissance for an attacker.`,
30480
+ file,
30481
+ line,
30482
+ fix: 'Return a generic error response to the client (e.g. status 500 + a request id) and log the full exception server-side via your logger (e.g. `logger.error("\u2026", e)` or `console.error(err)`).',
30483
+ evidence: { api }
30484
+ };
30485
+ }
30486
+ };
30487
+
30488
+ // src/analysis/passes/unrestricted-file-upload-pass.ts
30489
+ var UPLOAD_NAME_RE = /(?:getOriginalFilename|getSubmittedFileName|originalname|originalName|\.filename|\.Filename|FileHeader\.Filename|UploadFile)/;
30490
+ var FILE_SAFE_CALL_RE = /(?:secure_filename|FilenameUtils\.getExtension|\.lastIndexOf\(['"]\.['"]\)|ALLOWED_EXT|ALLOWED_EXTENSIONS|allowedExtensions|\bfileFilter\b|filepath\.Ext|path\.extname)/;
30491
+ function lineWindow(code, startLine, endLine) {
30492
+ const lines = code.split("\n");
30493
+ const s = Math.max(0, startLine - 1);
30494
+ const e = Math.min(lines.length, endLine);
30495
+ return lines.slice(s, e).join("\n");
30496
+ }
30497
+ function callHasUploadName(call) {
30498
+ for (const a of call.arguments) {
30499
+ const expr = (a.expression ?? a.variable ?? "").trim();
30500
+ if (UPLOAD_NAME_RE.test(expr)) return true;
30501
+ }
30502
+ if (UPLOAD_NAME_RE.test(call.receiver ?? "")) return true;
30503
+ return false;
30504
+ }
30505
+ var UnrestrictedFileUploadPass = class {
30506
+ name = "unrestricted-file-upload";
30507
+ category = "security";
30508
+ run(ctx) {
30509
+ const { graph, language, code } = ctx;
30510
+ const file = graph.ir.meta.file;
30511
+ const findings = [];
30512
+ const safeFunctionRanges = [];
30513
+ for (const t of graph.ir.types) {
30514
+ for (const m of t.methods) {
30515
+ const body2 = lineWindow(code, m.start_line, m.end_line);
30516
+ if (FILE_SAFE_CALL_RE.test(body2)) {
30517
+ safeFunctionRanges.push({ start: m.start_line, end: m.end_line });
30518
+ }
30519
+ }
30520
+ }
30521
+ const inSafeRange = (line) => {
30522
+ for (const r of safeFunctionRanges) {
30523
+ if (line >= r.start && line <= r.end) return true;
30524
+ }
30525
+ const win = lineWindow(code, Math.max(1, line - 20), line + 5);
30526
+ return FILE_SAFE_CALL_RE.test(win);
30527
+ };
30528
+ if (language === "java") {
30529
+ for (const call of graph.ir.calls) {
30530
+ const m = call.method_name ?? "";
30531
+ if (m === "transferTo" && callHasUploadName(call)) {
30532
+ if (inSafeRange(call.location.line)) continue;
30533
+ this.emit(
30534
+ ctx,
30535
+ findings,
30536
+ file,
30537
+ call.location.line,
30538
+ language,
30539
+ "MultipartFile.transferTo(<original filename>)"
30540
+ );
30541
+ continue;
30542
+ }
30543
+ if (m === "copy" && (call.receiver === "Files" || (call.receiver ?? "").endsWith(".Files"))) {
30544
+ if (callHasUploadName(call)) {
30545
+ if (inSafeRange(call.location.line)) continue;
30546
+ this.emit(
30547
+ ctx,
30548
+ findings,
30549
+ file,
30550
+ call.location.line,
30551
+ language,
30552
+ "Files.copy(input, Path.of(dir, <original filename>))"
30553
+ );
30554
+ }
30555
+ }
30556
+ }
30557
+ }
30558
+ if (language === "javascript" || language === "typescript") {
30559
+ for (const call of graph.ir.calls) {
30560
+ const m = call.method_name ?? "";
30561
+ const rec = call.receiver ?? "";
30562
+ if (m === "multer" || rec === "" && m === "multer") {
30563
+ const arg0 = call.arguments.find((a) => a.position === 0);
30564
+ const expr = (arg0?.expression ?? "").trim();
30565
+ if (/\bdest\s*:/.test(expr) && !/\bfileFilter\s*:/.test(expr)) {
30566
+ if (inSafeRange(call.location.line)) continue;
30567
+ this.emit(
30568
+ ctx,
30569
+ findings,
30570
+ file,
30571
+ call.location.line,
30572
+ language,
30573
+ "multer({ dest }) without fileFilter"
30574
+ );
30575
+ continue;
30576
+ }
30577
+ }
30578
+ if (rec === "fs" && (m === "writeFile" || m === "writeFileSync" || m === "appendFile")) {
30579
+ if (callHasUploadName(call) || call.arguments.some((a) => /\breq\.file(?:s)?\b/.test(a.expression ?? a.variable ?? ""))) {
30580
+ if (inSafeRange(call.location.line)) continue;
30581
+ this.emit(
30582
+ ctx,
30583
+ findings,
30584
+ file,
30585
+ call.location.line,
30586
+ language,
30587
+ `fs.${m}(<path>, req.file.buffer)`
30588
+ );
30589
+ }
30590
+ }
30591
+ }
30592
+ }
30593
+ if (language === "python") {
30594
+ for (const call of graph.ir.calls) {
30595
+ const m = call.method_name ?? "";
30596
+ if (m === "save") {
30597
+ const rec = call.receiver ?? "";
30598
+ if (!/^(f|file|upload|attachment)$/i.test(rec) && rec !== "") continue;
30599
+ if (!callHasUploadName(call)) continue;
30600
+ if (inSafeRange(call.location.line)) continue;
30601
+ this.emit(
30602
+ ctx,
30603
+ findings,
30604
+ file,
30605
+ call.location.line,
30606
+ language,
30607
+ "f.save(<dir>, f.filename) without secure_filename"
30608
+ );
30609
+ }
30610
+ }
30611
+ }
30612
+ if (language === "go") {
30613
+ for (const call of graph.ir.calls) {
30614
+ const m = call.method_name ?? "";
30615
+ const rec = call.receiver ?? "";
30616
+ if (rec === "os" && (m === "Create" || m === "OpenFile")) {
30617
+ if (callHasUploadName(call)) {
30618
+ if (inSafeRange(call.location.line)) continue;
30619
+ this.emit(
30620
+ ctx,
30621
+ findings,
30622
+ file,
30623
+ call.location.line,
30624
+ language,
30625
+ `os.${m}(<uploaded filename>)`
30626
+ );
30627
+ }
30628
+ }
30629
+ if ((rec === "os" || rec === "ioutil") && m === "WriteFile") {
30630
+ if (callHasUploadName(call)) {
30631
+ if (inSafeRange(call.location.line)) continue;
30632
+ this.emit(
30633
+ ctx,
30634
+ findings,
30635
+ file,
30636
+ call.location.line,
30637
+ language,
30638
+ `${rec}.WriteFile(<uploaded filename>, \u2026)`
30639
+ );
30640
+ }
30641
+ }
30642
+ }
30643
+ }
30644
+ return { findings };
30645
+ }
30646
+ emit(ctx, findings, file, line, language, api) {
30647
+ findings.push({ line, api, language });
30648
+ ctx.addFinding({
30649
+ id: `${this.name}-${file}-${line}`,
30650
+ pass: this.name,
30651
+ category: this.category,
30652
+ rule_id: this.name,
30653
+ cwe: "CWE-434",
30654
+ severity: "high",
30655
+ level: "error",
30656
+ message: `File upload saved using untrusted name (${api}) \u2014 no extension allow-list or filename canonicalization detected. An attacker can upload a \`.jsp\`/\`.php\`/\`.html\` file and request it back, achieving RCE or stored XSS.`,
30657
+ file,
30658
+ line,
30659
+ fix: 'Validate the uploaded extension against an allow-list (e.g. `Set.of("png","jpg")`), then save with a sanitized filename. In Python use `werkzeug.utils.secure_filename`. In multer pass a `fileFilter`. Never concatenate the upload\'s original filename into a save path without validation.',
30660
+ evidence: { api, language }
30661
+ });
30662
+ }
30663
+ };
30664
+
30665
+ // src/analysis/passes/plaintext-password-storage-pass.ts
30666
+ function isWriteStorageCall(call, language) {
30667
+ const method = call.method_name ?? "";
30668
+ const receiver = call.receiver ?? "";
30669
+ const recvLower = receiver.toLowerCase();
30670
+ if (language === "python") {
30671
+ if (method === "write" || method === "writelines") {
30672
+ return { credPos: 0, api: `<file>.${method}` };
30673
+ }
30674
+ if ((recvLower === "pickle" || recvLower === "json" || recvLower === "yaml") && (method === "dump" || method === "dumps")) {
30675
+ return { credPos: 0, api: `${receiver}.${method}` };
30676
+ }
30677
+ if (recvLower === "redis" && (method === "set" || method === "setex" || method === "hset")) {
30678
+ return { credPos: 1, api: `redis.${method}` };
30679
+ }
30680
+ }
30681
+ if (language === "javascript" || language === "typescript") {
30682
+ if ((recvLower === "fs" || recvLower.endsWith(".fs")) && (method === "writeFile" || method === "writeFileSync" || method === "appendFile" || method === "appendFileSync")) {
30683
+ return { credPos: 1, api: `fs.${method}` };
30684
+ }
30685
+ if ((recvLower === "localstorage" || recvLower === "sessionstorage") && method === "setItem") {
30686
+ return { credPos: 1, api: `${receiver}.setItem` };
30687
+ }
30688
+ if (recvLower === "redis" && (method === "set" || method === "setex" || method === "hset")) {
30689
+ return { credPos: 1, api: `redis.${method}` };
30690
+ }
30691
+ }
30692
+ if (language === "java") {
30693
+ if ((receiver === "Files" || receiver.endsWith(".Files")) && (method === "write" || method === "writeString")) {
30694
+ return { credPos: 1, api: `Files.${method}` };
30695
+ }
30696
+ if (method === "write") {
30697
+ const lc = (receiver ?? "").toLowerCase();
30698
+ if (lc.includes("writer") || lc.includes("file") || lc.includes("stream")) {
30699
+ return { credPos: 0, api: `${receiver}.write` };
30700
+ }
30701
+ }
30702
+ }
30703
+ if (language === "go") {
30704
+ if (receiver === "os" || receiver.endsWith("/os")) {
30705
+ if (method === "WriteFile") return { credPos: 1, api: "os.WriteFile" };
30706
+ }
30707
+ if (receiver === "ioutil" || receiver.endsWith("/ioutil")) {
30708
+ if (method === "WriteFile") return { credPos: 1, api: "ioutil.WriteFile" };
30709
+ }
30710
+ if (method === "WriteString" || method === "Write") {
30711
+ return { credPos: 0, api: `<file>.${method}` };
30712
+ }
30713
+ }
30714
+ return null;
30715
+ }
30716
+ var PlaintextPasswordStoragePass = class {
30717
+ name = "plaintext-password-storage";
30718
+ category = "security";
30719
+ run(ctx) {
30720
+ const { graph, language } = ctx;
30721
+ const file = graph.ir.meta.file;
30722
+ const findings = [];
30723
+ const callsByScope = /* @__PURE__ */ new Map();
30724
+ for (const call of graph.ir.calls) {
30725
+ const scope = call.in_method ?? "<top>";
30726
+ const arr = callsByScope.get(scope) ?? [];
30727
+ arr.push(call);
30728
+ callsByScope.set(scope, arr);
30729
+ }
30730
+ for (const call of graph.ir.calls) {
30731
+ const spec = isWriteStorageCall(call, language);
30732
+ if (!spec) continue;
30733
+ const credArg = call.arguments.find((a) => a.position === spec.credPos);
30734
+ if (!credArg) continue;
30735
+ if (!argLooksLikeCredential(credArg)) continue;
30736
+ const identExpr = (credArg.expression ?? "").trim();
30737
+ const head = identExpr.split(/[.\s(]/, 1)[0] ?? "";
30738
+ const identifier = credArg.variable ?? head;
30739
+ if (!identifier) continue;
30740
+ const scope = call.in_method ?? "<top>";
30741
+ const scopeCalls = callsByScope.get(scope) ?? [];
30742
+ const prior = scopeCalls.filter((c) => c.location.line < call.location.line);
30743
+ if (priorHashOf(identifier, prior)) continue;
30744
+ if (/\b(?:hashpw|hash|sha\d+|md5|bcrypt|argon2|pbkdf2|digest)\b/i.test(credArg.expression ?? "")) {
30745
+ continue;
30746
+ }
30747
+ const line = call.location.line;
30748
+ findings.push({ line, language, api: spec.api, identifier });
30749
+ ctx.addFinding({
30750
+ id: `${this.name}-${file}-${line}`,
30751
+ pass: this.name,
30752
+ category: this.category,
30753
+ rule_id: this.name,
30754
+ cwe: "CWE-256",
30755
+ severity: "high",
30756
+ level: "warning",
30757
+ message: `Credential \`${identifier}\` written in plaintext via \`${spec.api}\`. Passwords / secrets must be hashed (Argon2id, bcrypt) before storage.`,
30758
+ file,
30759
+ line,
30760
+ fix: "Hash the credential with Argon2id / bcrypt before writing it to disk, cookie, KV store, or database.",
30761
+ evidence: { identifier, api: spec.api, language }
30762
+ });
30763
+ }
30764
+ return { findings };
30765
+ }
30766
+ };
30767
+
30768
+ // src/analysis/passes/cleartext-credential-transport-pass.ts
30769
+ var LOCALHOST_RE = /^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(?::\d+)?$/i;
30770
+ function isInsecureHttpUrl(urlLiteral) {
30771
+ if (!urlLiteral) return false;
30772
+ if (!/^http:\/\//i.test(urlLiteral)) return false;
30773
+ const rest = urlLiteral.slice("http://".length);
30774
+ const host = rest.split("/", 1)[0] ?? "";
30775
+ if (LOCALHOST_RE.test(host)) return false;
30776
+ return true;
30777
+ }
30778
+ function anyArgCarriesCredential(call, startPos) {
30779
+ for (const a of call.arguments) {
30780
+ if (a.position < startPos) continue;
30781
+ if (argLooksLikeCredential(a)) return true;
30782
+ const expr = (a.expression ?? "").trim();
30783
+ if (!expr) continue;
30784
+ if (/(?:["'`]?(?:password|passwd|pwd|secret|api[_-]?key|auth[_-]?token|credential)["'`]?\s*[:=])/i.test(expr)) {
30785
+ return true;
30786
+ }
30787
+ if (/\b(?:password|passwd|pwd|secret|api_key|api-key|apiKey|auth_token|authToken|credential)\w*\b/i.test(expr)) {
30788
+ return true;
30789
+ }
30790
+ }
30791
+ return false;
30792
+ }
30793
+ var CleartextCredentialTransportPass = class {
30794
+ name = "cleartext-credential-transport";
30795
+ category = "security";
30796
+ run(ctx) {
30797
+ const { graph, language } = ctx;
30798
+ const file = graph.ir.meta.file;
30799
+ const findings = [];
30800
+ for (const call of graph.ir.calls) {
30801
+ const detection = this.detect(call, language);
30802
+ if (!detection) continue;
30803
+ const { api, url } = detection;
30804
+ const line = call.location.line;
30805
+ findings.push({ line, language, api, url });
30806
+ ctx.addFinding({
30807
+ id: `${this.name}-${file}-${line}`,
30808
+ pass: this.name,
30809
+ category: this.category,
30810
+ rule_id: this.name,
30811
+ cwe: "CWE-523",
30812
+ severity: "high",
30813
+ level: "error",
30814
+ message: `Credentials transmitted to \`${url}\` over HTTP via \`${api}\`. Cleartext transport exposes credentials to network observers.`,
30815
+ file,
30816
+ line,
30817
+ fix: "Use HTTPS (https://) for all endpoints that receive credentials. For internal traffic, terminate TLS at the service boundary.",
30818
+ evidence: { api, url, language }
30819
+ });
30820
+ }
30821
+ return { findings };
30822
+ }
30823
+ detect(call, language) {
30824
+ const method = call.method_name ?? "";
30825
+ const receiver = call.receiver ?? "";
30826
+ const recvLower = receiver.toLowerCase();
30827
+ if (language === "python") {
30828
+ if ((recvLower === "requests" || recvLower === "httpx" || recvLower.endsWith(".requests") || recvLower.endsWith(".httpx")) && (method === "post" || method === "put" || method === "patch" || method === "request")) {
30829
+ const url = literalAt(call, method === "request" ? 1 : 0);
30830
+ if (isInsecureHttpUrl(url) && anyArgCarriesCredential(call, 1)) {
30831
+ return { api: `${receiver}.${method}`, url };
30832
+ }
30833
+ }
30834
+ if (method === "urlopen" && recvLower.includes("urllib")) {
30835
+ const url = literalAt(call, 0);
30836
+ if (isInsecureHttpUrl(url) && anyArgCarriesCredential(call, 1)) {
30837
+ return { api: "urllib.request.urlopen", url };
30838
+ }
30839
+ }
30840
+ }
30841
+ if (language === "javascript" || language === "typescript") {
30842
+ if ((recvLower === "axios" || recvLower.endsWith(".axios")) && (method === "post" || method === "put" || method === "patch" || method === "request")) {
30843
+ const url = literalAt(call, 0);
30844
+ if (isInsecureHttpUrl(url) && anyArgCarriesCredential(call, 1)) {
30845
+ return { api: `axios.${method}`, url };
30846
+ }
30847
+ }
30848
+ if (method === "fetch" && receiver === "") {
30849
+ const url = literalAt(call, 0);
30850
+ if (isInsecureHttpUrl(url) && anyArgCarriesCredential(call, 1)) {
30851
+ return { api: "fetch", url };
30852
+ }
30853
+ }
30854
+ if (method === "request" && (recvLower === "http" || recvLower.endsWith(".http"))) {
30855
+ const url = literalAt(call, 0);
30856
+ if (isInsecureHttpUrl(url) && anyArgCarriesCredential(call, 1)) {
30857
+ return { api: "http.request", url };
30858
+ }
30859
+ }
30860
+ }
30861
+ if (language === "java" && method === "URL" && receiver === "") {
30862
+ const url = literalAt(call, 0);
30863
+ if (!isInsecureHttpUrl(url)) return null;
30864
+ const scope = call.in_method ?? null;
30865
+ if (!scope) return null;
30866
+ return null;
30867
+ }
30868
+ if (language === "go") {
30869
+ if (method === "Post" && (receiver === "http" || receiver.endsWith("/http"))) {
30870
+ const url = literalAt(call, 0);
30871
+ if (isInsecureHttpUrl(url) && anyArgCarriesCredential(call, 1)) {
30872
+ return { api: "http.Post", url };
30873
+ }
30874
+ }
30875
+ if (method === "NewRequest" && (receiver === "http" || receiver.endsWith("/http"))) {
30876
+ const url = literalAt(call, 1);
30877
+ if (isInsecureHttpUrl(url) && anyArgCarriesCredential(call, 2)) {
30878
+ return { api: "http.NewRequest", url };
30879
+ }
30880
+ }
30881
+ }
30882
+ return null;
30883
+ }
30884
+ };
30885
+
29984
30886
  // src/analysis/passes/tls-verify-disabled-pass.ts
29985
30887
  var PY_HTTP_METHODS = /* @__PURE__ */ new Set([
29986
30888
  "get",
@@ -32019,6 +32921,10 @@ async function analyze(code, filePath, language, options = {}) {
32019
32921
  if (!disabledPasses.has("weak-hash")) pipeline.add(new WeakHashPass());
32020
32922
  if (!disabledPasses.has("weak-crypto")) pipeline.add(new WeakCryptoPass());
32021
32923
  if (!disabledPasses.has("weak-random")) pipeline.add(new WeakRandomPass());
32924
+ if (!disabledPasses.has("weak-password-hash")) pipeline.add(new WeakPasswordHashPass());
32925
+ if (!disabledPasses.has("weak-password-encoding")) pipeline.add(new WeakPasswordEncodingPass());
32926
+ if (!disabledPasses.has("plaintext-password-storage")) pipeline.add(new PlaintextPasswordStoragePass());
32927
+ if (!disabledPasses.has("cleartext-credential-transport")) pipeline.add(new CleartextCredentialTransportPass());
32022
32928
  if (!disabledPasses.has("tls-verify-disabled")) pipeline.add(new TlsVerifyDisabledPass());
32023
32929
  if (!disabledPasses.has("module-side-effect")) pipeline.add(new ModuleSideEffectPass());
32024
32930
  if (!disabledPasses.has("cache-no-vary")) pipeline.add(new CacheNoVaryPass());
@@ -32026,6 +32932,8 @@ async function analyze(code, filePath, language, options = {}) {
32026
32932
  if (!disabledPasses.has("csrf-protection-disabled")) pipeline.add(new CsrfProtectionDisabledPass());
32027
32933
  if (!disabledPasses.has("xml-entity-expansion")) pipeline.add(new XmlEntityExpansionPass());
32028
32934
  if (!disabledPasses.has("mass-assignment")) pipeline.add(new MassAssignmentPass());
32935
+ if (!disabledPasses.has("info-disclosure-stacktrace")) pipeline.add(new InfoDisclosureStacktracePass());
32936
+ if (!disabledPasses.has("unrestricted-file-upload")) pipeline.add(new UnrestrictedFileUploadPass());
32029
32937
  const { results, findings } = pipeline.run(graph, code, language, config);
32030
32938
  const sinkFilter = results.get("sink-filter");
32031
32939
  const interProc = results.get("interprocedural");