auditai-scan 0.6.0 → 0.6.1

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/auditai-scan.mjs +148 -12
  2. package/package.json +1 -1
@@ -959,8 +959,23 @@ function secretScopeFor(fn, sf) {
959
959
  growNames(ownDeclarations(fn), scope);
960
960
  return scope;
961
961
  }
962
+ var STORED_SECRET_FIELD = /(^|_)(secret|signing_key|api_key|key_hash|token_hash|hmac_key)$/;
963
+ function readsStoredSecret(e) {
964
+ let hit = false;
965
+ const visit = (n) => {
966
+ if (hit) return;
967
+ if (ts3.isPropertyAccessExpression(n) && STORED_SECRET_FIELD.test(n.name.text.toLowerCase())) {
968
+ hit = true;
969
+ return;
970
+ }
971
+ n.forEachChild(visit);
972
+ };
973
+ visit(e);
974
+ return hit;
975
+ }
962
976
  function isSecretish(e, scope) {
963
977
  if (envNamesIn(e).some(isSecretEnvName)) return true;
978
+ if (readsStoredSecret(e)) return true;
964
979
  for (const id of identifiersIn(e)) if (scope.names.has(id)) return true;
965
980
  let calls = false;
966
981
  const visit = (n) => {
@@ -984,7 +999,7 @@ var EQUALITY = /* @__PURE__ */ new Set([
984
999
  ts3.SyntaxKind.EqualsEqualsToken,
985
1000
  ts3.SyntaxKind.ExclamationEqualsToken
986
1001
  ]);
987
- var COMPARE_CALLEE = /^(timingSafeEqual|safeCompare|secureCompare|safeEqual|constantTimeEqual|constantTimeCompare|timingSafeCompare|compare|compareSync|isEqual|equals?)$/i;
1002
+ var COMPARE_CALLEE = /^(timingSafeEqual|safeCompare|secureCompare|safeEqual|constantTimeEqual|constantTimeCompare|timingSafeCompare|compare|compareSync|isEqual|equals?|secretsMatch|secretMatch|matchesSecret|tokensMatch|tokenMatches|sameSecret|checkSecret|validSecret|isValidSecret)$/i;
988
1003
  var VERIFY_CALLEE = /^(verify|verifySync|jwtVerify|constructEvent|constructEventAsync|verifySignature|verifyWebhook|validateSignature)$/i;
989
1004
  var THROWING_VERIFIER = /^(jwtVerify|constructEvent|constructEventAsync)$/;
990
1005
  var THROWING_VERIFY_RECEIVER = /jwt|jose|jsonwebtoken/i;
@@ -1035,11 +1050,39 @@ function gates(node, fnBody) {
1035
1050
  }
1036
1051
  return false;
1037
1052
  }
1053
+ function receiverSecret(callee, secretish, builtFromSecret) {
1054
+ if (!ts3.isPropertyAccessExpression(callee)) return false;
1055
+ const recv = callee.expression;
1056
+ if (ts3.isNewExpression(recv)) return (recv.arguments ?? []).some(secretish);
1057
+ if (ts3.isIdentifier(recv) && builtFromSecret.has(recv.text)) return true;
1058
+ return secretish(recv);
1059
+ }
1060
+ function instancesBuiltFromSecret(body, secretish) {
1061
+ const out = /* @__PURE__ */ new Set();
1062
+ for (const d of collect(body, ts3.isVariableDeclaration)) {
1063
+ if (!d.initializer || !ts3.isIdentifier(d.name) || !ts3.isNewExpression(d.initializer)) continue;
1064
+ if ((d.initializer.arguments ?? []).some(secretish)) out.add(d.name.text);
1065
+ }
1066
+ return out;
1067
+ }
1068
+ function gatesByThrow(node, fnBody) {
1069
+ let child = node;
1070
+ let cur = node.parent;
1071
+ while (cur && cur !== fnBody && !isFunctionLikeNode(cur)) {
1072
+ if (ts3.isTryStatement(cur) && cur.tryBlock === child && cur.catchClause) {
1073
+ return exitKind(cur.catchClause.block) !== null;
1074
+ }
1075
+ child = cur;
1076
+ cur = cur.parent;
1077
+ }
1078
+ return false;
1079
+ }
1038
1080
  function secretChecksIn(fn, sf) {
1039
1081
  if (!fn.body) return [];
1040
1082
  const body = fn.body;
1041
1083
  const secrets = secretScopeFor(fn, sf);
1042
1084
  const secretish = (e) => isSecretish(e, secrets);
1085
+ const builtFromSecret = instancesBuiltFromSecret(body, secretish);
1043
1086
  const out = [];
1044
1087
  const push = (n) => {
1045
1088
  out.push({ node: n, text: n.getText(sf).replace(/\s+/g, " ").slice(0, 160) });
@@ -1058,8 +1101,9 @@ function secretChecksIn(fn, sf) {
1058
1101
  const verify = VERIFY_CALLEE.test(name);
1059
1102
  if (!compare && !verify) return;
1060
1103
  const secretArgs = n.arguments.filter(secretish).length;
1061
- if (secretArgs === 0 || secretArgs === n.arguments.length) return;
1062
- if (verify && throwsOnBadCredential(n) || gates(n, body)) push(n);
1104
+ const receiverHoldsSecret = verify && receiverSecret(n.expression, secretish, builtFromSecret);
1105
+ if (!receiverHoldsSecret && (secretArgs === 0 || secretArgs === n.arguments.length)) return;
1106
+ if (verify && throwsOnBadCredential(n) || gates(n, body) || gatesByThrow(n, body)) push(n);
1063
1107
  });
1064
1108
  return out;
1065
1109
  }
@@ -1086,7 +1130,7 @@ function nextAuthSessionNames(stmt, nextAuthLocal) {
1086
1130
  }
1087
1131
  return out;
1088
1132
  }
1089
- var CREDENTIAL_COLUMN = /(keyhash|tokenhash|hashedkey|hashedtoken|secrethash|apikey|apikeyhash|apitoken|accesstoken|sessiontoken|secret)$/;
1133
+ var CREDENTIAL_COLUMN = /(keyhash|tokenhash|hashedkey|hashedtoken|secrethash|apikey|apikeyhash|apitoken|accesstoken|sessiontoken|secret|token|sessioncode|invitecode|accesscode|sharecode)$/;
1090
1134
  function isCredentialColumn(column) {
1091
1135
  return column !== null && CREDENTIAL_COLUMN.test(column.toLowerCase().replace(/_/g, ""));
1092
1136
  }
@@ -3838,10 +3882,20 @@ var LOGICAL = /* @__PURE__ */ new Set([
3838
3882
  ts10.SyntaxKind.BarBarToken,
3839
3883
  ts10.SyntaxKind.AmpersandAmpersandToken
3840
3884
  ]);
3885
+ var SCHEMA_PARSE = /^(parse|safeParse|parseAsync|safeParseAsync)$/;
3886
+ function isSchemaParse(call, cx) {
3887
+ const callee = call.expression;
3888
+ if (!ts10.isPropertyAccessExpression(callee) || !SCHEMA_PARSE.test(callee.name.text)) return false;
3889
+ const recv = callee.expression;
3890
+ if (isWholeInput(recv, cx)) return false;
3891
+ return cx.strippingSchema(recv);
3892
+ }
3841
3893
  function isWholeInput(e, cx) {
3842
3894
  const u = unwrap(e);
3843
3895
  if (ts10.isIdentifier(u)) return cx.wholeName(u.text);
3844
3896
  if (ts10.isPropertyAccessExpression(u) || ts10.isElementAccessExpression(u)) {
3897
+ const inner = unwrap(u.expression);
3898
+ if (ts10.isCallExpression(inner) && isSchemaParse(inner, cx)) return false;
3845
3899
  return isWholeInput(u.expression, cx);
3846
3900
  }
3847
3901
  if (ts10.isObjectLiteralExpression(u)) {
@@ -3861,6 +3915,7 @@ function isWholeInput(e, cx) {
3861
3915
  }
3862
3916
  function isWholeCall(call, cx) {
3863
3917
  if (cx.requestBody(call)) return true;
3918
+ if (isSchemaParse(call, cx)) return false;
3864
3919
  const callee = call.expression;
3865
3920
  if (ts10.isPropertyAccessExpression(callee)) {
3866
3921
  const method = callee.name.text;
@@ -3882,7 +3937,8 @@ function callbackKeepsWhole(cb, receiverWhole, cx) {
3882
3937
  const inner = {
3883
3938
  wholeName: (n) => elementNames.has(n) || cx.wholeName(n),
3884
3939
  requestBody: cx.requestBody,
3885
- requestName: cx.requestName
3940
+ requestName: cx.requestName,
3941
+ strippingSchema: cx.strippingSchema
3886
3942
  };
3887
3943
  return ownReturns(f).some((r) => isWholeInput(r, inner));
3888
3944
  }
@@ -4207,6 +4263,7 @@ function usesInput(frame, e) {
4207
4263
  });
4208
4264
  return hit;
4209
4265
  }
4266
+ var CALLER_READ = /\b(?:await\s+)?cookies\(\)\s*(?:\.|$)|cookieStore\s*\.\s*get\s*\(|\bcookies\s*\.\s*get\s*\(|\b(?:await\s+)?headers\(\)\s*\.\s*get\s*\(/;
4210
4267
  var REQUEST_MEMBER = /^(json|formData|text|arrayBuffer|blob|body|headers|cookies|url|nextUrl|query|params|ip|geo)$/;
4211
4268
  var REQUEST_CLIENT_CALLEE = /client|supabase|prisma|drizzle/i;
4212
4269
  function handsOverRequest(frame, call) {
@@ -4275,13 +4332,28 @@ function isRequestBodyCall(frame, call) {
4275
4332
  if (frame.reqNames.has(recv.text)) return true;
4276
4333
  return frame.depth === 0 && frame.reqNames.size === 0 && REQUEST_NAME.test(recv.text);
4277
4334
  }
4278
- function wholeContext(frame) {
4335
+ function wholeContext(p, frame) {
4279
4336
  return {
4280
4337
  wholeName: (n) => frame.wholeNames.has(n),
4281
4338
  requestBody: (c) => isRequestBodyCall(frame, c),
4282
- requestName: (n) => frame.reqNames.has(n)
4339
+ requestName: (n) => frame.reqNames.has(n),
4340
+ strippingSchema: (e) => isStrippingSchema(p, frame, e)
4283
4341
  };
4284
4342
  }
4343
+ var OBJECT_SCHEMA = /\b(?:z|zod|v|valibot|yup)\s*\.\s*object\s*\(/;
4344
+ var SCHEMA_KEEPS_UNKNOWN = /\.\s*(?:passthrough|catchall|nonstrict|unknown)\s*\(/;
4345
+ function isStrippingSchema(p, frame, e) {
4346
+ const u = unwrap(e);
4347
+ if (ts11.isCallExpression(u) || ts11.isPropertyAccessExpression(u)) {
4348
+ const text2 = u.getText();
4349
+ return OBJECT_SCHEMA.test(text2) && !SCHEMA_KEEPS_UNKNOWN.test(text2);
4350
+ }
4351
+ if (!ts11.isIdentifier(u)) return false;
4352
+ const sym = scopeOf(p, frame.facts).get(u.text);
4353
+ if (sym?.kind !== "var") return false;
4354
+ const text = sym.init.getText();
4355
+ return OBJECT_SCHEMA.test(text) && !SCHEMA_KEEPS_UNKNOWN.test(text);
4356
+ }
4285
4357
  function receiverTainted(frame, call) {
4286
4358
  const callee = call.expression;
4287
4359
  if (!ts11.isPropertyAccessExpression(callee) && !ts11.isElementAccessExpression(callee)) return false;
@@ -4336,7 +4408,7 @@ function argBinding(p, arg, frame) {
4336
4408
  client,
4337
4409
  instance,
4338
4410
  tainted: derivedIn(frame, arg),
4339
- whole: isWholeInput(arg, wholeContext(frame)),
4411
+ whole: isWholeInput(arg, wholeContext(p, frame)),
4340
4412
  isRequest
4341
4413
  };
4342
4414
  }
@@ -4466,6 +4538,11 @@ function bindDeclarations(p, frame, acc) {
4466
4538
  continue;
4467
4539
  }
4468
4540
  }
4541
+ if (CALLER_READ.test(text)) {
4542
+ if (frame.depth === 0) handlerInput("header", names, decl);
4543
+ else bindInput(names, false);
4544
+ continue;
4545
+ }
4469
4546
  if (ts11.isCallExpression(init)) {
4470
4547
  const inst = instanceOfCall(p, init, frame, scope);
4471
4548
  if (inst && ts11.isIdentifier(decl.name)) {
@@ -4478,7 +4555,7 @@ function bindDeclarations(p, frame, acc) {
4478
4555
  if (args.some((a) => a.tainted || a.isRequest) || receiverTainted(frame, init)) {
4479
4556
  const rt = returnTaint(p, init, frame, scope, 0);
4480
4557
  if (rt === null || rt.tainted && rt.props === null) {
4481
- bindInput(names, isWholeInput(init, wholeContext(frame)));
4558
+ bindInput(names, isWholeInput(init, wholeContext(p, frame)));
4482
4559
  } else if (rt.tainted && rt.props !== null) {
4483
4560
  if (ts11.isIdentifier(decl.name))
4484
4561
  frame.partialInputs.set(decl.name.text, new Set(rt.props));
@@ -4515,7 +4592,7 @@ function bindDeclarations(p, frame, acc) {
4515
4592
  } else if (partial) bindPatternFrom(frame, decl.name, partial);
4516
4593
  }
4517
4594
  } else if (!isChainWithQuery(init) && derivedIn(frame, init)) {
4518
- bindInput(names, isWholeInput(init, wholeContext(frame)));
4595
+ bindInput(names, isWholeInput(init, wholeContext(p, frame)));
4519
4596
  }
4520
4597
  }
4521
4598
  if (frame.depth === 0) {
@@ -4619,7 +4696,7 @@ function analyzeFrame(p, frame, acc) {
4619
4696
  const payloadOf = (arg) => arg ? {
4620
4697
  text: arg.getText(sf).replace(/\s+/g, " ").slice(0, 200),
4621
4698
  inputDerived: derivedIn(frame, arg),
4622
- wholeInput: isWholeInput(arg, wholeContext(frame))
4699
+ wholeInput: isWholeInput(arg, wholeContext(p, frame))
4623
4700
  } : null;
4624
4701
  const storageHandles = storageBindingsIn(body);
4625
4702
  let callerScope = null;
@@ -4864,7 +4941,7 @@ function childFrame(p, call, target, frame) {
4864
4941
  pathPos: [...frame.pathPos, call.getStart(frame.sf)],
4865
4942
  exitPropagates: frame.exitPropagates && callResultChecked(call)
4866
4943
  };
4867
- const cx = wholeContext(frame);
4944
+ const cx = wholeContext(p, frame);
4868
4945
  target.fn.parameters.forEach((param, i) => {
4869
4946
  const arg = call.arguments[i];
4870
4947
  const ab = argBinding(p, arg, frame);
@@ -5157,6 +5234,51 @@ function calleePath(e) {
5157
5234
  if (ts11.isElementAccessExpression(u)) return `${calleePath(u.expression)}[]`;
5158
5235
  return "";
5159
5236
  }
5237
+ var EMPTY_GUARDS = { authChecks: [], roleChecks: [] };
5238
+ function layoutGuards(p, pageRel, cache) {
5239
+ const parts = pageRel.split("/");
5240
+ parts.pop();
5241
+ const out = { authChecks: [], roleChecks: [] };
5242
+ for (let i = parts.length; i > 0; i--) {
5243
+ const g = layoutGuardsFor(p, parts.slice(0, i).join("/"), cache);
5244
+ out.authChecks.push(...g.authChecks);
5245
+ out.roleChecks.push(...g.roleChecks);
5246
+ }
5247
+ return out;
5248
+ }
5249
+ var LAYOUT_EXTENSIONS = ["tsx", "ts", "jsx", "js"];
5250
+ function layoutGuardsFor(p, dir, cache) {
5251
+ const hit = cache.get(dir);
5252
+ if (hit) return hit;
5253
+ let guards = EMPTY_GUARDS;
5254
+ for (const ext of LAYOUT_EXTENSIONS) {
5255
+ const rel = `${dir}/layout.${ext}`;
5256
+ const sf = p.sources.get(rel);
5257
+ const facts = p.registry.get(rel);
5258
+ if (!sf || !facts || isClientComponentFile(sf)) continue;
5259
+ const fn = pageHandlerIn(sf);
5260
+ if (!fn) continue;
5261
+ try {
5262
+ const analysed = analyzeHandler(p, {
5263
+ rel,
5264
+ sf,
5265
+ facts,
5266
+ kind: "page",
5267
+ route: dir,
5268
+ method: "PAGE",
5269
+ fn: fn.fn,
5270
+ node: fn.node,
5271
+ wrapper: fn.wrapper
5272
+ });
5273
+ guards = { authChecks: analysed.authChecks, roleChecks: analysed.roleChecks ?? [] };
5274
+ } catch {
5275
+ guards = EMPTY_GUARDS;
5276
+ }
5277
+ break;
5278
+ }
5279
+ cache.set(dir, guards);
5280
+ return guards;
5281
+ }
5160
5282
  function analyzeHandler(p, h) {
5161
5283
  const { rel, sf, fn } = h;
5162
5284
  const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
@@ -5365,6 +5487,15 @@ function parseProject(rootInput, opts = {}) {
5365
5487
  storageBuckets: schema.storageBuckets
5366
5488
  };
5367
5489
  }
5490
+ var LAYOUT_CACHE = /* @__PURE__ */ new WeakMap();
5491
+ function layoutCacheOf(p) {
5492
+ let m = LAYOUT_CACHE.get(p);
5493
+ if (!m) {
5494
+ m = /* @__PURE__ */ new Map();
5495
+ LAYOUT_CACHE.set(p, m);
5496
+ }
5497
+ return m;
5498
+ }
5368
5499
  function analyzeFile(project, rel, sf, facts, out) {
5369
5500
  const { routes, exposures, fileIgnores } = out;
5370
5501
  {
@@ -5423,6 +5554,11 @@ function analyzeFile(project, rel, sf, facts, out) {
5423
5554
  node: handler.node,
5424
5555
  wrapper: handler.wrapper
5425
5556
  });
5557
+ const guards = layoutGuards(project, rel, layoutCacheOf(project));
5558
+ analysed.authChecks.push(...guards.authChecks);
5559
+ if (guards.roleChecks.length > 0) {
5560
+ analysed.roleChecks = [...analysed.roleChecks ?? [], ...guards.roleChecks];
5561
+ }
5426
5562
  if (analysed.queries.length > 0 || analysed.inputs.length > 0) routes.push(analysed);
5427
5563
  }
5428
5564
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditai-scan",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Deterministic security scanner for Next.js + Supabase apps: cross-tenant reads, RLS gaps, service-role misuse, mass assignment. No account, no model, seconds.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",