seaworthycode 1.1.2 → 1.2.3

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 (68) hide show
  1. package/dist/index.js +739 -42
  2. package/dist/index.js.map +1 -1
  3. package/package.json +14 -12
  4. package/rules/external/configuration/insecure-cookie-flags.test.yaml +114 -0
  5. package/rules/external/configuration/insecure-cookie-flags.yaml +77 -0
  6. package/rules/external/configuration/missing-ssl-verification.test.yaml +130 -0
  7. package/rules/external/configuration/missing-ssl-verification.yaml +123 -0
  8. package/rules/external/data-exposure/debug-artifacts.test.yaml +143 -0
  9. package/rules/external/data-exposure/debug-artifacts.yaml +57 -0
  10. package/rules/external/data-exposure/hardcoded-ip.test.yaml +131 -0
  11. package/rules/external/data-exposure/hardcoded-ip.yaml +67 -0
  12. package/rules/external/data-exposure/localstorage-sensitive-data.test.yaml +58 -0
  13. package/rules/external/data-exposure/localstorage-sensitive-data.yaml +60 -0
  14. package/rules/external/dependencies/deprecated-functions.test.yaml +92 -0
  15. package/rules/external/dependencies/deprecated-functions.yaml +66 -0
  16. package/rules/external/ops/hardcoded-port.test.yaml +141 -0
  17. package/rules/external/ops/hardcoded-port.yaml +91 -0
  18. package/rules/external/ops/insecure-file-permissions.test.yaml +111 -0
  19. package/rules/external/ops/insecure-file-permissions.yaml +91 -0
  20. package/rules/external/resilience/missing-memory-limit.test.yaml +54 -0
  21. package/rules/external/resilience/missing-memory-limit.yaml +59 -0
  22. package/rules/external/resilience/missing-timeout-config.test.yaml +179 -0
  23. package/rules/external/resilience/missing-timeout-config.yaml +124 -0
  24. package/rules/external/security/broken-crypto-algorithm.test.yaml +104 -0
  25. package/rules/external/security/broken-crypto-algorithm.yaml +79 -0
  26. package/rules/external/security/http-method-override.test.yaml +40 -0
  27. package/rules/external/security/http-method-override.yaml +35 -0
  28. package/rules/external/security/http-no-tls.test.yaml +115 -0
  29. package/rules/external/security/http-no-tls.yaml +59 -0
  30. package/rules/external/security/path-traversal.test.yaml +188 -0
  31. package/rules/external/security/path-traversal.yaml +210 -0
  32. package/rules/external/security/unsafe-innerhtml.test.yaml +72 -0
  33. package/rules/external/security/unsafe-innerhtml.yaml +46 -0
  34. package/rules/external/security/untrusted-deserialization.test.yaml +79 -0
  35. package/rules/external/security/untrusted-deserialization.yaml +48 -0
  36. package/rules/external/security/weak-encryption-mode.test.yaml +126 -0
  37. package/rules/external/security/weak-encryption-mode.yaml +104 -0
  38. package/rules/external/security/weak-hash-algorithm.test.yaml +121 -0
  39. package/rules/external/security/weak-hash-algorithm.yaml +87 -0
  40. package/rules/external/security/xxe-parsing.test.yaml +76 -0
  41. package/rules/external/security/xxe-parsing.yaml +63 -0
  42. package/rules/internal/security/dangerous-eval.test.yaml +126 -0
  43. package/rules/internal/security/dangerous-eval.yaml +134 -0
  44. package/rules/internal/security/no-document-write.test.yaml +33 -0
  45. package/rules/internal/security/no-document-write.yaml +40 -0
  46. package/rules/internal/taint/MATRIX.md +16 -0
  47. package/rules/internal/taint/bash.test.yaml +84 -0
  48. package/rules/internal/taint/bash.yaml +166 -0
  49. package/rules/internal/taint/dvwa.test.yaml +217 -0
  50. package/rules/internal/taint/go.test.yaml +207 -0
  51. package/rules/internal/taint/go.yaml +325 -0
  52. package/rules/internal/taint/java.test.yaml +158 -0
  53. package/rules/internal/taint/java.yaml +318 -0
  54. package/rules/internal/taint/javascript.test.yaml +374 -0
  55. package/rules/internal/taint/javascript.yaml +439 -0
  56. package/rules/internal/taint/php.test.yaml +134 -0
  57. package/rules/internal/taint/php.yaml +331 -0
  58. package/rules/internal/taint/python.test.yaml +427 -0
  59. package/rules/internal/taint/python.yaml +636 -0
  60. package/rules/internal/taint/ruby.test.yaml +148 -0
  61. package/rules/internal/taint/ruby.yaml +233 -0
  62. package/rules/internal/taint/rust.test.yaml +585 -0
  63. package/rules/internal/taint/rust.yaml +483 -0
  64. package/rules/internal/taint/tsx.test.yaml +140 -0
  65. package/rules/internal/taint/tsx.yaml +51 -0
  66. package/rules/internal/taint/typescript.test.yaml +393 -0
  67. package/rules/internal/taint/typescript.yaml +660 -0
  68. package/wasm/tree-sitter-sql.wasm +0 -0
package/dist/index.js CHANGED
@@ -6,13 +6,14 @@ import { existsSync as existsSync6 } from "fs";
6
6
  import { dirname as dirname7, resolve as resolve4 } from "path";
7
7
  import { fileURLToPath as fileURLToPath3 } from "url";
8
8
  import { cac } from "cac";
9
- import { createRequire as createRequire2 } from "module";
9
+ import { createRequire as createRequire3 } from "module";
10
10
 
11
11
  // src/cli-action.ts
12
12
  import { stat, access as access2 } from "fs/promises";
13
13
  import { resolve as resolve3 } from "path";
14
14
  import { homedir as homedir3 } from "os";
15
15
  import { join as join15 } from "path";
16
+ import { createRequire as createRequire2 } from "module";
16
17
 
17
18
  // ../core/dist/index.js
18
19
  import { readFileSync } from "fs";
@@ -36,6 +37,8 @@ import { createHash } from "crypto";
36
37
  import { isAbsolute, relative as relative3 } from "path";
37
38
  import { pathToFileURL } from "url";
38
39
  import { z } from "zod";
40
+ import { lookup } from "dns/promises";
41
+ import { request as httpsRequest } from "https";
39
42
  import { jwtVerify, createLocalJWKSet } from "jose";
40
43
  import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
41
44
  import { join as join6, resolve as pathResolve2 } from "path";
@@ -515,7 +518,9 @@ function sortFindings(findings) {
515
518
  const fileA = a.file ?? "";
516
519
  const fileB = b.file ?? "";
517
520
  if (fileA !== fileB) return fileA.localeCompare(fileB);
518
- return (a.line ?? 0) - (b.line ?? 0);
521
+ const lineDiff = (a.line ?? 0) - (b.line ?? 0);
522
+ if (lineDiff !== 0) return lineDiff;
523
+ return a.checkId.localeCompare(b.checkId);
519
524
  });
520
525
  }
521
526
  function dedupeDegradedFindings(findings) {
@@ -1438,6 +1443,52 @@ var copy = {
1438
1443
  "checks.sql-missing-rls.message": (match) => `RLS is not enabled or lacks restriction on a private/tenant table: ${match}`,
1439
1444
  "checks.sql-missing-rls.degraded": (path, language) => `${language} analysis was incomplete for ${path} (no grammar available)`,
1440
1445
  "remediation.security.sql-missing-rls": "Define a policy with CREATE POLICY ... ON ... USING (...) to restrict data access to authorized users.",
1446
+ // Security headers
1447
+ "checks.missing-security-headers.name": "Missing HTTP security headers",
1448
+ "checks.missing-security-headers.description": "Application does not set HTTP security response headers (HSTS, X-Content-Type-Options, X-Frame-Options, Permissions-Policy)",
1449
+ "remediation.security.missing-security-headers": "Add helmet() (Node.js) or equivalent security-header middleware. At minimum set Strict-Transport-Security, X-Content-Type-Options: nosniff, X-Frame-Options, and Permissions-Policy.",
1450
+ // GitHub Actions supply-chain
1451
+ "checks.gh-actions-unpinned.name": "Unpinned GitHub Actions",
1452
+ "checks.gh-actions-unpinned.description": "GitHub Actions workflow steps reference third-party actions by mutable tag rather than a full commit SHA",
1453
+ "remediation.security.gh-actions-unpinned": "Pin every third-party action to a full 40-character commit SHA, e.g. uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683. Use a tool like Renovate or pin-github-action to automate this.",
1454
+ "checks.gh-actions-excessive-permissions.name": "Excessive GitHub Actions permissions",
1455
+ "checks.gh-actions-excessive-permissions.description": "GitHub Actions workflow grants overly broad or undeclared default permissions",
1456
+ "remediation.security.gh-actions-excessive-permissions": 'Add a top-level "permissions: {}" block and grant only the minimum scopes needed per job, e.g. "permissions: { contents: read }".',
1457
+ // GraphQL
1458
+ "checks.graphql-introspection-prod.name": "GraphQL introspection in production",
1459
+ "checks.graphql-introspection-prod.description": "GraphQL server exposes introspection without disabling it for production environments",
1460
+ "remediation.security.graphql-introspection-prod": 'Disable GraphQL introspection in production: set introspection: process.env.NODE_ENV !== "production" in Apollo Server, or use a validationRules plugin that blocks introspection queries.',
1461
+ "checks.graphql-no-depth-limit.name": "GraphQL missing depth limit",
1462
+ "checks.graphql-no-depth-limit.description": "GraphQL server has no query depth or complexity limiting, enabling denial-of-service via deeply nested queries",
1463
+ "remediation.security.graphql-no-depth-limit": "Add query depth limiting with graphql-depth-limit or complexity analysis with graphql-query-complexity. Wire the validation rule into ApolloServer validationRules or the equivalent for your framework.",
1464
+ // JWT
1465
+ "checks.jwt-alg-none.name": 'JWT allows algorithm "none"',
1466
+ "checks.jwt-alg-none.description": 'JWT verification does not restrict algorithms, allowing forged unsigned tokens via the "none" algorithm',
1467
+ "remediation.security.jwt-alg-none": 'Always pass an explicit algorithms allowlist to jwt.verify() (e.g. { algorithms: ["HS256"] }). Never accept "none" as a valid algorithm.',
1468
+ "checks.jwt-no-expiry.name": "JWT issued without expiry",
1469
+ "checks.jwt-no-expiry.description": "JWT tokens are signed without an expiry claim (exp / expiresIn), making them valid indefinitely",
1470
+ "remediation.security.jwt-no-expiry": 'Set a short expiry on all issued JWTs using expiresIn (jsonwebtoken), .setExpirationTime() (jose), or an "exp" claim in the payload (PyJWT). Use refresh tokens to extend sessions.',
1471
+ // Browser token storage
1472
+ "checks.refresh-token-in-localstorage.name": "Refresh token in localStorage",
1473
+ "checks.refresh-token-in-localstorage.description": "Refresh tokens or long-lived credentials stored in localStorage are vulnerable to theft via XSS",
1474
+ "remediation.security.refresh-token-in-localstorage": "Store refresh tokens in httpOnly, Secure, SameSite=Strict cookies \u2014 not localStorage or sessionStorage. XSS cannot read httpOnly cookies.",
1475
+ // Dockerfile hardening
1476
+ "checks.dockerfile-runs-as-root.name": "Dockerfile runs as root",
1477
+ "checks.dockerfile-runs-as-root.description": "Container image has no non-root USER directive \u2014 all processes run as root",
1478
+ "remediation.ops.dockerfile-runs-as-root": "Create a non-root user and switch to it before CMD/ENTRYPOINT: RUN addgroup -S app && adduser -S app -G app followed by USER app.",
1479
+ "checks.dockerfile-secrets-in-env.name": "Secrets in Dockerfile ENV",
1480
+ "checks.dockerfile-secrets-in-env.description": "Dockerfile bakes secrets or credentials into image layers via ENV or ARG instructions",
1481
+ "remediation.ops.dockerfile-secrets-in-env": "Never pass secrets via ENV or ARG \u2014 they persist in image history. Use Docker BuildKit secrets (RUN --mount=type=secret), environment variables injected at container start, or a secrets manager.",
1482
+ "checks.dockerfile-no-healthcheck.name": "Dockerfile missing HEALTHCHECK",
1483
+ "checks.dockerfile-no-healthcheck.description": "Dockerfile has no HEALTHCHECK instruction \u2014 container orchestrators cannot detect unhealthy state",
1484
+ "remediation.ops.dockerfile-no-healthcheck": "Add a HEALTHCHECK instruction, e.g.: HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8080/health || exit 1",
1485
+ "checks.missing-dockerignore.name": "Missing .dockerignore",
1486
+ "checks.missing-dockerignore.description": "Dockerfile is present without a .dockerignore \u2014 build context may include node_modules, .env files, or other sensitive data",
1487
+ "remediation.ops.missing-dockerignore": "Add a .dockerignore file listing directories and files to exclude from the build context: node_modules, .env, .git, *.log, dist, coverage.",
1488
+ // ORM N+1
1489
+ "checks.orm-n-plus-one.name": "ORM N+1 query pattern",
1490
+ "checks.orm-n-plus-one.description": "ORM results are iterated with a per-row database query instead of a single batch load",
1491
+ "remediation.resilience.orm-n-plus-one": "Replace per-row queries inside loops with a single batch query using include/eager loading (Prisma include, Sequelize eager loading, SQLAlchemy joinedload, ActiveRecord includes).",
1441
1492
  // Reporting labels
1442
1493
  "reporting.analysisType.pattern": "pattern",
1443
1494
  "reporting.analysisType.semantic": "semantic"
@@ -1559,36 +1610,36 @@ var ScanRunner = class {
1559
1610
  astCache: /* @__PURE__ */ new Map()
1560
1611
  };
1561
1612
  const results = [];
1562
- const mutex = [];
1563
- const inFlight = /* @__PURE__ */ new Map();
1564
- const findingsPerCategory = /* @__PURE__ */ new Map();
1565
- for (const check2 of checks) {
1566
- inFlight.set(check2.category, (inFlight.get(check2.category) ?? 0) + 1);
1567
- }
1613
+ const checkResults = /* @__PURE__ */ new Map();
1568
1614
  await runWithConcurrency(
1569
1615
  checks,
1570
1616
  async (check2) => {
1571
1617
  try {
1572
1618
  const findings2 = await check2.run(ctx);
1573
- mutex.push(...findings2);
1574
- if (options.onCategoryComplete) {
1575
- findingsPerCategory.set(check2.category, (findingsPerCategory.get(check2.category) ?? 0) + findings2.length);
1576
- }
1619
+ checkResults.set(check2.id, findings2);
1577
1620
  } catch (err) {
1578
1621
  console.error(`Check "${check2.id}" failed: ${String(err)}`);
1579
- } finally {
1580
- if (options.onCategoryComplete) {
1581
- const remaining = (inFlight.get(check2.category) ?? 1) - 1;
1582
- inFlight.set(check2.category, remaining);
1583
- if (remaining === 0) {
1584
- options.onCategoryComplete(check2.category, findingsPerCategory.get(check2.category) ?? 0);
1585
- }
1586
- }
1587
1622
  }
1588
1623
  },
1589
1624
  concurrency
1590
1625
  );
1591
- results.push(...mutex);
1626
+ for (const check2 of checks) {
1627
+ results.push(...checkResults.get(check2.id) ?? []);
1628
+ }
1629
+ if (options.onCategoryComplete) {
1630
+ const findingsPerCategory = /* @__PURE__ */ new Map();
1631
+ for (const check2 of checks) {
1632
+ const count = (checkResults.get(check2.id) ?? []).length;
1633
+ findingsPerCategory.set(check2.category, (findingsPerCategory.get(check2.category) ?? 0) + count);
1634
+ }
1635
+ const seen = /* @__PURE__ */ new Set();
1636
+ for (const check2 of checks) {
1637
+ if (!seen.has(check2.category)) {
1638
+ seen.add(check2.category);
1639
+ options.onCategoryComplete(check2.category, findingsPerCategory.get(check2.category) ?? 0);
1640
+ }
1641
+ }
1642
+ }
1592
1643
  if (ctx.astCache) {
1593
1644
  for (const promise of ctx.astCache.values()) {
1594
1645
  try {
@@ -3132,16 +3183,60 @@ var REFRESH_WINDOW_MS = 5 * 60 * 1e3;
3132
3183
  var LicenseClient = class {
3133
3184
  baseUrl;
3134
3185
  timeout;
3186
+ userAgent;
3135
3187
  cache = null;
3136
3188
  jwksFetched = false;
3137
3189
  constructor(config = {}) {
3138
3190
  this.baseUrl = config.baseUrl ?? LICENSE_SERVER_URL;
3139
3191
  this.timeout = config.timeout ?? 1e4;
3192
+ this.userAgent = config.userAgent ?? "seaworthycode/unknown";
3193
+ }
3194
+ async fetchWithRetry(url, init) {
3195
+ try {
3196
+ return await fetch(url, init);
3197
+ } catch (err) {
3198
+ if (!(err instanceof TypeError)) throw err;
3199
+ return this.fetchIpv4(url, init);
3200
+ }
3201
+ }
3202
+ async fetchIpv4(url, init) {
3203
+ const parsed = new URL(url);
3204
+ const { address } = await lookup(parsed.hostname, { family: 4 });
3205
+ const initHeaders = init.headers ?? {};
3206
+ const headers = { ...initHeaders, Host: parsed.hostname };
3207
+ return new Promise((resolve22, reject) => {
3208
+ const options = {
3209
+ hostname: address,
3210
+ port: parseInt(parsed.port || "443"),
3211
+ path: parsed.pathname + (parsed.search || ""),
3212
+ method: init.method ?? "GET",
3213
+ headers,
3214
+ servername: parsed.hostname,
3215
+ signal: init.signal ?? void 0
3216
+ };
3217
+ const req = httpsRequest(options, (res) => {
3218
+ const chunks = [];
3219
+ res.on("data", (chunk) => chunks.push(chunk));
3220
+ res.on("end", () => {
3221
+ const resHeaders = new Headers();
3222
+ for (const [k, v] of Object.entries(res.headers)) {
3223
+ if (typeof v === "string") resHeaders.set(k, v);
3224
+ else if (Array.isArray(v)) resHeaders.set(k, v.join(", "));
3225
+ }
3226
+ resolve22(new Response(Buffer.concat(chunks), { status: res.statusCode ?? 0, headers: resHeaders }));
3227
+ });
3228
+ res.on("error", reject);
3229
+ });
3230
+ req.on("error", reject);
3231
+ if (init.body) req.write(init.body);
3232
+ req.end();
3233
+ });
3140
3234
  }
3141
3235
  async ensureJwks() {
3142
3236
  if (this.jwksFetched) return;
3143
- const response = await fetch(`${this.baseUrl}/.well-known/jwks.json`, {
3144
- signal: AbortSignal.timeout(this.timeout)
3237
+ const response = await this.fetchWithRetry(`${this.baseUrl}/.well-known/jwks.json`, {
3238
+ signal: AbortSignal.timeout(this.timeout),
3239
+ headers: { "User-Agent": this.userAgent }
3145
3240
  });
3146
3241
  if (!response.ok) {
3147
3242
  throw new Error("jwks.fetch_failed");
@@ -3165,9 +3260,9 @@ var LicenseClient = class {
3165
3260
  }
3166
3261
  try {
3167
3262
  await this.ensureJwks();
3168
- const response = await fetch(`${this.baseUrl}/validate`, {
3263
+ const response = await this.fetchWithRetry(`${this.baseUrl}/validate`, {
3169
3264
  method: "POST",
3170
- headers: { "Content-Type": "application/json" },
3265
+ headers: { "Content-Type": "application/json", "User-Agent": this.userAgent },
3171
3266
  body: JSON.stringify({ licenseKey: licenseKey ?? "free" }),
3172
3267
  signal: AbortSignal.timeout(this.timeout)
3173
3268
  });
@@ -3178,7 +3273,8 @@ var LicenseClient = class {
3178
3273
  return fail("license.rate_limited");
3179
3274
  }
3180
3275
  if (!response.ok) {
3181
- return fail("license.server_error");
3276
+ const rayId = response.headers.get("cf-ray") ?? "";
3277
+ return fail("license.server_error", `HTTP ${response.status}${rayId ? ` ray=${rayId}` : ""}`);
3182
3278
  }
3183
3279
  const data = await response.json();
3184
3280
  const parsed = licenseResponseSchema.safeParse(data);
@@ -3207,12 +3303,12 @@ var LicenseClient = class {
3207
3303
  });
3208
3304
  } catch (err) {
3209
3305
  if (err instanceof DOMException && err.name === "TimeoutError") {
3210
- return fail("license.server_unreachable");
3306
+ return fail("license.server_unreachable", "timeout");
3211
3307
  }
3212
3308
  if (err instanceof TypeError) {
3213
- return fail("license.server_unreachable");
3309
+ return fail("license.server_unreachable", err.message);
3214
3310
  }
3215
- return fail("license.server_unreachable");
3311
+ return fail("license.server_unreachable", String(err));
3216
3312
  }
3217
3313
  }
3218
3314
  shouldRefresh() {
@@ -3754,6 +3850,22 @@ function mergeFindings(tsFindings, otherFindings) {
3754
3850
  }
3755
3851
  return Array.from(map.values());
3756
3852
  }
3853
+ var ENV_SUBSTITUTION_PATTERNS = [
3854
+ /^env\(\s*[A-Za-z_][A-Za-z0-9_]*\s*\)$/,
3855
+ /^\$\{[A-Za-z_][A-Za-z0-9_]*(?::[-=+?][^}]*)?\}$/,
3856
+ /^\$[A-Za-z_][A-Za-z0-9_]*$/,
3857
+ /^%[A-Za-z_][A-Za-z0-9_]*%$/,
3858
+ /^<%=?\s*ENV\[['"][A-Za-z_][A-Za-z0-9_]*['"]\]\s*%>$/,
3859
+ /^process\.env(?:\.[A-Za-z_][A-Za-z0-9_]*|\[['"][A-Za-z_][A-Za-z0-9_]*['"]\])$/,
3860
+ /^os\.(?:environ\[['"][A-Za-z_][A-Za-z0-9_]*['"]\]|getenv\(['"][A-Za-z_][A-Za-z0-9_]*['"]\))$/
3861
+ ];
3862
+ function isEnvSubstitution(value) {
3863
+ const trimmed = value.trim();
3864
+ for (const pattern of ENV_SUBSTITUTION_PATTERNS) {
3865
+ if (pattern.test(trimmed)) return true;
3866
+ }
3867
+ return false;
3868
+ }
3757
3869
  var CHECK_ID = "security.hardcoded-secrets";
3758
3870
  var SECRET_PATTERNS = [
3759
3871
  { regex: /AWS_SECRET_ACCESS_KEY\s*=\s*['"]([A-Za-z0-9/+=]{40})['"]/gi, name: "AWS secret access key", minLength: 40, method: "known-format" },
@@ -3789,6 +3901,8 @@ var regexCheck = {
3789
3901
  const matches = Array.from(line.matchAll(pattern.regex));
3790
3902
  for (let matchIdx = 0; matchIdx < matches.length; matchIdx++) {
3791
3903
  const match = matches[matchIdx];
3904
+ const captured = match[1] ?? match[0];
3905
+ if (isEnvSubstitution(captured)) continue;
3792
3906
  findings.push({
3793
3907
  id: `${CHECK_ID}:${file.relativePath}:${lineNum + 1}:${(match.index ?? 0) + 1}:${matchIdx}`,
3794
3908
  checkId: CHECK_ID,
@@ -3945,6 +4059,7 @@ var tsCheck = createTreeSitterCheck({
3945
4059
  const trimmedValue = value.trim();
3946
4060
  if (!/^['"`].*['"`]$/.test(trimmedValue)) return false;
3947
4061
  const cleanValue = trimmedValue.replace(/^['"`]|['"`]$/g, "");
4062
+ if (isEnvSubstitution(cleanValue)) return false;
3948
4063
  for (const rule of NAME_RULES) {
3949
4064
  if (rule.regex.test(name) && cleanValue.length >= rule.minLength) {
3950
4065
  return true;
@@ -5203,7 +5318,7 @@ var MIN_VALUE_LENGTH = 8;
5203
5318
  var SECRET_ASSIGNMENT_PATTERNS = [
5204
5319
  /(?:const|let|var)\s+(\w*(?:secret|api[_-]?key|access[_-]?key|private[_-]?key|auth[_-]?token|bearer[_-]?token|refresh[_-]?token|apiKey|accessKey|privateKey|authToken|bearerToken|refreshToken)\w*)\s*=\s*['"]([^'"]{8,})['"]/i,
5205
5320
  /(\w*(?:secret|api[_-]?key|access[_-]?key|private[_-]?key|auth[_-]?token|bearer[_-]?token|refresh[_-]?token|apiKey|accessKey|privateKey|authToken|bearerToken|refreshToken)\w*)\s*:\s*['"]([^'"]{8,})['"]/i,
5206
- /(?:<!--|\/\*)[^]*?(\w*(?:secret|api[_-]?key|access[_-]?key|private[_-]?key|auth[_-]?token|bearer[_-]?token|refresh[_-]?token|apiKey|accessKey|privateKey|authToken|bearerToken|refreshToken)\w*)\s*[:=]\s*['"]([^'"]{8,})['"]/i
5321
+ /(?:<!--|\/\*)[^]{0,300}?(\w*(?:secret|api[_-]?key|access[_-]?key|private[_-]?key|auth[_-]?token|bearer[_-]?token|refresh[_-]?token|apiKey|accessKey|privateKey|authToken|bearerToken|refreshToken)\w*)\s*[:=]\s*['"]([^'"]{8,})['"]/i
5207
5322
  ];
5208
5323
  var REMEDIATION9 = getCopy("remediation.configuration.secrets-in-source");
5209
5324
  function stripQuotes(value) {
@@ -5233,7 +5348,9 @@ var regexCheck3 = {
5233
5348
  const name = match[1] ?? "";
5234
5349
  const value = match[2] ?? "";
5235
5350
  if (!isCompoundSecretName(name)) continue;
5236
- if (stripQuotes(value).length < MIN_VALUE_LENGTH) continue;
5351
+ const cleanValue = stripQuotes(value);
5352
+ if (cleanValue.length < MIN_VALUE_LENGTH) continue;
5353
+ if (isEnvSubstitution(cleanValue)) continue;
5237
5354
  const before = content.slice(0, match.index ?? 0);
5238
5355
  const line = before.split("\n").length;
5239
5356
  const col = (before.split("\n").at(-1) ?? "").length + 1;
@@ -5262,7 +5379,9 @@ var regexCheck3 = {
5262
5379
  const name = match[1] ?? "";
5263
5380
  const value = match[2] ?? "";
5264
5381
  if (!isCompoundSecretName(name)) continue;
5265
- if (stripQuotes(value).length < MIN_VALUE_LENGTH) continue;
5382
+ const cleanValue = stripQuotes(value);
5383
+ if (cleanValue.length < MIN_VALUE_LENGTH) continue;
5384
+ if (isEnvSubstitution(cleanValue)) continue;
5266
5385
  findings.push({
5267
5386
  id: `${CHECK_ID5}:${file.relativePath}:${lineNum + 1}:${(match.index ?? 0) + 1}:${matchIdx}`,
5268
5387
  checkId: CHECK_ID5,
@@ -5398,7 +5517,9 @@ var tsCheck3 = createTreeSitterCheck({
5398
5517
  const value = captures.value;
5399
5518
  if (!name || !value) return false;
5400
5519
  if (!isCompoundSecretName(name)) return false;
5401
- return stripQuotes(value).length >= MIN_VALUE_LENGTH;
5520
+ const cleanValue = stripQuotes(value);
5521
+ if (cleanValue.length < MIN_VALUE_LENGTH) return false;
5522
+ return !isEnvSubstitution(cleanValue);
5402
5523
  },
5403
5524
  confidenceFactory: (captures) => mechanicalConfidence("ast", void 0, captures.value ?? "")
5404
5525
  });
@@ -5628,10 +5749,25 @@ var ENTRYPOINT_PATTERNS = [
5628
5749
  /(?:^|[\\/])bin[\\/]rails$/i,
5629
5750
  /(?:^|[\\/])application_controller\.rb$/i
5630
5751
  ];
5752
+ var DOCKERFILE_CHECK_IDS = /* @__PURE__ */ new Set([
5753
+ "ops.dockerfile-runs-as-root",
5754
+ "ops.dockerfile-secrets-in-env",
5755
+ "ops.dockerfile-no-healthcheck"
5756
+ ]);
5757
+ var GH_ACTIONS_CHECK_IDS = /* @__PURE__ */ new Set([
5758
+ "security.gh-actions-unpinned",
5759
+ "security.gh-actions-excessive-permissions"
5760
+ ]);
5631
5761
  function isProCheckEligible(file, template) {
5632
5762
  if (template?.checkId === "security.sql-missing-rls") {
5633
5763
  return file.language === "sql" && !isDocumentationPath(file.relativePath) && !isNonProductionPath(file.relativePath);
5634
5764
  }
5765
+ if (template?.checkId && DOCKERFILE_CHECK_IDS.has(template.checkId)) {
5766
+ return file.language === "dockerfile" && !isDocumentationPath(file.relativePath) && !isNonProductionPath(file.relativePath);
5767
+ }
5768
+ if (template?.checkId && GH_ACTIONS_CHECK_IDS.has(template.checkId)) {
5769
+ return file.language === "yaml" && /(?:^|[\\/])\.github[\\/]workflows[\\/]/i.test(file.relativePath) && !isDocumentationPath(file.relativePath) && !isNonProductionPath(file.relativePath);
5770
+ }
5635
5771
  return PRO_CHECK_SOURCE_LANGUAGES.has(file.language) && !isDocumentationPath(file.relativePath) && !isNonProductionPath(file.relativePath);
5636
5772
  }
5637
5773
  function pickProjectAnchorFile(files) {
@@ -7519,6 +7655,10 @@ registry.register({
7519
7655
  }
7520
7656
  });
7521
7657
  var CHECK_ID10 = "security.xss-surface";
7658
+ var CGI_PATH_HINT = /(^|[\/_-])cgi([\/_.-]|$)/i;
7659
+ function isLikelyCgiBashScript(file) {
7660
+ return CGI_PATH_HINT.test(file.relativePath);
7661
+ }
7522
7662
  var QUERIES = {
7523
7663
  javascript: `
7524
7664
  (call_expression
@@ -7802,7 +7942,10 @@ registry.register(
7802
7942
  queries: QUERIES,
7803
7943
  messageKey: "checks.xss-surface.message",
7804
7944
  degradedMessageKey: "checks.xss-surface.degraded",
7805
- remediationKey: "remediation.security.xss-surface"
7945
+ remediationKey: "remediation.security.xss-surface",
7946
+ // Bash echo $VAR writes to stdout, not a browser. Only treat bash
7947
+ // scripts as XSS-relevant when they look like CGI handlers.
7948
+ fileFilter: (file) => file.language !== "bash" || isLikelyCgiBashScript(file)
7806
7949
  })
7807
7950
  );
7808
7951
  var CHECK_ID11 = "security.os-command-injection";
@@ -10234,7 +10377,7 @@ registry.register({
10234
10377
  });
10235
10378
  var CHECK_ID40 = "security.weak-email-validation";
10236
10379
  var REMEDIATION34 = getCopy("remediation.security.weak-email-validation");
10237
- var WEAK_EMAIL_REGEX_LINE = /\/\^[^\/]*@[^\/]*\\\.[^\/]{0,4}\$\/[gimsuy]*/g;
10380
+ var WEAK_EMAIL_REGEX_LINE = /\/\^[^/]*@[^/]*\\\.[^/]{0,4}\$\/[gimsuy]*/g;
10238
10381
  var LOOSE_REGEX_CLASS = /\[\^?\\?s@\]\+@\[\^?\\?s@\]\+\\\.\[\^?\\?s@\]\+/g;
10239
10382
  var EMAIL_CONTEXT = /email|e-?mail|validateEmail|isEmail|checkEmail|verifyEmail|deliverable/is;
10240
10383
  function lineAndColumn15(content, index) {
@@ -10290,7 +10433,7 @@ registry.register({
10290
10433
  var CHECK_ID41 = "security.idempotency-key-leak";
10291
10434
  var REMEDIATION35 = getCopy("remediation.security.idempotency-key-leak");
10292
10435
  var IDEMPOTENCY_CONTEXT = /idempotency[_-]?key|idempotency[_-]?token/i;
10293
- var TEMPLATE_LITERAL_INTERPOLATION = /\`[^`]*\$\{[^}]+\}[^`]*\`/g;
10436
+ var TEMPLATE_LITERAL_INTERPOLATION = new RegExp("`[^`]*\\$\\{[^}]+\\}[^`]*`", "g");
10294
10437
  var STRUCTURED_ID_PATTERN = /(?:lic(?:ense|ence)|sub(?:scription)?|order|cust(?:omer)?|user|tx|pay(?:ment)?|inv(?:oice)?)[-/]/i;
10295
10438
  function lineAndColumn16(content, index) {
10296
10439
  const prefix = content.slice(0, index);
@@ -14730,6 +14873,556 @@ registry.register({
14730
14873
  }
14731
14874
  }
14732
14875
  });
14876
+ var PROMPT_VERSION31 = "1.0.0";
14877
+ var fallbackPatterns31 = [
14878
+ {
14879
+ regex: /helmet\s*\(/i,
14880
+ message: "No HTTP security headers middleware detected",
14881
+ severity: "medium",
14882
+ absenceBased: true
14883
+ }
14884
+ ];
14885
+ async function buildPrompt31(sourceFiles) {
14886
+ const parts = [
14887
+ "Check if the application configures HTTP security response headers.",
14888
+ "In Node.js (Express, Fastify, Nest), look for helmet() middleware or explicit setHeader calls for: Strict-Transport-Security, X-Content-Type-Options: nosniff, X-Frame-Options (or Content-Security-Policy with frame-ancestors), and Permissions-Policy.",
14889
+ "In Python Django, check for SECURE_HSTS_SECONDS, SECURE_CONTENT_TYPE_NOSNIFF, X_FRAME_OPTIONS in settings files.",
14890
+ "In Python FastAPI/Flask, check for SecurityHeadersMiddleware or manual response.headers assignments covering these headers.",
14891
+ "Flag apps that initialise a server without any of these security headers. Prefer one project-level finding rather than one per route file. Do NOT flag if helmet() or a known security-header middleware is present."
14892
+ ];
14893
+ for (const file of sourceFiles) {
14894
+ const content = await file.content();
14895
+ parts.push(`--- ${file.relativePath} ---
14896
+ ${content.slice(0, 3e3)}`);
14897
+ }
14898
+ return parts.join("\n\n");
14899
+ }
14900
+ function parseResponse31(response, checkId) {
14901
+ return parseLLMResponse(response, checkId, "medium");
14902
+ }
14903
+ var promptTemplate31 = {
14904
+ checkId: "security.missing-security-headers",
14905
+ promptVersion: PROMPT_VERSION31,
14906
+ projectScoped: true,
14907
+ buildPrompt: buildPrompt31,
14908
+ parseResponse: parseResponse31,
14909
+ fallbackPatterns: fallbackPatterns31
14910
+ };
14911
+ registry.register({
14912
+ id: "security.missing-security-headers",
14913
+ name: "Missing security headers",
14914
+ description: "Application does not set HTTP security response headers",
14915
+ category: "security",
14916
+ severity: "medium",
14917
+ minTier: "pro",
14918
+ async run(ctx) {
14919
+ return runLLMCheck(ctx, promptTemplate31);
14920
+ }
14921
+ });
14922
+ var PROMPT_VERSION32 = "1.0.0";
14923
+ var fallbackPatterns32 = [
14924
+ {
14925
+ regex: /uses:\s+(?!\.\/)\S+@(?![0-9a-f]{40}\b)\S+/,
14926
+ message: "GitHub Actions step uses an unpinned action reference (not a 40-char commit SHA)",
14927
+ severity: "medium"
14928
+ }
14929
+ ];
14930
+ async function buildPrompt32(sourceFiles) {
14931
+ const parts = [
14932
+ "Check GitHub Actions workflow files for third-party action steps not pinned to a full 40-character commit SHA.",
14933
+ "Safe: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683",
14934
+ "Unsafe (flag these): uses: actions/checkout@v4, uses: actions/checkout@main, uses: actions/checkout@latest, uses: actions/checkout@master",
14935
+ "First-party actions (uses: ./ prefix) are always acceptable \u2014 do not flag them.",
14936
+ 'Flag every "uses:" line referencing a third-party action without a 40-hex-char commit SHA.'
14937
+ ];
14938
+ for (const file of sourceFiles) {
14939
+ const content = await file.content();
14940
+ parts.push(`--- ${file.relativePath} ---
14941
+ ${content.slice(0, 4e3)}`);
14942
+ }
14943
+ return parts.join("\n\n");
14944
+ }
14945
+ function parseResponse32(response, checkId) {
14946
+ return parseLLMResponse(response, checkId, "medium");
14947
+ }
14948
+ var promptTemplate32 = {
14949
+ checkId: "security.gh-actions-unpinned",
14950
+ promptVersion: PROMPT_VERSION32,
14951
+ buildPrompt: buildPrompt32,
14952
+ parseResponse: parseResponse32,
14953
+ fallbackPatterns: fallbackPatterns32
14954
+ };
14955
+ registry.register({
14956
+ id: "security.gh-actions-unpinned",
14957
+ name: "Unpinned GitHub Actions",
14958
+ description: "GitHub Actions steps reference third-party actions by mutable tag rather than commit SHA",
14959
+ category: "security",
14960
+ severity: "medium",
14961
+ minTier: "pro",
14962
+ async run(ctx) {
14963
+ return runLLMCheck(ctx, promptTemplate32);
14964
+ }
14965
+ });
14966
+ var PROMPT_VERSION33 = "1.0.0";
14967
+ var fallbackPatterns33 = [
14968
+ {
14969
+ regex: /permissions:\s*write-all/i,
14970
+ message: "Workflow grants write-all permissions",
14971
+ severity: "low"
14972
+ }
14973
+ ];
14974
+ async function buildPrompt33(sourceFiles) {
14975
+ const parts = [
14976
+ "Check GitHub Actions workflow files for excessive or undeclared permission grants.",
14977
+ `Flag workflows that: (1) use "permissions: write-all" at the top level or in a job, (2) have no top-level "permissions:" key at all (GitHub's default grants broad write access when omitted), or (3) grant write-level permissions far broader than the job's purpose (e.g. contents: write on a check that only reads).`,
14978
+ 'A well-secured workflow adds "permissions: {}" at the top level and grants only the minimum needed per job, e.g. "permissions: { contents: read }".',
14979
+ "Do NOT flag workflows that already have explicitly scoped, narrow permissions defined."
14980
+ ];
14981
+ for (const file of sourceFiles) {
14982
+ const content = await file.content();
14983
+ parts.push(`--- ${file.relativePath} ---
14984
+ ${content.slice(0, 4e3)}`);
14985
+ }
14986
+ return parts.join("\n\n");
14987
+ }
14988
+ function parseResponse33(response, checkId) {
14989
+ return parseLLMResponse(response, checkId, "low");
14990
+ }
14991
+ var promptTemplate33 = {
14992
+ checkId: "security.gh-actions-excessive-permissions",
14993
+ promptVersion: PROMPT_VERSION33,
14994
+ buildPrompt: buildPrompt33,
14995
+ parseResponse: parseResponse33,
14996
+ fallbackPatterns: fallbackPatterns33
14997
+ };
14998
+ registry.register({
14999
+ id: "security.gh-actions-excessive-permissions",
15000
+ name: "Excessive GitHub Actions permissions",
15001
+ description: "GitHub Actions workflow grants overly broad or undeclared permissions",
15002
+ category: "security",
15003
+ severity: "low",
15004
+ minTier: "pro",
15005
+ async run(ctx) {
15006
+ return runLLMCheck(ctx, promptTemplate33);
15007
+ }
15008
+ });
15009
+ var PROMPT_VERSION34 = "1.0.0";
15010
+ var fallbackPatterns34 = [
15011
+ {
15012
+ regex: /introspection:\s*true/i,
15013
+ message: "GraphQL introspection is explicitly enabled \u2014 disable in production",
15014
+ severity: "medium"
15015
+ }
15016
+ ];
15017
+ async function buildPrompt34(sourceFiles) {
15018
+ const parts = [
15019
+ "Check if a GraphQL server has introspection enabled for production traffic.",
15020
+ 'In Apollo Server (Node.js), flag ApolloServer instances with introspection: true where there is no production guard (e.g. process.env.NODE_ENV !== "production"), or where introspection is not explicitly set to false.',
15021
+ "In GraphQL Yoga or Mercurius, check for equivalent introspection settings.",
15022
+ "In Python (Strawberry, graphene), flag schema servers without a mechanism to disable introspection in non-development environments.",
15023
+ "Only flag when no environment guard exists that would prevent introspection in production. Return [] if introspection is already disabled or gated on a non-production environment check."
15024
+ ];
15025
+ for (const file of sourceFiles) {
15026
+ const content = await file.content();
15027
+ parts.push(`--- ${file.relativePath} ---
15028
+ ${content.slice(0, 3e3)}`);
15029
+ }
15030
+ return parts.join("\n\n");
15031
+ }
15032
+ function parseResponse34(response, checkId) {
15033
+ return parseLLMResponse(response, checkId, "medium");
15034
+ }
15035
+ var promptTemplate34 = {
15036
+ checkId: "security.graphql-introspection-prod",
15037
+ promptVersion: PROMPT_VERSION34,
15038
+ buildPrompt: buildPrompt34,
15039
+ parseResponse: parseResponse34,
15040
+ fallbackPatterns: fallbackPatterns34
15041
+ };
15042
+ registry.register({
15043
+ id: "security.graphql-introspection-prod",
15044
+ name: "GraphQL introspection in production",
15045
+ description: "GraphQL server exposes introspection without disabling it for production",
15046
+ category: "security",
15047
+ severity: "medium",
15048
+ minTier: "pro",
15049
+ async run(ctx) {
15050
+ return runLLMCheck(ctx, promptTemplate34);
15051
+ }
15052
+ });
15053
+ var PROMPT_VERSION35 = "1.0.0";
15054
+ var fallbackPatterns35 = [];
15055
+ async function buildPrompt35(sourceFiles) {
15056
+ const parts = [
15057
+ "Check if a GraphQL server enforces query depth or complexity limits.",
15058
+ "Flag GraphQL server setups (Apollo Server, GraphQL Yoga, Mercurius, Strawberry, graphene) that configure no query depth or complexity protection \u2014 none of: graphql-depth-limit, depthLimit, graphql-cost-analysis, graphql-query-complexity, or custom validationRules that restrict query shape.",
15059
+ "Without depth limiting, deeply nested queries cause exponential resolver work and can be used as a denial-of-service vector.",
15060
+ "Emit one project-level finding pointing at the server setup file. Return [] if any depth, complexity, or cost validation plugin is wired into the server, or if no GraphQL server is present."
15061
+ ];
15062
+ for (const file of sourceFiles) {
15063
+ const content = await file.content();
15064
+ parts.push(`--- ${file.relativePath} ---
15065
+ ${content.slice(0, 3e3)}`);
15066
+ }
15067
+ return parts.join("\n\n");
15068
+ }
15069
+ function parseResponse35(response, checkId) {
15070
+ return parseLLMResponse(response, checkId, "medium");
15071
+ }
15072
+ var promptTemplate35 = {
15073
+ checkId: "security.graphql-no-depth-limit",
15074
+ promptVersion: PROMPT_VERSION35,
15075
+ projectScoped: true,
15076
+ buildPrompt: buildPrompt35,
15077
+ parseResponse: parseResponse35,
15078
+ fallbackPatterns: fallbackPatterns35
15079
+ };
15080
+ registry.register({
15081
+ id: "security.graphql-no-depth-limit",
15082
+ name: "GraphQL no depth limit",
15083
+ description: "GraphQL server has no query depth or complexity limiting",
15084
+ category: "security",
15085
+ severity: "medium",
15086
+ minTier: "pro",
15087
+ async run(ctx) {
15088
+ return runLLMCheck(ctx, promptTemplate35);
15089
+ }
15090
+ });
15091
+ var PROMPT_VERSION36 = "1.0.0";
15092
+ var fallbackPatterns36 = [
15093
+ {
15094
+ regex: /algorithm[s'"`]*\s*[=:]\s*['"`]none['"`]/i,
15095
+ message: 'JWT algorithm set to "none" \u2014 token signatures are not verified',
15096
+ severity: "high"
15097
+ }
15098
+ ];
15099
+ async function buildPrompt36(sourceFiles) {
15100
+ const parts = [
15101
+ "Check for insecure JWT algorithm configurations.",
15102
+ 'Flag any explicit use of algorithm "none" in JWT signing or verification (jsonwebtoken, jose, PyJWT, ruby-jwt, etc.).',
15103
+ 'Also flag jwt.verify() or jwt.decode() calls that do NOT pass an "algorithms" allowlist \u2014 without one, an attacker can forge tokens by switching to the "none" algorithm.',
15104
+ "In Python PyJWT: flag jwt.decode() missing the algorithms=[...] parameter.",
15105
+ 'In Node.js jsonwebtoken: flag jwt.verify() without an algorithms option, or with algorithm/algorithms set to "none".',
15106
+ "In jose: flag jwtVerify() without explicit algorithms.",
15107
+ "Return [] if all verify/decode calls have an explicit non-empty algorithms allowlist."
15108
+ ];
15109
+ for (const file of sourceFiles) {
15110
+ const content = await file.content();
15111
+ parts.push(`--- ${file.relativePath} ---
15112
+ ${content.slice(0, 3e3)}`);
15113
+ }
15114
+ return parts.join("\n\n");
15115
+ }
15116
+ function parseResponse36(response, checkId) {
15117
+ return parseLLMResponse(response, checkId, "high");
15118
+ }
15119
+ var promptTemplate36 = {
15120
+ checkId: "security.jwt-alg-none",
15121
+ promptVersion: PROMPT_VERSION36,
15122
+ buildPrompt: buildPrompt36,
15123
+ parseResponse: parseResponse36,
15124
+ fallbackPatterns: fallbackPatterns36
15125
+ };
15126
+ registry.register({
15127
+ id: "security.jwt-alg-none",
15128
+ name: "JWT algorithm none",
15129
+ description: 'JWT verification allows the "none" algorithm or lacks an algorithms allowlist',
15130
+ category: "security",
15131
+ severity: "high",
15132
+ minTier: "pro",
15133
+ async run(ctx) {
15134
+ return runLLMCheck(ctx, promptTemplate36);
15135
+ }
15136
+ });
15137
+ var PROMPT_VERSION37 = "1.0.1";
15138
+ var fallbackPatterns37 = [];
15139
+ async function buildPrompt37(sourceFiles) {
15140
+ const parts = [
15141
+ "Check for JWT tokens issued without an expiry claim.",
15142
+ 'Flag jwt.sign(payload, secret) calls (jsonwebtoken) that have no "expiresIn" in the options argument.',
15143
+ "Flag new SignJWT(payload).sign(key) calls (jose) that do not chain .setExpirationTime(...).",
15144
+ 'Flag jwt.encode(payload, key) (PyJWT) where the payload dict has no "exp" key.',
15145
+ "Tokens without expiry are valid indefinitely \u2014 a stolen token cannot be invalidated through expiry.",
15146
+ 'Do NOT flag if the payload object itself already contains an "exp" property set before the sign call.',
15147
+ "Do NOT flag short-lived access tokens that have expiresIn set."
15148
+ ];
15149
+ for (const file of sourceFiles) {
15150
+ const content = await file.content();
15151
+ parts.push(`--- ${file.relativePath} ---
15152
+ ${content.slice(0, 3e3)}`);
15153
+ }
15154
+ return parts.join("\n\n");
15155
+ }
15156
+ function parseResponse37(response, checkId) {
15157
+ return parseLLMResponse(response, checkId, "medium");
15158
+ }
15159
+ var promptTemplate37 = {
15160
+ checkId: "security.jwt-no-expiry",
15161
+ promptVersion: PROMPT_VERSION37,
15162
+ buildPrompt: buildPrompt37,
15163
+ parseResponse: parseResponse37,
15164
+ fallbackPatterns: fallbackPatterns37
15165
+ };
15166
+ registry.register({
15167
+ id: "security.jwt-no-expiry",
15168
+ name: "JWT issued without expiry",
15169
+ description: "JWT tokens are signed without an expiry claim, making them valid indefinitely",
15170
+ category: "security",
15171
+ severity: "medium",
15172
+ minTier: "pro",
15173
+ async run(ctx) {
15174
+ return runLLMCheck(ctx, promptTemplate37);
15175
+ }
15176
+ });
15177
+ var PROMPT_VERSION38 = "1.0.0";
15178
+ var fallbackPatterns38 = [
15179
+ {
15180
+ regex: /(?:localStorage|sessionStorage)\.setItem\s*\(\s*['"`][^'"`]*(?:refresh|refreshToken|refresh_token)[^'"`]*['"`]/i,
15181
+ message: "Refresh token or long-lived credential stored in localStorage/sessionStorage",
15182
+ severity: "high"
15183
+ }
15184
+ ];
15185
+ async function buildPrompt38(sourceFiles) {
15186
+ const parts = [
15187
+ "Check for refresh tokens or long-lived authentication credentials written to browser localStorage or sessionStorage.",
15188
+ 'Flag any code that calls localStorage.setItem() or sessionStorage.setItem() (or equivalent bracket notation assignments) with a key containing "refresh", "refreshToken", or "refresh_token".',
15189
+ "Also flag storing full auth/session tokens intended for reuse across browser sessions in localStorage.",
15190
+ "XSS attacks can read all localStorage values \u2014 refresh tokens should be in httpOnly cookies instead.",
15191
+ "Short-lived access tokens held in JavaScript memory variables (not persisted to storage) are acceptable. Only flag explicit writes to localStorage/sessionStorage."
15192
+ ];
15193
+ for (const file of sourceFiles) {
15194
+ const content = await file.content();
15195
+ parts.push(`--- ${file.relativePath} ---
15196
+ ${content.slice(0, 3e3)}`);
15197
+ }
15198
+ return parts.join("\n\n");
15199
+ }
15200
+ function parseResponse38(response, checkId) {
15201
+ return parseLLMResponse(response, checkId, "high");
15202
+ }
15203
+ var promptTemplate38 = {
15204
+ checkId: "security.refresh-token-in-localstorage",
15205
+ promptVersion: PROMPT_VERSION38,
15206
+ buildPrompt: buildPrompt38,
15207
+ parseResponse: parseResponse38,
15208
+ fallbackPatterns: fallbackPatterns38
15209
+ };
15210
+ registry.register({
15211
+ id: "security.refresh-token-in-localstorage",
15212
+ name: "Refresh token in localStorage",
15213
+ description: "Refresh tokens or long-lived credentials stored in localStorage are vulnerable to XSS theft",
15214
+ category: "security",
15215
+ severity: "high",
15216
+ minTier: "pro",
15217
+ async run(ctx) {
15218
+ return runLLMCheck(ctx, promptTemplate38);
15219
+ }
15220
+ });
15221
+ var PROMPT_VERSION39 = "1.0.0";
15222
+ var fallbackPatterns39 = [
15223
+ {
15224
+ regex: /^\s*USER\s+(?!root\b|0\b)/im,
15225
+ message: "Dockerfile has no non-root USER directive \u2014 container runs as root",
15226
+ severity: "medium",
15227
+ absenceBased: true
15228
+ }
15229
+ ];
15230
+ async function buildPrompt39(sourceFiles) {
15231
+ const parts = [
15232
+ "Check Dockerfiles for containers running as root.",
15233
+ "Flag Dockerfiles that have no USER instruction before the final CMD or ENTRYPOINT, or where the last USER directive sets root (USER root or USER 0).",
15234
+ "A secure Dockerfile creates a non-privileged user and switches to it, e.g.: RUN adduser --disabled-password appuser followed by USER appuser.",
15235
+ "Running containers as root means any process escape gains host root privileges.",
15236
+ "Return [] if a non-root USER is set before CMD/ENTRYPOINT."
15237
+ ];
15238
+ for (const file of sourceFiles) {
15239
+ const content = await file.content();
15240
+ parts.push(`--- ${file.relativePath} ---
15241
+ ${content}`);
15242
+ }
15243
+ return parts.join("\n\n");
15244
+ }
15245
+ function parseResponse39(response, checkId) {
15246
+ return parseLLMResponse(response, checkId, "medium");
15247
+ }
15248
+ var promptTemplate39 = {
15249
+ checkId: "ops.dockerfile-runs-as-root",
15250
+ promptVersion: PROMPT_VERSION39,
15251
+ buildPrompt: buildPrompt39,
15252
+ parseResponse: parseResponse39,
15253
+ fallbackPatterns: fallbackPatterns39
15254
+ };
15255
+ registry.register({
15256
+ id: "ops.dockerfile-runs-as-root",
15257
+ name: "Dockerfile runs as root",
15258
+ description: "Container image has no non-root USER directive",
15259
+ category: "ops",
15260
+ severity: "medium",
15261
+ minTier: "pro",
15262
+ async run(ctx) {
15263
+ return runLLMCheck(ctx, promptTemplate39);
15264
+ }
15265
+ });
15266
+ var PROMPT_VERSION40 = "1.0.0";
15267
+ var fallbackPatterns40 = [
15268
+ {
15269
+ regex: /^\s*(?:ENV|ARG)\s+\S*(?:KEY|TOKEN|SECRET|PASSWORD|PASS|CREDENTIAL|API_KEY|PRIVATE)\s*=\s*\S+/im,
15270
+ message: "Dockerfile bakes a secret-shaped value into the image via ENV or ARG",
15271
+ severity: "high"
15272
+ }
15273
+ ];
15274
+ async function buildPrompt40(sourceFiles) {
15275
+ const parts = [
15276
+ "Check Dockerfiles for secrets or credentials baked into image layers via ENV or ARG instructions.",
15277
+ "Flag ENV or ARG directives where the variable name suggests a secret (*_KEY, *_TOKEN, *_SECRET, *_PASSWORD, *_PASS, *_CREDENTIAL, *_PRIVATE, DATABASE_URL, PRIVATE_KEY) and a literal non-placeholder value is assigned.",
15278
+ 'ENV values are baked into every image layer and visible via "docker inspect". ARG values are also stored in the build history.',
15279
+ "Flag: ENV DATABASE_URL=postgres://user:password@host/db, ENV API_KEY=sk-abc123, ARG STRIPE_SECRET=sk_live_...",
15280
+ "Do NOT flag: ENV PORT=3000, ENV NODE_ENV=production, ARG BUILD_DATE, or ENV DATABASE_URL= (empty assignment)."
15281
+ ];
15282
+ for (const file of sourceFiles) {
15283
+ const content = await file.content();
15284
+ parts.push(`--- ${file.relativePath} ---
15285
+ ${content}`);
15286
+ }
15287
+ return parts.join("\n\n");
15288
+ }
15289
+ function parseResponse40(response, checkId) {
15290
+ return parseLLMResponse(response, checkId, "high");
15291
+ }
15292
+ var promptTemplate40 = {
15293
+ checkId: "ops.dockerfile-secrets-in-env",
15294
+ promptVersion: PROMPT_VERSION40,
15295
+ buildPrompt: buildPrompt40,
15296
+ parseResponse: parseResponse40,
15297
+ fallbackPatterns: fallbackPatterns40
15298
+ };
15299
+ registry.register({
15300
+ id: "ops.dockerfile-secrets-in-env",
15301
+ name: "Secrets in Dockerfile ENV",
15302
+ description: "Dockerfile bakes secrets into image layers via ENV or ARG instructions",
15303
+ category: "ops",
15304
+ severity: "high",
15305
+ minTier: "pro",
15306
+ async run(ctx) {
15307
+ return runLLMCheck(ctx, promptTemplate40);
15308
+ }
15309
+ });
15310
+ var PROMPT_VERSION41 = "1.0.0";
15311
+ var fallbackPatterns41 = [
15312
+ {
15313
+ regex: /^\s*HEALTHCHECK\b/im,
15314
+ message: "Dockerfile has no HEALTHCHECK instruction \u2014 container orchestrators cannot detect unhealthy state",
15315
+ severity: "low",
15316
+ absenceBased: true
15317
+ }
15318
+ ];
15319
+ async function buildPrompt41(sourceFiles) {
15320
+ const parts = [
15321
+ "Check if a Dockerfile includes a HEALTHCHECK instruction.",
15322
+ "Without HEALTHCHECK, Docker and container orchestrators like Kubernetes cannot detect an unhealthy container and will continue routing traffic to it.",
15323
+ "Flag Dockerfiles that have no HEALTHCHECK instruction at all.",
15324
+ "A minimal HEALTHCHECK example: HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8080/health || exit 1",
15325
+ "Return [] if a HEALTHCHECK instruction is present."
15326
+ ];
15327
+ for (const file of sourceFiles) {
15328
+ const content = await file.content();
15329
+ parts.push(`--- ${file.relativePath} ---
15330
+ ${content}`);
15331
+ }
15332
+ return parts.join("\n\n");
15333
+ }
15334
+ function parseResponse41(response, checkId) {
15335
+ return parseLLMResponse(response, checkId, "low");
15336
+ }
15337
+ var promptTemplate41 = {
15338
+ checkId: "ops.dockerfile-no-healthcheck",
15339
+ promptVersion: PROMPT_VERSION41,
15340
+ buildPrompt: buildPrompt41,
15341
+ parseResponse: parseResponse41,
15342
+ fallbackPatterns: fallbackPatterns41
15343
+ };
15344
+ registry.register({
15345
+ id: "ops.dockerfile-no-healthcheck",
15346
+ name: "Dockerfile missing HEALTHCHECK",
15347
+ description: "Dockerfile has no HEALTHCHECK instruction \u2014 orchestrators cannot detect unhealthy containers",
15348
+ category: "ops",
15349
+ severity: "low",
15350
+ minTier: "pro",
15351
+ async run(ctx) {
15352
+ return runLLMCheck(ctx, promptTemplate41);
15353
+ }
15354
+ });
15355
+ registry.register({
15356
+ id: "ops.missing-dockerignore",
15357
+ name: "Missing .dockerignore",
15358
+ description: "Dockerfile present without a .dockerignore \u2014 build context may leak sensitive files",
15359
+ category: "ops",
15360
+ severity: "low",
15361
+ minTier: "pro",
15362
+ async run(ctx) {
15363
+ const dockerfile = ctx.files.find((f) => f.language === "dockerfile");
15364
+ if (!dockerfile) return [];
15365
+ const hasDockerignore = ctx.files.some((f) => /(?:^|[\\/])\.dockerignore$/.test(f.relativePath));
15366
+ if (hasDockerignore) return [];
15367
+ let remediation;
15368
+ try {
15369
+ remediation = getCopy("remediation.ops.missing-dockerignore");
15370
+ } catch {
15371
+ }
15372
+ return [
15373
+ {
15374
+ id: "ops.missing-dockerignore-1",
15375
+ checkId: "ops.missing-dockerignore",
15376
+ category: "ops",
15377
+ severity: "low",
15378
+ message: "Dockerfile exists but no .dockerignore found \u2014 sensitive files (node_modules, .env, secrets) may be included in the build context",
15379
+ file: dockerfile.relativePath,
15380
+ line: 1,
15381
+ remediation
15382
+ }
15383
+ ];
15384
+ }
15385
+ });
15386
+ var PROMPT_VERSION42 = "1.0.0";
15387
+ var fallbackPatterns42 = [];
15388
+ async function buildPrompt42(sourceFiles) {
15389
+ const parts = [
15390
+ "Check for N+1 query patterns where ORM results are iterated and a new database query is issued per row.",
15391
+ "In Prisma (TypeScript): flag findMany() or findFirst() results iterated with for...of or forEach where a separate findUnique/findFirst/findMany is called inside the loop body.",
15392
+ "In Sequelize (Node.js): flag findAll() results where model.find() or association.get() is called per element inside a loop.",
15393
+ "In TypeORM: flag find()/findBy() results with repository calls inside the iteration.",
15394
+ "In SQLAlchemy (Python): flag query.all() or session.scalars() results where session.get() or a new query is executed per row.",
15395
+ "In Active Record (Ruby): flag .all or .where results iterated with .each where an association is accessed without eager loading (use .includes instead).",
15396
+ "Flag only clear, direct N+1 patterns \u2014 a loop body that calls a query on each iteration. Do not flag loops that process already-loaded data."
15397
+ ];
15398
+ for (const file of sourceFiles) {
15399
+ const content = await file.content();
15400
+ parts.push(`--- ${file.relativePath} ---
15401
+ ${content.slice(0, 3e3)}`);
15402
+ }
15403
+ return parts.join("\n\n");
15404
+ }
15405
+ function parseResponse42(response, checkId) {
15406
+ return parseLLMResponse(response, checkId, "medium");
15407
+ }
15408
+ var promptTemplate42 = {
15409
+ checkId: "resilience.orm-n-plus-one",
15410
+ promptVersion: PROMPT_VERSION42,
15411
+ buildPrompt: buildPrompt42,
15412
+ parseResponse: parseResponse42,
15413
+ fallbackPatterns: fallbackPatterns42
15414
+ };
15415
+ registry.register({
15416
+ id: "resilience.orm-n-plus-one",
15417
+ name: "ORM N+1 query pattern",
15418
+ description: "ORM results are iterated with a per-row database query instead of a batch load",
15419
+ category: "resilience",
15420
+ severity: "medium",
15421
+ minTier: "pro",
15422
+ async run(ctx) {
15423
+ return runLLMCheck(ctx, promptTemplate42);
15424
+ }
15425
+ });
14733
15426
  var LANGUAGE_CODES = [
14734
15427
  "javascript",
14735
15428
  "typescript",
@@ -15169,6 +15862,7 @@ var copy3 = {
15169
15862
  "cli.sarif.stdout_conflict": "Flags --sarif-stdout and --json cannot be used together. Use --output to send JSON to a file.",
15170
15863
  "cli.error.llmRequired": "LLM API key required for Pro checks. Set it with seaworthy config or the SEAWORTHY_LLM_API_KEY environment variable.",
15171
15864
  "cli.warn.llmSkipped": "Pro checks skipped: no LLM API key configured. Set it with seaworthy config or the SEAWORTHY_LLM_API_KEY environment variable to enable semantic analysis.",
15865
+ "cli.warn.licenseInvalid": "Your license key is invalid or has been revoked. Running in free tier.\nTo re-enter your key, run: npx seaworthycode setup",
15172
15866
  "cli.error.config.missingKey": "Missing config key. Usage: seaworthy config <set|get> <key>",
15173
15867
  "cli.error.config.missingValue": "Missing config value. Usage: seaworthy config set <key> <value>",
15174
15868
  "cli.error.config.unknownKey": "Unknown config key. Valid keys: licenseKey, llmProvider, llmApiKey, llmModel, llmApiUrl",
@@ -15515,6 +16209,8 @@ async function checkUpdate() {
15515
16209
  }
15516
16210
 
15517
16211
  // src/cli-action.ts
16212
+ var _require = createRequire2(import.meta.url);
16213
+ var { version: CLI_VERSION } = _require("../package.json");
15518
16214
  function getConfigFile2() {
15519
16215
  return join15(process.env.SEAWORTHY_CONFIG_DIR ?? join15(homedir3(), ".seaworthy"), "config");
15520
16216
  }
@@ -15591,7 +16287,7 @@ async function runCli(options) {
15591
16287
  await setupCommand.handler({ action: "run" }, { tier: "free", licensedTo: "free tier" });
15592
16288
  }
15593
16289
  const provider = new FsCredentialProvider();
15594
- const license = new LicenseClient({ baseUrl: LICENSE_SERVER_URL });
16290
+ const license = new LicenseClient({ baseUrl: LICENSE_SERVER_URL, userAgent: `seaworthycode/${CLI_VERSION}` });
15595
16291
  const authResult = await resolveAuth(provider, license);
15596
16292
  if (!authResult.ok) {
15597
16293
  const normalized = authResult.code.replace(/^errors\./, "");
@@ -15600,6 +16296,7 @@ async function runCli(options) {
15600
16296
  const message = resolveErrorMessage(authResult.code);
15601
16297
  return { code: resolveExitCode(authResult.code), error: message };
15602
16298
  }
16299
+ console.error(getCopy2("cli.warn.licenseInvalid"));
15603
16300
  }
15604
16301
  const auth = authResult.ok ? authResult.value : { tier: "free", token: "", licensedTo: "free tier" };
15605
16302
  const allowedTiers = {
@@ -15714,7 +16411,7 @@ var repoRootEnv = resolve4(dirname7(fileURLToPath3(import.meta.url)), "../../.."
15714
16411
  if (existsSync6(repoRootEnv)) {
15715
16412
  loadEnv({ path: repoRootEnv, override: false });
15716
16413
  }
15717
- var require5 = createRequire2(import.meta.url);
16414
+ var require5 = createRequire3(import.meta.url);
15718
16415
  var { version } = require5("../package.json");
15719
16416
  var cli = cac("seaworthy");
15720
16417
  cli.version(version);
@@ -15764,6 +16461,10 @@ cli.help(() => {
15764
16461
  $ npx seaworthycode ./my-project --sarif report.sarif
15765
16462
  $ npx seaworthycode --provider kimi ./my-project`
15766
16463
  },
16464
+ {
16465
+ title: "Commands",
16466
+ body: ` npx seaworthycode setup Interactive setup for license key and LLM provider`
16467
+ },
15767
16468
  {
15768
16469
  title: "Options",
15769
16470
  body: ` --json Output JSON to stdout
@@ -15779,10 +16480,6 @@ cli.help(() => {
15779
16480
  --show-ids Show check IDs alongside names
15780
16481
  --debug Enable debug logging`
15781
16482
  },
15782
- {
15783
- title: "Commands",
15784
- body: ` seaworthy setup Interactive first-run setup`
15785
- },
15786
16483
  {
15787
16484
  title: "Configuration",
15788
16485
  body: ` ~/.seaworthy/config # license key, LLM provider, API key, model, url`