seaworthycode 1.1.4 → 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.
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";
@@ -1440,6 +1443,52 @@ var copy = {
1440
1443
  "checks.sql-missing-rls.message": (match) => `RLS is not enabled or lacks restriction on a private/tenant table: ${match}`,
1441
1444
  "checks.sql-missing-rls.degraded": (path, language) => `${language} analysis was incomplete for ${path} (no grammar available)`,
1442
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).",
1443
1492
  // Reporting labels
1444
1493
  "reporting.analysisType.pattern": "pattern",
1445
1494
  "reporting.analysisType.semantic": "semantic"
@@ -3134,16 +3183,60 @@ var REFRESH_WINDOW_MS = 5 * 60 * 1e3;
3134
3183
  var LicenseClient = class {
3135
3184
  baseUrl;
3136
3185
  timeout;
3186
+ userAgent;
3137
3187
  cache = null;
3138
3188
  jwksFetched = false;
3139
3189
  constructor(config = {}) {
3140
3190
  this.baseUrl = config.baseUrl ?? LICENSE_SERVER_URL;
3141
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
+ });
3142
3234
  }
3143
3235
  async ensureJwks() {
3144
3236
  if (this.jwksFetched) return;
3145
- const response = await fetch(`${this.baseUrl}/.well-known/jwks.json`, {
3146
- 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 }
3147
3240
  });
3148
3241
  if (!response.ok) {
3149
3242
  throw new Error("jwks.fetch_failed");
@@ -3167,9 +3260,9 @@ var LicenseClient = class {
3167
3260
  }
3168
3261
  try {
3169
3262
  await this.ensureJwks();
3170
- const response = await fetch(`${this.baseUrl}/validate`, {
3263
+ const response = await this.fetchWithRetry(`${this.baseUrl}/validate`, {
3171
3264
  method: "POST",
3172
- headers: { "Content-Type": "application/json" },
3265
+ headers: { "Content-Type": "application/json", "User-Agent": this.userAgent },
3173
3266
  body: JSON.stringify({ licenseKey: licenseKey ?? "free" }),
3174
3267
  signal: AbortSignal.timeout(this.timeout)
3175
3268
  });
@@ -3180,7 +3273,8 @@ var LicenseClient = class {
3180
3273
  return fail("license.rate_limited");
3181
3274
  }
3182
3275
  if (!response.ok) {
3183
- 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}` : ""}`);
3184
3278
  }
3185
3279
  const data = await response.json();
3186
3280
  const parsed = licenseResponseSchema.safeParse(data);
@@ -3209,12 +3303,12 @@ var LicenseClient = class {
3209
3303
  });
3210
3304
  } catch (err) {
3211
3305
  if (err instanceof DOMException && err.name === "TimeoutError") {
3212
- return fail("license.server_unreachable");
3306
+ return fail("license.server_unreachable", "timeout");
3213
3307
  }
3214
3308
  if (err instanceof TypeError) {
3215
- return fail("license.server_unreachable");
3309
+ return fail("license.server_unreachable", err.message);
3216
3310
  }
3217
- return fail("license.server_unreachable");
3311
+ return fail("license.server_unreachable", String(err));
3218
3312
  }
3219
3313
  }
3220
3314
  shouldRefresh() {
@@ -3756,6 +3850,22 @@ function mergeFindings(tsFindings, otherFindings) {
3756
3850
  }
3757
3851
  return Array.from(map.values());
3758
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
+ }
3759
3869
  var CHECK_ID = "security.hardcoded-secrets";
3760
3870
  var SECRET_PATTERNS = [
3761
3871
  { regex: /AWS_SECRET_ACCESS_KEY\s*=\s*['"]([A-Za-z0-9/+=]{40})['"]/gi, name: "AWS secret access key", minLength: 40, method: "known-format" },
@@ -3791,6 +3901,8 @@ var regexCheck = {
3791
3901
  const matches = Array.from(line.matchAll(pattern.regex));
3792
3902
  for (let matchIdx = 0; matchIdx < matches.length; matchIdx++) {
3793
3903
  const match = matches[matchIdx];
3904
+ const captured = match[1] ?? match[0];
3905
+ if (isEnvSubstitution(captured)) continue;
3794
3906
  findings.push({
3795
3907
  id: `${CHECK_ID}:${file.relativePath}:${lineNum + 1}:${(match.index ?? 0) + 1}:${matchIdx}`,
3796
3908
  checkId: CHECK_ID,
@@ -3947,6 +4059,7 @@ var tsCheck = createTreeSitterCheck({
3947
4059
  const trimmedValue = value.trim();
3948
4060
  if (!/^['"`].*['"`]$/.test(trimmedValue)) return false;
3949
4061
  const cleanValue = trimmedValue.replace(/^['"`]|['"`]$/g, "");
4062
+ if (isEnvSubstitution(cleanValue)) return false;
3950
4063
  for (const rule of NAME_RULES) {
3951
4064
  if (rule.regex.test(name) && cleanValue.length >= rule.minLength) {
3952
4065
  return true;
@@ -5205,7 +5318,7 @@ var MIN_VALUE_LENGTH = 8;
5205
5318
  var SECRET_ASSIGNMENT_PATTERNS = [
5206
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,
5207
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,
5208
- /(?:<!--|\/\*)[^]*?(\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
5209
5322
  ];
5210
5323
  var REMEDIATION9 = getCopy("remediation.configuration.secrets-in-source");
5211
5324
  function stripQuotes(value) {
@@ -5235,7 +5348,9 @@ var regexCheck3 = {
5235
5348
  const name = match[1] ?? "";
5236
5349
  const value = match[2] ?? "";
5237
5350
  if (!isCompoundSecretName(name)) continue;
5238
- 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;
5239
5354
  const before = content.slice(0, match.index ?? 0);
5240
5355
  const line = before.split("\n").length;
5241
5356
  const col = (before.split("\n").at(-1) ?? "").length + 1;
@@ -5264,7 +5379,9 @@ var regexCheck3 = {
5264
5379
  const name = match[1] ?? "";
5265
5380
  const value = match[2] ?? "";
5266
5381
  if (!isCompoundSecretName(name)) continue;
5267
- 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;
5268
5385
  findings.push({
5269
5386
  id: `${CHECK_ID5}:${file.relativePath}:${lineNum + 1}:${(match.index ?? 0) + 1}:${matchIdx}`,
5270
5387
  checkId: CHECK_ID5,
@@ -5400,7 +5517,9 @@ var tsCheck3 = createTreeSitterCheck({
5400
5517
  const value = captures.value;
5401
5518
  if (!name || !value) return false;
5402
5519
  if (!isCompoundSecretName(name)) return false;
5403
- 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);
5404
5523
  },
5405
5524
  confidenceFactory: (captures) => mechanicalConfidence("ast", void 0, captures.value ?? "")
5406
5525
  });
@@ -5630,10 +5749,25 @@ var ENTRYPOINT_PATTERNS = [
5630
5749
  /(?:^|[\\/])bin[\\/]rails$/i,
5631
5750
  /(?:^|[\\/])application_controller\.rb$/i
5632
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
+ ]);
5633
5761
  function isProCheckEligible(file, template) {
5634
5762
  if (template?.checkId === "security.sql-missing-rls") {
5635
5763
  return file.language === "sql" && !isDocumentationPath(file.relativePath) && !isNonProductionPath(file.relativePath);
5636
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
+ }
5637
5771
  return PRO_CHECK_SOURCE_LANGUAGES.has(file.language) && !isDocumentationPath(file.relativePath) && !isNonProductionPath(file.relativePath);
5638
5772
  }
5639
5773
  function pickProjectAnchorFile(files) {
@@ -7521,6 +7655,10 @@ registry.register({
7521
7655
  }
7522
7656
  });
7523
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
+ }
7524
7662
  var QUERIES = {
7525
7663
  javascript: `
7526
7664
  (call_expression
@@ -7804,7 +7942,10 @@ registry.register(
7804
7942
  queries: QUERIES,
7805
7943
  messageKey: "checks.xss-surface.message",
7806
7944
  degradedMessageKey: "checks.xss-surface.degraded",
7807
- 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)
7808
7949
  })
7809
7950
  );
7810
7951
  var CHECK_ID11 = "security.os-command-injection";
@@ -14732,6 +14873,556 @@ registry.register({
14732
14873
  }
14733
14874
  }
14734
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
+ });
14735
15426
  var LANGUAGE_CODES = [
14736
15427
  "javascript",
14737
15428
  "typescript",
@@ -15518,6 +16209,8 @@ async function checkUpdate() {
15518
16209
  }
15519
16210
 
15520
16211
  // src/cli-action.ts
16212
+ var _require = createRequire2(import.meta.url);
16213
+ var { version: CLI_VERSION } = _require("../package.json");
15521
16214
  function getConfigFile2() {
15522
16215
  return join15(process.env.SEAWORTHY_CONFIG_DIR ?? join15(homedir3(), ".seaworthy"), "config");
15523
16216
  }
@@ -15594,7 +16287,7 @@ async function runCli(options) {
15594
16287
  await setupCommand.handler({ action: "run" }, { tier: "free", licensedTo: "free tier" });
15595
16288
  }
15596
16289
  const provider = new FsCredentialProvider();
15597
- const license = new LicenseClient({ baseUrl: LICENSE_SERVER_URL });
16290
+ const license = new LicenseClient({ baseUrl: LICENSE_SERVER_URL, userAgent: `seaworthycode/${CLI_VERSION}` });
15598
16291
  const authResult = await resolveAuth(provider, license);
15599
16292
  if (!authResult.ok) {
15600
16293
  const normalized = authResult.code.replace(/^errors\./, "");
@@ -15718,7 +16411,7 @@ var repoRootEnv = resolve4(dirname7(fileURLToPath3(import.meta.url)), "../../.."
15718
16411
  if (existsSync6(repoRootEnv)) {
15719
16412
  loadEnv({ path: repoRootEnv, override: false });
15720
16413
  }
15721
- var require5 = createRequire2(import.meta.url);
16414
+ var require5 = createRequire3(import.meta.url);
15722
16415
  var { version } = require5("../package.json");
15723
16416
  var cli = cac("seaworthy");
15724
16417
  cli.version(version);