cognium-dev 3.69.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 +290 -1
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -29814,6 +29814,293 @@ class ModuleSideEffectPass {
29814
29814
  }
29815
29815
  }
29816
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
+
29817
30104
  // ../circle-ir/dist/analysis/passes/jwt-verify-disabled-pass.js
29818
30105
  var PY_VERIFY_SIGNATURE_FALSE_RE = /["']verify_signature["']\s*:\s*False\b/;
29819
30106
  var PY_VERIFY_KW_FALSE_RE = /\bverify\s*=\s*False\b/;
@@ -31408,6 +31695,8 @@ async function analyze(code, filePath, language, options = {}) {
31408
31695
  pipeline.add(new TlsVerifyDisabledPass);
31409
31696
  if (!disabledPasses.has("module-side-effect"))
31410
31697
  pipeline.add(new ModuleSideEffectPass);
31698
+ if (!disabledPasses.has("cache-no-vary"))
31699
+ pipeline.add(new CacheNoVaryPass);
31411
31700
  if (!disabledPasses.has("jwt-verify-disabled"))
31412
31701
  pipeline.add(new JwtVerifyDisabledPass);
31413
31702
  if (!disabledPasses.has("csrf-protection-disabled"))
@@ -31609,7 +31898,7 @@ var colors = {
31609
31898
  };
31610
31899
 
31611
31900
  // src/version.ts
31612
- var version = "3.69.0";
31901
+ var version = "3.70.0";
31613
31902
 
31614
31903
  // src/formatters.ts
31615
31904
  var SINK_SEVERITY = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "3.69.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.69.0"
68
+ "circle-ir": "^3.70.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^25.5.0",