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.
- package/dist/index.js +739 -42
- package/dist/index.js.map +1 -1
- package/package.json +14 -12
- package/rules/external/configuration/insecure-cookie-flags.test.yaml +114 -0
- package/rules/external/configuration/insecure-cookie-flags.yaml +77 -0
- package/rules/external/configuration/missing-ssl-verification.test.yaml +130 -0
- package/rules/external/configuration/missing-ssl-verification.yaml +123 -0
- package/rules/external/data-exposure/debug-artifacts.test.yaml +143 -0
- package/rules/external/data-exposure/debug-artifacts.yaml +57 -0
- package/rules/external/data-exposure/hardcoded-ip.test.yaml +131 -0
- package/rules/external/data-exposure/hardcoded-ip.yaml +67 -0
- package/rules/external/data-exposure/localstorage-sensitive-data.test.yaml +58 -0
- package/rules/external/data-exposure/localstorage-sensitive-data.yaml +60 -0
- package/rules/external/dependencies/deprecated-functions.test.yaml +92 -0
- package/rules/external/dependencies/deprecated-functions.yaml +66 -0
- package/rules/external/ops/hardcoded-port.test.yaml +141 -0
- package/rules/external/ops/hardcoded-port.yaml +91 -0
- package/rules/external/ops/insecure-file-permissions.test.yaml +111 -0
- package/rules/external/ops/insecure-file-permissions.yaml +91 -0
- package/rules/external/resilience/missing-memory-limit.test.yaml +54 -0
- package/rules/external/resilience/missing-memory-limit.yaml +59 -0
- package/rules/external/resilience/missing-timeout-config.test.yaml +179 -0
- package/rules/external/resilience/missing-timeout-config.yaml +124 -0
- package/rules/external/security/broken-crypto-algorithm.test.yaml +104 -0
- package/rules/external/security/broken-crypto-algorithm.yaml +79 -0
- package/rules/external/security/http-method-override.test.yaml +40 -0
- package/rules/external/security/http-method-override.yaml +35 -0
- package/rules/external/security/http-no-tls.test.yaml +115 -0
- package/rules/external/security/http-no-tls.yaml +59 -0
- package/rules/external/security/path-traversal.test.yaml +188 -0
- package/rules/external/security/path-traversal.yaml +210 -0
- package/rules/external/security/unsafe-innerhtml.test.yaml +72 -0
- package/rules/external/security/unsafe-innerhtml.yaml +46 -0
- package/rules/external/security/untrusted-deserialization.test.yaml +79 -0
- package/rules/external/security/untrusted-deserialization.yaml +48 -0
- package/rules/external/security/weak-encryption-mode.test.yaml +126 -0
- package/rules/external/security/weak-encryption-mode.yaml +104 -0
- package/rules/external/security/weak-hash-algorithm.test.yaml +121 -0
- package/rules/external/security/weak-hash-algorithm.yaml +87 -0
- package/rules/external/security/xxe-parsing.test.yaml +76 -0
- package/rules/external/security/xxe-parsing.yaml +63 -0
- package/rules/internal/security/dangerous-eval.test.yaml +126 -0
- package/rules/internal/security/dangerous-eval.yaml +134 -0
- package/rules/internal/security/no-document-write.test.yaml +33 -0
- package/rules/internal/security/no-document-write.yaml +40 -0
- package/rules/internal/taint/MATRIX.md +16 -0
- package/rules/internal/taint/bash.test.yaml +84 -0
- package/rules/internal/taint/bash.yaml +166 -0
- package/rules/internal/taint/dvwa.test.yaml +217 -0
- package/rules/internal/taint/go.test.yaml +207 -0
- package/rules/internal/taint/go.yaml +325 -0
- package/rules/internal/taint/java.test.yaml +158 -0
- package/rules/internal/taint/java.yaml +318 -0
- package/rules/internal/taint/javascript.test.yaml +374 -0
- package/rules/internal/taint/javascript.yaml +439 -0
- package/rules/internal/taint/php.test.yaml +134 -0
- package/rules/internal/taint/php.yaml +331 -0
- package/rules/internal/taint/python.test.yaml +427 -0
- package/rules/internal/taint/python.yaml +636 -0
- package/rules/internal/taint/ruby.test.yaml +148 -0
- package/rules/internal/taint/ruby.yaml +233 -0
- package/rules/internal/taint/rust.test.yaml +585 -0
- package/rules/internal/taint/rust.yaml +483 -0
- package/rules/internal/taint/tsx.test.yaml +140 -0
- package/rules/internal/taint/tsx.yaml +51 -0
- package/rules/internal/taint/typescript.test.yaml +393 -0
- package/rules/internal/taint/typescript.yaml +660 -0
- 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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
/(?:<!--|\/\*)[^]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = /\/\^[
|
|
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 =
|
|
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 =
|
|
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`
|