cognium-dev 3.68.0 → 3.70.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 (2) hide show
  1. package/dist/cli.js +523 -1
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -29583,6 +29583,524 @@ class TlsVerifyDisabledPass {
29583
29583
  }
29584
29584
  }
29585
29585
 
29586
+ // ../circle-ir/dist/analysis/passes/module-side-effect-pass.js
29587
+ var JS_EXEC_METHODS = new Set([
29588
+ "exec",
29589
+ "spawn",
29590
+ "execSync",
29591
+ "spawnSync",
29592
+ "execFile",
29593
+ "execFileSync"
29594
+ ]);
29595
+ var JS_EXEC_RECEIVERS = new Set([
29596
+ "child_process",
29597
+ "cp"
29598
+ ]);
29599
+ var JS_NETWORK_RECEIVER_METHOD = new Set([
29600
+ "https:request",
29601
+ "http:request",
29602
+ "https:get",
29603
+ "http:get"
29604
+ ]);
29605
+ var JS_NETWORK_MAYBE = new Set([
29606
+ "fetch"
29607
+ ]);
29608
+ var JS_ENV_SIGNAL_RE = /\bprocess\.env\b|\bos\.homedir\b|\/etc\/(passwd|shadow)\b|\.ssh\/id_(rsa|dsa|ed25519)\b|\bhomedir\b/;
29609
+ var PKG_JSON_BENIGN_INSTALL = new Set([
29610
+ "node-gyp rebuild",
29611
+ "prebuild-install",
29612
+ "prebuild-install || node-gyp rebuild",
29613
+ "husky install",
29614
+ "patch-package",
29615
+ "npm run build"
29616
+ ]);
29617
+ var PKG_JSON_INSTALL_SHELL_RE = /\b(curl|wget|nc|ncat|node\s+-e|node\s+-r|sh\s+-c|bash\s+-c|eval|base64\s+-d)\b/;
29618
+ var PY_NETWORK_RECEIVER_METHODS = [
29619
+ { receiver: "requests", method: "post" },
29620
+ { receiver: "requests", method: "put" },
29621
+ { receiver: "urllib.request", method: "urlopen" },
29622
+ { receiver: "socket", method: "create_connection" },
29623
+ { receiver: "socket", method: "connect" },
29624
+ { receiver: "subprocess", method: "run" },
29625
+ { receiver: "subprocess", method: "Popen" },
29626
+ { receiver: "os", method: "system" }
29627
+ ];
29628
+ var PY_ENV_SIGNAL_RE = /\bos\.environ\b|\bpwd\.getpw\b|\bid_(rsa|dsa|ed25519)\b|\bhome\b|\b\/etc\/(passwd|shadow)\b|\bPath\.home\b|\bglob\.glob\b/;
29629
+ var GO_INIT_DANGEROUS = [
29630
+ { receiver: "exec", method: "Command" },
29631
+ { receiver: "http", method: "Post" },
29632
+ { receiver: "http", method: "Get" },
29633
+ { receiver: "net", method: "LookupTXT" },
29634
+ { receiver: "os", method: "Setenv" }
29635
+ ];
29636
+ var RUST_DANGEROUS_METHODS = new Set([
29637
+ "Command::new",
29638
+ "process::Command::new",
29639
+ "std::process::Command::new",
29640
+ "new"
29641
+ ]);
29642
+ var RUST_DANGEROUS_RECEIVERS = new Set([
29643
+ "Command",
29644
+ "process::Command",
29645
+ "std::process::Command",
29646
+ "reqwest",
29647
+ "reqwest::blocking"
29648
+ ]);
29649
+
29650
+ class ModuleSideEffectPass {
29651
+ name = "module-side-effect";
29652
+ category = "security";
29653
+ run(ctx) {
29654
+ const { graph, language, code } = ctx;
29655
+ const file = graph.ir.meta.file;
29656
+ const findings = [];
29657
+ const emit = (line, pattern, api) => {
29658
+ if (findings.some((f) => f.line === line && f.pattern === pattern))
29659
+ return;
29660
+ findings.push({ line, language, pattern, api });
29661
+ ctx.addFinding({
29662
+ id: `${this.name}-${file}-${line}-${pattern.replace(/\W+/g, "-")}`,
29663
+ pass: this.name,
29664
+ category: this.category,
29665
+ rule_id: this.name,
29666
+ cwe: "CWE-829",
29667
+ severity: "high",
29668
+ level: "error",
29669
+ message: `Module-level / install-time side effect (${pattern}) in \`${api}\`. ` + `Code that runs at import / build / install time is invisible to ` + `runtime defenses and is the standard delivery vector for supply-` + `chain droppers (shai-hulud-style harvesters, malicious typosquats, ` + `build.rs exfil). If this side effect is intentional, move it into ` + `an explicit function invoked at runtime; if it is install-time ` + `configuration, restrict it to documented APIs (e.g. \`cargo:\` ` + `directives, \`node-gyp rebuild\`).`,
29670
+ file,
29671
+ line,
29672
+ fix: this.fixFor(language, pattern),
29673
+ evidence: { language, api, pattern }
29674
+ });
29675
+ };
29676
+ const isRustBuildScript = language === "rust" && /\bbuild\.rs$/.test(file);
29677
+ for (const call of graph.ir.calls) {
29678
+ if (language === "rust" && !isRustBuildScript)
29679
+ continue;
29680
+ const det = this.detectCall(call, language);
29681
+ if (!det)
29682
+ continue;
29683
+ emit(call.location.line, det.pattern, det.api);
29684
+ }
29685
+ if (language === "javascript" || language === "typescript") {
29686
+ if (/\bpackage\.json$/.test(file)) {
29687
+ for (const extra of this.scanPackageJson(code)) {
29688
+ emit(extra.line, extra.pattern, extra.api);
29689
+ }
29690
+ }
29691
+ }
29692
+ return { findings };
29693
+ }
29694
+ detectCall(call, language) {
29695
+ const method = call.method_name;
29696
+ const receiver = call.receiver ?? "";
29697
+ if (language === "javascript" || language === "typescript") {
29698
+ if (call.in_method != null)
29699
+ return null;
29700
+ if (JS_EXEC_RECEIVERS.has(receiver) && JS_EXEC_METHODS.has(method)) {
29701
+ return {
29702
+ pattern: "module-level child_process call",
29703
+ api: `${receiver}.${method}`
29704
+ };
29705
+ }
29706
+ const recvMethod = `${receiver}:${method}`;
29707
+ if (JS_NETWORK_RECEIVER_METHOD.has(recvMethod)) {
29708
+ return {
29709
+ pattern: "module-level network request",
29710
+ api: `${receiver}.${method}`
29711
+ };
29712
+ }
29713
+ if (JS_NETWORK_MAYBE.has(method) && receiver === "") {
29714
+ for (const arg of call.arguments) {
29715
+ if (JS_ENV_SIGNAL_RE.test(arg.expression ?? "")) {
29716
+ return {
29717
+ pattern: "module-level fetch of process.env",
29718
+ api: method
29719
+ };
29720
+ }
29721
+ }
29722
+ }
29723
+ return null;
29724
+ }
29725
+ if (language === "python") {
29726
+ if (call.in_method != null)
29727
+ return null;
29728
+ for (const tuple of PY_NETWORK_RECEIVER_METHODS) {
29729
+ if (receiver === tuple.receiver && method === tuple.method) {
29730
+ for (const arg of call.arguments) {
29731
+ if (PY_ENV_SIGNAL_RE.test(arg.expression ?? "")) {
29732
+ return {
29733
+ pattern: "import-time network call with env signal",
29734
+ api: `${receiver}.${method}`
29735
+ };
29736
+ }
29737
+ }
29738
+ }
29739
+ }
29740
+ return null;
29741
+ }
29742
+ if (language === "go") {
29743
+ if (call.in_method !== "init")
29744
+ return null;
29745
+ for (const tuple of GO_INIT_DANGEROUS) {
29746
+ if (receiver === tuple.receiver && method === tuple.method) {
29747
+ return {
29748
+ pattern: "init() install-time side effect",
29749
+ api: `${receiver}.${method}`
29750
+ };
29751
+ }
29752
+ }
29753
+ return null;
29754
+ }
29755
+ if (language === "rust") {
29756
+ const recv = receiver.trim();
29757
+ if (RUST_DANGEROUS_RECEIVERS.has(recv) || recv.startsWith("Command::")) {
29758
+ if (method === "new" || RUST_DANGEROUS_METHODS.has(method) || method === "get" || method === "post") {
29759
+ return {
29760
+ pattern: "build.rs side effect",
29761
+ api: `${recv || method}.${method}`
29762
+ };
29763
+ }
29764
+ }
29765
+ return null;
29766
+ }
29767
+ return null;
29768
+ }
29769
+ scanPackageJson(code) {
29770
+ const out2 = [];
29771
+ const lines = code.split(`
29772
+ `);
29773
+ const installRe = /"(pre|post)?install"\s*:\s*"([^"]+)"/i;
29774
+ for (let i2 = 0;i2 < lines.length; i2++) {
29775
+ const m = lines[i2].match(installRe);
29776
+ if (!m)
29777
+ continue;
29778
+ const value = m[2].trim();
29779
+ if (PKG_JSON_BENIGN_INSTALL.has(value))
29780
+ continue;
29781
+ if (!PKG_JSON_INSTALL_SHELL_RE.test(value))
29782
+ continue;
29783
+ out2.push({
29784
+ line: i2 + 1,
29785
+ pattern: "npm lifecycle hook executes shell",
29786
+ api: `scripts.${m[1] ?? ""}install`
29787
+ });
29788
+ }
29789
+ return out2;
29790
+ }
29791
+ fixFor(language, pattern) {
29792
+ if (pattern.includes("child_process")) {
29793
+ return "Remove the module-level child_process call. If an install-time " + "step is genuinely required, move it into an explicit function and " + "document why it must run at install time.";
29794
+ }
29795
+ if (pattern.includes("module-level network")) {
29796
+ return "Network requests should not run at module load. Move the call " + "inside an exported function called explicitly by the caller.";
29797
+ }
29798
+ if (pattern.includes("module-level fetch of process.env")) {
29799
+ return "Exfiltrating `process.env` at module load is the canonical " + "supply-chain dropper shape. Remove this code or, if intentional, " + "gate it behind an explicit opt-in.";
29800
+ }
29801
+ if (pattern.includes("npm lifecycle hook")) {
29802
+ return "Replace the install-script shell payload with a build tool " + "(e.g. `node-gyp rebuild`, `prebuild-install`). Lifecycle scripts " + "that invoke curl/wget/node -e/sh -c are how supply-chain droppers " + "are delivered.";
29803
+ }
29804
+ if (pattern.includes("import-time network call")) {
29805
+ return "Move the network call inside an explicit function. Sending " + "`os.environ` or filesystem secrets at module import is the canonical " + "credential-harvester shape.";
29806
+ }
29807
+ if (pattern.includes("init()")) {
29808
+ return "Move the side effect out of `init()`. Go `init` functions " + "run automatically on package import; network/exec calls there are " + "invisible to the caller and are how supply-chain droppers operate.";
29809
+ }
29810
+ if (pattern.includes("build.rs")) {
29811
+ return "`build.rs` should only emit `cargo:` directives. " + "Spawning subprocesses or making network requests at build time is " + "a documented supply-chain attack vector (see RUSTSEC).";
29812
+ }
29813
+ return "Remove the module-level side effect or move it inside an " + "explicit, runtime-invoked function.";
29814
+ }
29815
+ }
29816
+
29817
+ // ../circle-ir/dist/analysis/passes/cache-no-vary-pass.js
29818
+ function isSharedCacheable(value) {
29819
+ const v = value.toLowerCase();
29820
+ if (/\b(private|no-store|no-cache)\b/.test(v))
29821
+ return false;
29822
+ const pub = /\bpublic\b/.test(v);
29823
+ const maxMatch = /\b(?:s-maxage|max-age)\s*=\s*(\d+)/.exec(v);
29824
+ const positiveMax = maxMatch ? Number(maxMatch[1]) > 0 : false;
29825
+ return pub || positiveMax;
29826
+ }
29827
+ function isVaryCovering(value) {
29828
+ const v = value.toLowerCase();
29829
+ return /\b(cookie|authorization|\*)\b/.test(v);
29830
+ }
29831
+ var JS_AUTH_SIGNAL_RE = /\b(?:req|request)\s*\.\s*(?:cookies|session|user(?:Id|Name)?)\b|\b(?:req|request)\s*\.\s*headers\s*\.\s*(?:cookie|authorization)\b|\bres(?:ponse)?\s*\.\s*cookie\s*\(/i;
29832
+ var PY_AUTH_SIGNAL_RE = /\brequest\s*\.\s*cookies\b|\brequest\s*\.\s*headers\s*\.\s*get\s*\(\s*['"]Authorization['"]|\brequest\s*\.\s*authorization\b|\bsession\s*\[|\b(?:g\.user|current_user)\b|\bset_cookie\s*\(/i;
29833
+ var GO_AUTH_SIGNAL_RE = /\br\s*\.\s*Cookie\s*\(|\br\s*\.\s*Header\s*(?:\(\)|\.)\s*\.?\s*Get\s*\(\s*"(?:Cookie|Authorization)"|\br\s*\.\s*BasicAuth\s*\(|\bhttp\s*\.\s*SetCookie\s*\(|\bc\s*\.\s*(?:GetHeader|Cookie|SetCookie)\s*\(/;
29834
+ var JAVA_AUTH_SIGNAL_RE = /@CookieValue\b|@RequestHeader\s*\(\s*"(?:Cookie|Authorization)"|\brequest\s*\.\s*getCookies\s*\(|\brequest\s*\.\s*getHeader\s*\(\s*"(?:Cookie|Authorization)"|\bresponse\s*\.\s*addCookie\s*\(|\bSecurityContextHolder\b|\bPrincipal\s+\w+|\bAuthentication\s+\w+/;
29835
+ var PY_CACHE_HEADER_ASSIGN_RE = /\w+(?:\s*\.\s*\w+)*\s*\.\s*headers\s*\[\s*['"]Cache-Control['"]\s*\]\s*=\s*(['"])([^'"]*)\1/i;
29836
+ var PY_VARY_HEADER_ASSIGN_RE = /\w+(?:\s*\.\s*\w+)*\s*\.\s*headers\s*\[\s*['"]Vary['"]\s*\]\s*=\s*(['"])([^'"]*)\1/i;
29837
+ var PY_VARY_DECORATOR_RE = /^\s*@\s*(?:vary_on_cookie|vary_on_headers)\b/;
29838
+ var PY_CACHE_CONTROL_DECORATOR_RE = /^\s*@\s*cache_control\s*\(([^)]*)\)/;
29839
+ var JS_HEADER_METHODS = new Set(["setHeader", "set", "header"]);
29840
+ var GO_HEADER_METHODS = new Set(["Set", "Add"]);
29841
+ var JAVA_HEADER_METHODS = new Set(["setHeader", "addHeader"]);
29842
+ var JS_RES_RECEIVERS = new Set(["res", "response", "ctx"]);
29843
+ function classifyCall(call, language) {
29844
+ const method = call.method_name;
29845
+ const receiver = (call.receiver ?? "").trim();
29846
+ const arg0 = call.arguments[0]?.literal ?? null;
29847
+ const arg1 = call.arguments[1]?.literal ?? null;
29848
+ if (language === "javascript" || language === "typescript") {
29849
+ if (JS_RES_RECEIVERS.has(receiver) && JS_HEADER_METHODS.has(method)) {
29850
+ const header = (arg0 ?? "").toLowerCase();
29851
+ if (header === "cache-control" && arg1 && isSharedCacheable(arg1)) {
29852
+ return { kind: "cache-public", value: arg1 };
29853
+ }
29854
+ if (header === "vary" && arg1 && isVaryCovering(arg1)) {
29855
+ return { kind: "vary" };
29856
+ }
29857
+ }
29858
+ if (JS_RES_RECEIVERS.has(receiver) && method === "vary") {
29859
+ const v = arg0 ?? "";
29860
+ if (isVaryCovering(v) || v === "")
29861
+ return { kind: "vary" };
29862
+ }
29863
+ if (JS_RES_RECEIVERS.has(receiver) && method === "cookie") {
29864
+ return { kind: "auth" };
29865
+ }
29866
+ return null;
29867
+ }
29868
+ if (language === "python") {
29869
+ if (receiver === "request.cookies" || receiver === "request.session") {
29870
+ return { kind: "auth" };
29871
+ }
29872
+ if (receiver === "request.headers" && method === "get") {
29873
+ const v = (arg0 ?? "").toLowerCase();
29874
+ if (v === "authorization" || v === "cookie")
29875
+ return { kind: "auth" };
29876
+ }
29877
+ if ((receiver === "response" || receiver === "resp") && method === "set_cookie") {
29878
+ return { kind: "auth" };
29879
+ }
29880
+ if (method === "patch_vary_headers")
29881
+ return { kind: "vary" };
29882
+ if (method === "patch_cache_control") {
29883
+ const argTxt = call.arguments.map((a) => a.expression ?? "").join(",");
29884
+ if (/\bpublic\s*=\s*True\b/.test(argTxt)) {
29885
+ return { kind: "cache-public", value: argTxt };
29886
+ }
29887
+ }
29888
+ return null;
29889
+ }
29890
+ if (language === "go") {
29891
+ if ((receiver === "w.Header()" || receiver === "rw.Header()") && GO_HEADER_METHODS.has(method)) {
29892
+ const header = (arg0 ?? "").toLowerCase();
29893
+ if (header === "cache-control" && arg1 && isSharedCacheable(arg1)) {
29894
+ return { kind: "cache-public", value: arg1 };
29895
+ }
29896
+ if (header === "vary" && arg1 && isVaryCovering(arg1)) {
29897
+ return { kind: "vary" };
29898
+ }
29899
+ }
29900
+ if (receiver === "c" && method === "Header") {
29901
+ const header = (arg0 ?? "").toLowerCase();
29902
+ if (header === "cache-control" && arg1 && isSharedCacheable(arg1)) {
29903
+ return { kind: "cache-public", value: arg1 };
29904
+ }
29905
+ if (header === "vary" && arg1 && isVaryCovering(arg1)) {
29906
+ return { kind: "vary" };
29907
+ }
29908
+ }
29909
+ if (receiver === "r" && (method === "Cookie" || method === "BasicAuth")) {
29910
+ return { kind: "auth" };
29911
+ }
29912
+ if ((receiver === "r.Header" || receiver === "r.Header()") && method === "Get") {
29913
+ const v = (arg0 ?? "").toLowerCase();
29914
+ if (v === "cookie" || v === "authorization")
29915
+ return { kind: "auth" };
29916
+ }
29917
+ if (receiver === "http" && method === "SetCookie")
29918
+ return { kind: "auth" };
29919
+ if (receiver === "c" && (method === "Cookie" || method === "GetHeader" || method === "SetCookie")) {
29920
+ return { kind: "auth" };
29921
+ }
29922
+ return null;
29923
+ }
29924
+ if (language === "java") {
29925
+ if ((receiver === "response" || receiver === "resp") && JAVA_HEADER_METHODS.has(method)) {
29926
+ const header = (arg0 ?? "").toLowerCase();
29927
+ if (header === "cache-control" && arg1 && isSharedCacheable(arg1)) {
29928
+ return { kind: "cache-public", value: arg1 };
29929
+ }
29930
+ if (header === "vary" && arg1 && isVaryCovering(arg1)) {
29931
+ return { kind: "vary" };
29932
+ }
29933
+ }
29934
+ if ((receiver === "headers" || receiver === "httpHeaders") && method === "setCacheControl") {
29935
+ return { kind: "cache-public", value: "HttpHeaders.setCacheControl(...)" };
29936
+ }
29937
+ if ((receiver === "headers" || receiver === "httpHeaders") && method === "setVary") {
29938
+ return { kind: "vary" };
29939
+ }
29940
+ if (receiver === "request" && method === "getCookies") {
29941
+ return { kind: "auth" };
29942
+ }
29943
+ if (receiver === "request" && method === "getHeader") {
29944
+ const v = (arg0 ?? "").toLowerCase();
29945
+ if (v === "cookie" || v === "authorization")
29946
+ return { kind: "auth" };
29947
+ }
29948
+ if ((receiver === "response" || receiver === "resp") && method === "addCookie") {
29949
+ return { kind: "auth" };
29950
+ }
29951
+ return null;
29952
+ }
29953
+ return null;
29954
+ }
29955
+ function authSignalRegex(language) {
29956
+ switch (language) {
29957
+ case "javascript":
29958
+ case "typescript":
29959
+ return JS_AUTH_SIGNAL_RE;
29960
+ case "python":
29961
+ return PY_AUTH_SIGNAL_RE;
29962
+ case "go":
29963
+ return GO_AUTH_SIGNAL_RE;
29964
+ case "java":
29965
+ return JAVA_AUTH_SIGNAL_RE;
29966
+ default:
29967
+ return null;
29968
+ }
29969
+ }
29970
+ function scanWindow(code, language, startLine, endLine) {
29971
+ const lines = code.split(`
29972
+ `);
29973
+ const lo = Math.max(0, startLine - 1);
29974
+ const hi = Math.min(lines.length, endLine);
29975
+ const out2 = { vary: false, auth: false };
29976
+ const authRe = authSignalRegex(language);
29977
+ for (let i2 = lo;i2 < hi; i2++) {
29978
+ const ln = lines[i2];
29979
+ if (authRe && authRe.test(ln))
29980
+ out2.auth = true;
29981
+ if (language === "python") {
29982
+ if (!out2.cachePublic) {
29983
+ const mc = PY_CACHE_HEADER_ASSIGN_RE.exec(ln);
29984
+ if (mc && isSharedCacheable(mc[2])) {
29985
+ out2.cachePublic = { line: i2 + 1, value: mc[2] };
29986
+ }
29987
+ }
29988
+ if (!out2.cachePublic) {
29989
+ const md = PY_CACHE_CONTROL_DECORATOR_RE.exec(ln);
29990
+ if (md) {
29991
+ const argTxt = md[1];
29992
+ if (/\bpublic\s*=\s*True\b/.test(argTxt) && (/\bmax_age\s*=\s*[1-9]\d*\b/.test(argTxt) || !/max_age/.test(argTxt))) {
29993
+ out2.cachePublic = { line: i2 + 1, value: argTxt };
29994
+ }
29995
+ }
29996
+ }
29997
+ if (!out2.vary) {
29998
+ const mv = PY_VARY_HEADER_ASSIGN_RE.exec(ln);
29999
+ if (mv && isVaryCovering(mv[2]))
30000
+ out2.vary = true;
30001
+ if (PY_VARY_DECORATOR_RE.test(ln))
30002
+ out2.vary = true;
30003
+ }
30004
+ }
30005
+ }
30006
+ return out2;
30007
+ }
30008
+
30009
+ class CacheNoVaryPass {
30010
+ name = "cache-no-vary";
30011
+ category = "security";
30012
+ run(ctx) {
30013
+ const { graph, language, code } = ctx;
30014
+ const file = graph.ir.meta.file;
30015
+ const findings = [];
30016
+ const isSupported = language === "javascript" || language === "typescript" || language === "python" || language === "go" || language === "java";
30017
+ if (!isSupported)
30018
+ return { findings };
30019
+ if (/(?:\.test|\.spec)\.[jt]sx?$/i.test(file) || /__tests__\/|\/tests?\//i.test(file)) {
30020
+ return { findings };
30021
+ }
30022
+ const callsByHandler = new Map;
30023
+ for (const call of graph.ir.calls) {
30024
+ const key = call.in_method ?? "<top>";
30025
+ let arr = callsByHandler.get(key);
30026
+ if (!arr) {
30027
+ arr = [];
30028
+ callsByHandler.set(key, arr);
30029
+ }
30030
+ arr.push(call);
30031
+ }
30032
+ const emit = (line, handler, cacheValue) => {
30033
+ if (findings.some((f) => f.line === line && f.handler === handler))
30034
+ return;
30035
+ findings.push({ line, language, handler, cacheValue });
30036
+ ctx.addFinding({
30037
+ id: `${this.name}-${file}-${line}`,
30038
+ pass: this.name,
30039
+ category: this.category,
30040
+ rule_id: this.name,
30041
+ cwe: "CWE-524",
30042
+ severity: "medium",
30043
+ level: "warning",
30044
+ message: `Response sets a shared-cacheable Cache-Control ('${cacheValue}') in ` + `a handler that reads authenticated or user-scoped state, but does ` + `not set 'Vary: Cookie' or 'Vary: Authorization'. A shared cache ` + `(CDN, reverse proxy, ISP cache) keys the response by URL only and ` + `may serve one user's body to another. (CWE-524)`,
30045
+ file,
30046
+ line,
30047
+ fix: `Either add 'Vary: Cookie' (or 'Vary: Authorization') so caches key ` + `on the user identity, or change the directive to 'private' / ` + `'no-store' so the response is never shared-cached.`,
30048
+ evidence: {
30049
+ language,
30050
+ handler: handler ?? "<top>",
30051
+ cacheValue
30052
+ }
30053
+ });
30054
+ };
30055
+ for (const [handlerKey, calls] of callsByHandler) {
30056
+ const handler = handlerKey === "<top>" ? null : handlerKey;
30057
+ const cachePublicHits = [];
30058
+ let varyFromCalls = false;
30059
+ let authFromCalls = false;
30060
+ for (const call of calls) {
30061
+ const cls = classifyCall(call, language);
30062
+ if (!cls)
30063
+ continue;
30064
+ if (cls.kind === "cache-public") {
30065
+ cachePublicHits.push({
30066
+ line: call.location.line,
30067
+ value: cls.value ?? ""
30068
+ });
30069
+ } else if (cls.kind === "vary") {
30070
+ varyFromCalls = true;
30071
+ } else if (cls.kind === "auth") {
30072
+ authFromCalls = true;
30073
+ }
30074
+ }
30075
+ let minLine = Infinity;
30076
+ let maxLine = -Infinity;
30077
+ for (const c of calls) {
30078
+ if (c.location?.line) {
30079
+ minLine = Math.min(minLine, c.location.line);
30080
+ maxLine = Math.max(maxLine, c.location.line);
30081
+ }
30082
+ }
30083
+ if (minLine === Infinity)
30084
+ continue;
30085
+ const winStart = Math.max(1, minLine - 5);
30086
+ const winEnd = maxLine + 5;
30087
+ const winScan = scanWindow(code, language, winStart, winEnd);
30088
+ if (winScan.cachePublic)
30089
+ cachePublicHits.push(winScan.cachePublic);
30090
+ const vary = varyFromCalls || winScan.vary;
30091
+ const auth = authFromCalls || winScan.auth;
30092
+ if (cachePublicHits.length === 0)
30093
+ continue;
30094
+ if (vary)
30095
+ continue;
30096
+ if (!auth)
30097
+ continue;
30098
+ emit(cachePublicHits[0].line, handler, cachePublicHits[0].value);
30099
+ }
30100
+ return { findings };
30101
+ }
30102
+ }
30103
+
29586
30104
  // ../circle-ir/dist/analysis/passes/jwt-verify-disabled-pass.js
29587
30105
  var PY_VERIFY_SIGNATURE_FALSE_RE = /["']verify_signature["']\s*:\s*False\b/;
29588
30106
  var PY_VERIFY_KW_FALSE_RE = /\bverify\s*=\s*False\b/;
@@ -31175,6 +31693,10 @@ async function analyze(code, filePath, language, options = {}) {
31175
31693
  pipeline.add(new WeakRandomPass);
31176
31694
  if (!disabledPasses.has("tls-verify-disabled"))
31177
31695
  pipeline.add(new TlsVerifyDisabledPass);
31696
+ if (!disabledPasses.has("module-side-effect"))
31697
+ pipeline.add(new ModuleSideEffectPass);
31698
+ if (!disabledPasses.has("cache-no-vary"))
31699
+ pipeline.add(new CacheNoVaryPass);
31178
31700
  if (!disabledPasses.has("jwt-verify-disabled"))
31179
31701
  pipeline.add(new JwtVerifyDisabledPass);
31180
31702
  if (!disabledPasses.has("csrf-protection-disabled"))
@@ -31376,7 +31898,7 @@ var colors = {
31376
31898
  };
31377
31899
 
31378
31900
  // src/version.ts
31379
- var version = "3.68.0";
31901
+ var version = "3.70.0";
31380
31902
 
31381
31903
  // src/formatters.ts
31382
31904
  var SINK_SEVERITY = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "3.68.0",
3
+ "version": "3.70.0",
4
4
  "description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -65,7 +65,7 @@
65
65
  "registry": "https://registry.npmjs.org/"
66
66
  },
67
67
  "dependencies": {
68
- "circle-ir": "^3.68.0"
68
+ "circle-ir": "^3.70.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^25.5.0",