seaworthycode 1.1.4 → 1.2.4
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 +856 -39
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
- 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 createRequire4 } from "module";
|
|
10
10
|
|
|
11
11
|
// src/cli-action.ts
|
|
12
|
-
import { stat, access as access2 } from "fs/promises";
|
|
12
|
+
import { stat, access as access2, appendFile, mkdir } 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";
|
|
@@ -90,6 +93,7 @@ var CACHE_DIR = join(CONFIG_DIR, "cache");
|
|
|
90
93
|
var AST_CACHE_DIR = join(CACHE_DIR, "ast");
|
|
91
94
|
var DEP_CACHE_DIR = join(CACHE_DIR, "deps");
|
|
92
95
|
var LLM_CACHE_DIR = join(CACHE_DIR, "llm");
|
|
96
|
+
var LOG_DIR = join(CONFIG_DIR, "logs");
|
|
93
97
|
var VALID_CATEGORIES = /* @__PURE__ */ new Set([
|
|
94
98
|
"security",
|
|
95
99
|
"resilience",
|
|
@@ -397,11 +401,11 @@ async function crawl(options) {
|
|
|
397
401
|
const ext2 = entry.name.includes(".") ? entry.name.slice(entry.name.lastIndexOf(".")) : "";
|
|
398
402
|
if (SKIP_EXTENSIONS.has(ext2)) continue;
|
|
399
403
|
try {
|
|
400
|
-
const
|
|
401
|
-
if (!
|
|
402
|
-
if (
|
|
403
|
-
const { isBinary
|
|
404
|
-
if (
|
|
404
|
+
const stat2 = await fs.stat(fullPath);
|
|
405
|
+
if (!stat2.isFile()) continue;
|
|
406
|
+
if (stat2.size > maxFileSize) continue;
|
|
407
|
+
const { isBinary, isGenerated } = await peekFileHead(fullPath);
|
|
408
|
+
if (isBinary || isGenerated) continue;
|
|
405
409
|
if (await shouldSkipMinifiedJs(fullPath, entry.name)) continue;
|
|
406
410
|
files.push({
|
|
407
411
|
path: fullPath,
|
|
@@ -417,17 +421,20 @@ async function crawl(options) {
|
|
|
417
421
|
if (ig.ignores(relPath)) continue;
|
|
418
422
|
const ext = entry.name.includes(".") ? entry.name.slice(entry.name.lastIndexOf(".")) : "";
|
|
419
423
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
424
|
+
try {
|
|
425
|
+
const stat2 = await fs.stat(fullPath);
|
|
426
|
+
if (stat2.size > maxFileSize) continue;
|
|
427
|
+
const { isBinary, isGenerated } = await peekFileHead(fullPath);
|
|
428
|
+
if (isBinary || isGenerated) continue;
|
|
429
|
+
if (await shouldSkipMinifiedJs(fullPath, entry.name)) continue;
|
|
430
|
+
files.push({
|
|
431
|
+
path: fullPath,
|
|
432
|
+
relativePath: relPath,
|
|
433
|
+
content: () => fs.readFile(fullPath, "utf-8"),
|
|
434
|
+
language: inferLanguage(entry.name)
|
|
435
|
+
});
|
|
436
|
+
} catch {
|
|
437
|
+
}
|
|
431
438
|
}
|
|
432
439
|
}
|
|
433
440
|
await walk(targetDir);
|
|
@@ -776,10 +783,12 @@ var copy = {
|
|
|
776
783
|
"report.tierWatermark": (licensedTo) => `\u2693 Pro \xB7 ${licensedTo}`,
|
|
777
784
|
"report.scanProgress.starting": (targetDir) => `\u2693 Scanning ${targetDir}...`,
|
|
778
785
|
"report.scanProgress.category": (label, count) => count > 0 ? ` \u2693 ${label} \u2014 ${count} issue(s)` : ` \u2693 ${label} \u2713`,
|
|
786
|
+
"report.licenseNotice": (licensedTo) => `Licensed to ${licensedTo}`,
|
|
779
787
|
"report.cta.clean": `\u2713 All clear \u2014 your project is shipshape!`,
|
|
780
788
|
"report.cta.hasFindings": `Fix these before you ship. ${SITE_URL}/docs`,
|
|
781
789
|
"report.cta.critical": `\u26A0 Critical issues found \u2014 patch before you deploy. ${SITE_URL}/docs`,
|
|
782
790
|
"report.proUpsellNamed": (names, count) => `\u2693 Pro unlocks ${count} more check${count === 1 ? "" : "s"} (${names}). ${SITE_URL}`,
|
|
791
|
+
"report.preScanNotice": (count) => `\u2693 Pro unlocks ${count} additional checks. Upgrade at ${SITE_URL}`,
|
|
783
792
|
"report.llmPrivacyWarning": "Pro checks will send code context to your LLM provider via your API key. No data is sent to Seaworthy servers.",
|
|
784
793
|
"report.confidenceHigh": "high confidence",
|
|
785
794
|
"report.confidenceMedium": "medium confidence",
|
|
@@ -1440,6 +1449,52 @@ var copy = {
|
|
|
1440
1449
|
"checks.sql-missing-rls.message": (match) => `RLS is not enabled or lacks restriction on a private/tenant table: ${match}`,
|
|
1441
1450
|
"checks.sql-missing-rls.degraded": (path, language) => `${language} analysis was incomplete for ${path} (no grammar available)`,
|
|
1442
1451
|
"remediation.security.sql-missing-rls": "Define a policy with CREATE POLICY ... ON ... USING (...) to restrict data access to authorized users.",
|
|
1452
|
+
// Security headers
|
|
1453
|
+
"checks.missing-security-headers.name": "Missing HTTP security headers",
|
|
1454
|
+
"checks.missing-security-headers.description": "Application does not set HTTP security response headers (HSTS, X-Content-Type-Options, X-Frame-Options, Permissions-Policy)",
|
|
1455
|
+
"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.",
|
|
1456
|
+
// GitHub Actions supply-chain
|
|
1457
|
+
"checks.gh-actions-unpinned.name": "Unpinned GitHub Actions",
|
|
1458
|
+
"checks.gh-actions-unpinned.description": "GitHub Actions workflow steps reference third-party actions by mutable tag rather than a full commit SHA",
|
|
1459
|
+
"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.",
|
|
1460
|
+
"checks.gh-actions-excessive-permissions.name": "Excessive GitHub Actions permissions",
|
|
1461
|
+
"checks.gh-actions-excessive-permissions.description": "GitHub Actions workflow grants overly broad or undeclared default permissions",
|
|
1462
|
+
"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 }".',
|
|
1463
|
+
// GraphQL
|
|
1464
|
+
"checks.graphql-introspection-prod.name": "GraphQL introspection in production",
|
|
1465
|
+
"checks.graphql-introspection-prod.description": "GraphQL server exposes introspection without disabling it for production environments",
|
|
1466
|
+
"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.',
|
|
1467
|
+
"checks.graphql-no-depth-limit.name": "GraphQL missing depth limit",
|
|
1468
|
+
"checks.graphql-no-depth-limit.description": "GraphQL server has no query depth or complexity limiting, enabling denial-of-service via deeply nested queries",
|
|
1469
|
+
"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.",
|
|
1470
|
+
// JWT
|
|
1471
|
+
"checks.jwt-alg-none.name": 'JWT allows algorithm "none"',
|
|
1472
|
+
"checks.jwt-alg-none.description": 'JWT verification does not restrict algorithms, allowing forged unsigned tokens via the "none" algorithm',
|
|
1473
|
+
"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.',
|
|
1474
|
+
"checks.jwt-no-expiry.name": "JWT issued without expiry",
|
|
1475
|
+
"checks.jwt-no-expiry.description": "JWT tokens are signed without an expiry claim (exp / expiresIn), making them valid indefinitely",
|
|
1476
|
+
"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.',
|
|
1477
|
+
// Browser token storage
|
|
1478
|
+
"checks.refresh-token-in-localstorage.name": "Refresh token in localStorage",
|
|
1479
|
+
"checks.refresh-token-in-localstorage.description": "Refresh tokens or long-lived credentials stored in localStorage are vulnerable to theft via XSS",
|
|
1480
|
+
"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.",
|
|
1481
|
+
// Dockerfile hardening
|
|
1482
|
+
"checks.dockerfile-runs-as-root.name": "Dockerfile runs as root",
|
|
1483
|
+
"checks.dockerfile-runs-as-root.description": "Container image has no non-root USER directive \u2014 all processes run as root",
|
|
1484
|
+
"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.",
|
|
1485
|
+
"checks.dockerfile-secrets-in-env.name": "Secrets in Dockerfile ENV",
|
|
1486
|
+
"checks.dockerfile-secrets-in-env.description": "Dockerfile bakes secrets or credentials into image layers via ENV or ARG instructions",
|
|
1487
|
+
"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.",
|
|
1488
|
+
"checks.dockerfile-no-healthcheck.name": "Dockerfile missing HEALTHCHECK",
|
|
1489
|
+
"checks.dockerfile-no-healthcheck.description": "Dockerfile has no HEALTHCHECK instruction \u2014 container orchestrators cannot detect unhealthy state",
|
|
1490
|
+
"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",
|
|
1491
|
+
"checks.missing-dockerignore.name": "Missing .dockerignore",
|
|
1492
|
+
"checks.missing-dockerignore.description": "Dockerfile is present without a .dockerignore \u2014 build context may include node_modules, .env files, or other sensitive data",
|
|
1493
|
+
"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.",
|
|
1494
|
+
// ORM N+1
|
|
1495
|
+
"checks.orm-n-plus-one.name": "ORM N+1 query pattern",
|
|
1496
|
+
"checks.orm-n-plus-one.description": "ORM results are iterated with a per-row database query instead of a single batch load",
|
|
1497
|
+
"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
1498
|
// Reporting labels
|
|
1444
1499
|
"reporting.analysisType.pattern": "pattern",
|
|
1445
1500
|
"reporting.analysisType.semantic": "semantic"
|
|
@@ -3134,16 +3189,60 @@ var REFRESH_WINDOW_MS = 5 * 60 * 1e3;
|
|
|
3134
3189
|
var LicenseClient = class {
|
|
3135
3190
|
baseUrl;
|
|
3136
3191
|
timeout;
|
|
3192
|
+
userAgent;
|
|
3137
3193
|
cache = null;
|
|
3138
3194
|
jwksFetched = false;
|
|
3139
3195
|
constructor(config = {}) {
|
|
3140
3196
|
this.baseUrl = config.baseUrl ?? LICENSE_SERVER_URL;
|
|
3141
3197
|
this.timeout = config.timeout ?? 1e4;
|
|
3198
|
+
this.userAgent = config.userAgent ?? "seaworthycode/unknown";
|
|
3199
|
+
}
|
|
3200
|
+
async fetchWithRetry(url, init) {
|
|
3201
|
+
try {
|
|
3202
|
+
return await fetch(url, init);
|
|
3203
|
+
} catch (err) {
|
|
3204
|
+
if (!(err instanceof TypeError)) throw err;
|
|
3205
|
+
return this.fetchIpv4(url, init);
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
async fetchIpv4(url, init) {
|
|
3209
|
+
const parsed = new URL(url);
|
|
3210
|
+
const { address } = await lookup(parsed.hostname, { family: 4 });
|
|
3211
|
+
const initHeaders = init.headers ?? {};
|
|
3212
|
+
const headers = { ...initHeaders, Host: parsed.hostname };
|
|
3213
|
+
return new Promise((resolve22, reject) => {
|
|
3214
|
+
const options = {
|
|
3215
|
+
hostname: address,
|
|
3216
|
+
port: parseInt(parsed.port || "443"),
|
|
3217
|
+
path: parsed.pathname + (parsed.search || ""),
|
|
3218
|
+
method: init.method ?? "GET",
|
|
3219
|
+
headers,
|
|
3220
|
+
servername: parsed.hostname,
|
|
3221
|
+
signal: init.signal ?? void 0
|
|
3222
|
+
};
|
|
3223
|
+
const req = httpsRequest(options, (res) => {
|
|
3224
|
+
const chunks = [];
|
|
3225
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
3226
|
+
res.on("end", () => {
|
|
3227
|
+
const resHeaders = new Headers();
|
|
3228
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
3229
|
+
if (typeof v === "string") resHeaders.set(k, v);
|
|
3230
|
+
else if (Array.isArray(v)) resHeaders.set(k, v.join(", "));
|
|
3231
|
+
}
|
|
3232
|
+
resolve22(new Response(Buffer.concat(chunks), { status: res.statusCode ?? 0, headers: resHeaders }));
|
|
3233
|
+
});
|
|
3234
|
+
res.on("error", reject);
|
|
3235
|
+
});
|
|
3236
|
+
req.on("error", reject);
|
|
3237
|
+
if (init.body) req.write(init.body);
|
|
3238
|
+
req.end();
|
|
3239
|
+
});
|
|
3142
3240
|
}
|
|
3143
3241
|
async ensureJwks() {
|
|
3144
3242
|
if (this.jwksFetched) return;
|
|
3145
|
-
const response = await
|
|
3146
|
-
signal: AbortSignal.timeout(this.timeout)
|
|
3243
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/.well-known/jwks.json`, {
|
|
3244
|
+
signal: AbortSignal.timeout(this.timeout),
|
|
3245
|
+
headers: { "User-Agent": this.userAgent }
|
|
3147
3246
|
});
|
|
3148
3247
|
if (!response.ok) {
|
|
3149
3248
|
throw new Error("jwks.fetch_failed");
|
|
@@ -3167,9 +3266,9 @@ var LicenseClient = class {
|
|
|
3167
3266
|
}
|
|
3168
3267
|
try {
|
|
3169
3268
|
await this.ensureJwks();
|
|
3170
|
-
const response = await
|
|
3269
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/validate`, {
|
|
3171
3270
|
method: "POST",
|
|
3172
|
-
headers: { "Content-Type": "application/json" },
|
|
3271
|
+
headers: { "Content-Type": "application/json", "User-Agent": this.userAgent },
|
|
3173
3272
|
body: JSON.stringify({ licenseKey: licenseKey ?? "free" }),
|
|
3174
3273
|
signal: AbortSignal.timeout(this.timeout)
|
|
3175
3274
|
});
|
|
@@ -3180,7 +3279,8 @@ var LicenseClient = class {
|
|
|
3180
3279
|
return fail("license.rate_limited");
|
|
3181
3280
|
}
|
|
3182
3281
|
if (!response.ok) {
|
|
3183
|
-
|
|
3282
|
+
const rayId = response.headers.get("cf-ray") ?? "";
|
|
3283
|
+
return fail("license.server_error", `HTTP ${response.status}${rayId ? ` ray=${rayId}` : ""}`);
|
|
3184
3284
|
}
|
|
3185
3285
|
const data = await response.json();
|
|
3186
3286
|
const parsed = licenseResponseSchema.safeParse(data);
|
|
@@ -3209,12 +3309,12 @@ var LicenseClient = class {
|
|
|
3209
3309
|
});
|
|
3210
3310
|
} catch (err) {
|
|
3211
3311
|
if (err instanceof DOMException && err.name === "TimeoutError") {
|
|
3212
|
-
return fail("license.server_unreachable");
|
|
3312
|
+
return fail("license.server_unreachable", "timeout");
|
|
3213
3313
|
}
|
|
3214
3314
|
if (err instanceof TypeError) {
|
|
3215
|
-
return fail("license.server_unreachable");
|
|
3315
|
+
return fail("license.server_unreachable", err.message);
|
|
3216
3316
|
}
|
|
3217
|
-
return fail("license.server_unreachable");
|
|
3317
|
+
return fail("license.server_unreachable", String(err));
|
|
3218
3318
|
}
|
|
3219
3319
|
}
|
|
3220
3320
|
shouldRefresh() {
|
|
@@ -3756,6 +3856,22 @@ function mergeFindings(tsFindings, otherFindings) {
|
|
|
3756
3856
|
}
|
|
3757
3857
|
return Array.from(map.values());
|
|
3758
3858
|
}
|
|
3859
|
+
var ENV_SUBSTITUTION_PATTERNS = [
|
|
3860
|
+
/^env\(\s*[A-Za-z_][A-Za-z0-9_]*\s*\)$/,
|
|
3861
|
+
/^\$\{[A-Za-z_][A-Za-z0-9_]*(?::[-=+?][^}]*)?\}$/,
|
|
3862
|
+
/^\$[A-Za-z_][A-Za-z0-9_]*$/,
|
|
3863
|
+
/^%[A-Za-z_][A-Za-z0-9_]*%$/,
|
|
3864
|
+
/^<%=?\s*ENV\[['"][A-Za-z_][A-Za-z0-9_]*['"]\]\s*%>$/,
|
|
3865
|
+
/^process\.env(?:\.[A-Za-z_][A-Za-z0-9_]*|\[['"][A-Za-z_][A-Za-z0-9_]*['"]\])$/,
|
|
3866
|
+
/^os\.(?:environ\[['"][A-Za-z_][A-Za-z0-9_]*['"]\]|getenv\(['"][A-Za-z_][A-Za-z0-9_]*['"]\))$/
|
|
3867
|
+
];
|
|
3868
|
+
function isEnvSubstitution(value) {
|
|
3869
|
+
const trimmed = value.trim();
|
|
3870
|
+
for (const pattern of ENV_SUBSTITUTION_PATTERNS) {
|
|
3871
|
+
if (pattern.test(trimmed)) return true;
|
|
3872
|
+
}
|
|
3873
|
+
return false;
|
|
3874
|
+
}
|
|
3759
3875
|
var CHECK_ID = "security.hardcoded-secrets";
|
|
3760
3876
|
var SECRET_PATTERNS = [
|
|
3761
3877
|
{ 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 +3907,8 @@ var regexCheck = {
|
|
|
3791
3907
|
const matches = Array.from(line.matchAll(pattern.regex));
|
|
3792
3908
|
for (let matchIdx = 0; matchIdx < matches.length; matchIdx++) {
|
|
3793
3909
|
const match = matches[matchIdx];
|
|
3910
|
+
const captured = match[1] ?? match[0];
|
|
3911
|
+
if (isEnvSubstitution(captured)) continue;
|
|
3794
3912
|
findings.push({
|
|
3795
3913
|
id: `${CHECK_ID}:${file.relativePath}:${lineNum + 1}:${(match.index ?? 0) + 1}:${matchIdx}`,
|
|
3796
3914
|
checkId: CHECK_ID,
|
|
@@ -3947,6 +4065,7 @@ var tsCheck = createTreeSitterCheck({
|
|
|
3947
4065
|
const trimmedValue = value.trim();
|
|
3948
4066
|
if (!/^['"`].*['"`]$/.test(trimmedValue)) return false;
|
|
3949
4067
|
const cleanValue = trimmedValue.replace(/^['"`]|['"`]$/g, "");
|
|
4068
|
+
if (isEnvSubstitution(cleanValue)) return false;
|
|
3950
4069
|
for (const rule of NAME_RULES) {
|
|
3951
4070
|
if (rule.regex.test(name) && cleanValue.length >= rule.minLength) {
|
|
3952
4071
|
return true;
|
|
@@ -5205,7 +5324,7 @@ var MIN_VALUE_LENGTH = 8;
|
|
|
5205
5324
|
var SECRET_ASSIGNMENT_PATTERNS = [
|
|
5206
5325
|
/(?: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
5326
|
/(\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
|
-
/(?:<!--|\/\*)[^]
|
|
5327
|
+
/(?:<!--|\/\*)[^]{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
5328
|
];
|
|
5210
5329
|
var REMEDIATION9 = getCopy("remediation.configuration.secrets-in-source");
|
|
5211
5330
|
function stripQuotes(value) {
|
|
@@ -5235,7 +5354,9 @@ var regexCheck3 = {
|
|
|
5235
5354
|
const name = match[1] ?? "";
|
|
5236
5355
|
const value = match[2] ?? "";
|
|
5237
5356
|
if (!isCompoundSecretName(name)) continue;
|
|
5238
|
-
|
|
5357
|
+
const cleanValue = stripQuotes(value);
|
|
5358
|
+
if (cleanValue.length < MIN_VALUE_LENGTH) continue;
|
|
5359
|
+
if (isEnvSubstitution(cleanValue)) continue;
|
|
5239
5360
|
const before = content.slice(0, match.index ?? 0);
|
|
5240
5361
|
const line = before.split("\n").length;
|
|
5241
5362
|
const col = (before.split("\n").at(-1) ?? "").length + 1;
|
|
@@ -5264,7 +5385,9 @@ var regexCheck3 = {
|
|
|
5264
5385
|
const name = match[1] ?? "";
|
|
5265
5386
|
const value = match[2] ?? "";
|
|
5266
5387
|
if (!isCompoundSecretName(name)) continue;
|
|
5267
|
-
|
|
5388
|
+
const cleanValue = stripQuotes(value);
|
|
5389
|
+
if (cleanValue.length < MIN_VALUE_LENGTH) continue;
|
|
5390
|
+
if (isEnvSubstitution(cleanValue)) continue;
|
|
5268
5391
|
findings.push({
|
|
5269
5392
|
id: `${CHECK_ID5}:${file.relativePath}:${lineNum + 1}:${(match.index ?? 0) + 1}:${matchIdx}`,
|
|
5270
5393
|
checkId: CHECK_ID5,
|
|
@@ -5400,7 +5523,9 @@ var tsCheck3 = createTreeSitterCheck({
|
|
|
5400
5523
|
const value = captures.value;
|
|
5401
5524
|
if (!name || !value) return false;
|
|
5402
5525
|
if (!isCompoundSecretName(name)) return false;
|
|
5403
|
-
|
|
5526
|
+
const cleanValue = stripQuotes(value);
|
|
5527
|
+
if (cleanValue.length < MIN_VALUE_LENGTH) return false;
|
|
5528
|
+
return !isEnvSubstitution(cleanValue);
|
|
5404
5529
|
},
|
|
5405
5530
|
confidenceFactory: (captures) => mechanicalConfidence("ast", void 0, captures.value ?? "")
|
|
5406
5531
|
});
|
|
@@ -5630,10 +5755,25 @@ var ENTRYPOINT_PATTERNS = [
|
|
|
5630
5755
|
/(?:^|[\\/])bin[\\/]rails$/i,
|
|
5631
5756
|
/(?:^|[\\/])application_controller\.rb$/i
|
|
5632
5757
|
];
|
|
5758
|
+
var DOCKERFILE_CHECK_IDS = /* @__PURE__ */ new Set([
|
|
5759
|
+
"ops.dockerfile-runs-as-root",
|
|
5760
|
+
"ops.dockerfile-secrets-in-env",
|
|
5761
|
+
"ops.dockerfile-no-healthcheck"
|
|
5762
|
+
]);
|
|
5763
|
+
var GH_ACTIONS_CHECK_IDS = /* @__PURE__ */ new Set([
|
|
5764
|
+
"security.gh-actions-unpinned",
|
|
5765
|
+
"security.gh-actions-excessive-permissions"
|
|
5766
|
+
]);
|
|
5633
5767
|
function isProCheckEligible(file, template) {
|
|
5634
5768
|
if (template?.checkId === "security.sql-missing-rls") {
|
|
5635
5769
|
return file.language === "sql" && !isDocumentationPath(file.relativePath) && !isNonProductionPath(file.relativePath);
|
|
5636
5770
|
}
|
|
5771
|
+
if (template?.checkId && DOCKERFILE_CHECK_IDS.has(template.checkId)) {
|
|
5772
|
+
return file.language === "dockerfile" && !isDocumentationPath(file.relativePath) && !isNonProductionPath(file.relativePath);
|
|
5773
|
+
}
|
|
5774
|
+
if (template?.checkId && GH_ACTIONS_CHECK_IDS.has(template.checkId)) {
|
|
5775
|
+
return file.language === "yaml" && /(?:^|[\\/])\.github[\\/]workflows[\\/]/i.test(file.relativePath) && !isDocumentationPath(file.relativePath) && !isNonProductionPath(file.relativePath);
|
|
5776
|
+
}
|
|
5637
5777
|
return PRO_CHECK_SOURCE_LANGUAGES.has(file.language) && !isDocumentationPath(file.relativePath) && !isNonProductionPath(file.relativePath);
|
|
5638
5778
|
}
|
|
5639
5779
|
function pickProjectAnchorFile(files) {
|
|
@@ -7521,6 +7661,10 @@ registry.register({
|
|
|
7521
7661
|
}
|
|
7522
7662
|
});
|
|
7523
7663
|
var CHECK_ID10 = "security.xss-surface";
|
|
7664
|
+
var CGI_PATH_HINT = new RegExp("(^|[/_-])cgi([/_.-]|$)", "i");
|
|
7665
|
+
function isLikelyCgiBashScript(file) {
|
|
7666
|
+
return CGI_PATH_HINT.test(file.relativePath);
|
|
7667
|
+
}
|
|
7524
7668
|
var QUERIES = {
|
|
7525
7669
|
javascript: `
|
|
7526
7670
|
(call_expression
|
|
@@ -7804,7 +7948,10 @@ registry.register(
|
|
|
7804
7948
|
queries: QUERIES,
|
|
7805
7949
|
messageKey: "checks.xss-surface.message",
|
|
7806
7950
|
degradedMessageKey: "checks.xss-surface.degraded",
|
|
7807
|
-
remediationKey: "remediation.security.xss-surface"
|
|
7951
|
+
remediationKey: "remediation.security.xss-surface",
|
|
7952
|
+
// Bash echo $VAR writes to stdout, not a browser. Only treat bash
|
|
7953
|
+
// scripts as XSS-relevant when they look like CGI handlers.
|
|
7954
|
+
fileFilter: (file) => file.language !== "bash" || isLikelyCgiBashScript(file)
|
|
7808
7955
|
})
|
|
7809
7956
|
);
|
|
7810
7957
|
var CHECK_ID11 = "security.os-command-injection";
|
|
@@ -14732,6 +14879,556 @@ registry.register({
|
|
|
14732
14879
|
}
|
|
14733
14880
|
}
|
|
14734
14881
|
});
|
|
14882
|
+
var PROMPT_VERSION31 = "1.0.0";
|
|
14883
|
+
var fallbackPatterns31 = [
|
|
14884
|
+
{
|
|
14885
|
+
regex: /helmet\s*\(/i,
|
|
14886
|
+
message: "No HTTP security headers middleware detected",
|
|
14887
|
+
severity: "medium",
|
|
14888
|
+
absenceBased: true
|
|
14889
|
+
}
|
|
14890
|
+
];
|
|
14891
|
+
async function buildPrompt31(sourceFiles) {
|
|
14892
|
+
const parts = [
|
|
14893
|
+
"Check if the application configures HTTP security response headers.",
|
|
14894
|
+
"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.",
|
|
14895
|
+
"In Python Django, check for SECURE_HSTS_SECONDS, SECURE_CONTENT_TYPE_NOSNIFF, X_FRAME_OPTIONS in settings files.",
|
|
14896
|
+
"In Python FastAPI/Flask, check for SecurityHeadersMiddleware or manual response.headers assignments covering these headers.",
|
|
14897
|
+
"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."
|
|
14898
|
+
];
|
|
14899
|
+
for (const file of sourceFiles) {
|
|
14900
|
+
const content = await file.content();
|
|
14901
|
+
parts.push(`--- ${file.relativePath} ---
|
|
14902
|
+
${content.slice(0, 3e3)}`);
|
|
14903
|
+
}
|
|
14904
|
+
return parts.join("\n\n");
|
|
14905
|
+
}
|
|
14906
|
+
function parseResponse31(response, checkId) {
|
|
14907
|
+
return parseLLMResponse(response, checkId, "medium");
|
|
14908
|
+
}
|
|
14909
|
+
var promptTemplate31 = {
|
|
14910
|
+
checkId: "security.missing-security-headers",
|
|
14911
|
+
promptVersion: PROMPT_VERSION31,
|
|
14912
|
+
projectScoped: true,
|
|
14913
|
+
buildPrompt: buildPrompt31,
|
|
14914
|
+
parseResponse: parseResponse31,
|
|
14915
|
+
fallbackPatterns: fallbackPatterns31
|
|
14916
|
+
};
|
|
14917
|
+
registry.register({
|
|
14918
|
+
id: "security.missing-security-headers",
|
|
14919
|
+
name: "Missing security headers",
|
|
14920
|
+
description: "Application does not set HTTP security response headers",
|
|
14921
|
+
category: "security",
|
|
14922
|
+
severity: "medium",
|
|
14923
|
+
minTier: "pro",
|
|
14924
|
+
async run(ctx) {
|
|
14925
|
+
return runLLMCheck(ctx, promptTemplate31);
|
|
14926
|
+
}
|
|
14927
|
+
});
|
|
14928
|
+
var PROMPT_VERSION32 = "1.0.0";
|
|
14929
|
+
var fallbackPatterns32 = [
|
|
14930
|
+
{
|
|
14931
|
+
regex: /uses:\s+(?!\.\/)\S+@(?![0-9a-f]{40}\b)\S+/,
|
|
14932
|
+
message: "GitHub Actions step uses an unpinned action reference (not a 40-char commit SHA)",
|
|
14933
|
+
severity: "medium"
|
|
14934
|
+
}
|
|
14935
|
+
];
|
|
14936
|
+
async function buildPrompt32(sourceFiles) {
|
|
14937
|
+
const parts = [
|
|
14938
|
+
"Check GitHub Actions workflow files for third-party action steps not pinned to a full 40-character commit SHA.",
|
|
14939
|
+
"Safe: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683",
|
|
14940
|
+
"Unsafe (flag these): uses: actions/checkout@v4, uses: actions/checkout@main, uses: actions/checkout@latest, uses: actions/checkout@master",
|
|
14941
|
+
"First-party actions (uses: ./ prefix) are always acceptable \u2014 do not flag them.",
|
|
14942
|
+
'Flag every "uses:" line referencing a third-party action without a 40-hex-char commit SHA.'
|
|
14943
|
+
];
|
|
14944
|
+
for (const file of sourceFiles) {
|
|
14945
|
+
const content = await file.content();
|
|
14946
|
+
parts.push(`--- ${file.relativePath} ---
|
|
14947
|
+
${content.slice(0, 4e3)}`);
|
|
14948
|
+
}
|
|
14949
|
+
return parts.join("\n\n");
|
|
14950
|
+
}
|
|
14951
|
+
function parseResponse32(response, checkId) {
|
|
14952
|
+
return parseLLMResponse(response, checkId, "medium");
|
|
14953
|
+
}
|
|
14954
|
+
var promptTemplate32 = {
|
|
14955
|
+
checkId: "security.gh-actions-unpinned",
|
|
14956
|
+
promptVersion: PROMPT_VERSION32,
|
|
14957
|
+
buildPrompt: buildPrompt32,
|
|
14958
|
+
parseResponse: parseResponse32,
|
|
14959
|
+
fallbackPatterns: fallbackPatterns32
|
|
14960
|
+
};
|
|
14961
|
+
registry.register({
|
|
14962
|
+
id: "security.gh-actions-unpinned",
|
|
14963
|
+
name: "Unpinned GitHub Actions",
|
|
14964
|
+
description: "GitHub Actions steps reference third-party actions by mutable tag rather than commit SHA",
|
|
14965
|
+
category: "security",
|
|
14966
|
+
severity: "medium",
|
|
14967
|
+
minTier: "pro",
|
|
14968
|
+
async run(ctx) {
|
|
14969
|
+
return runLLMCheck(ctx, promptTemplate32);
|
|
14970
|
+
}
|
|
14971
|
+
});
|
|
14972
|
+
var PROMPT_VERSION33 = "1.0.0";
|
|
14973
|
+
var fallbackPatterns33 = [
|
|
14974
|
+
{
|
|
14975
|
+
regex: /permissions:\s*write-all/i,
|
|
14976
|
+
message: "Workflow grants write-all permissions",
|
|
14977
|
+
severity: "low"
|
|
14978
|
+
}
|
|
14979
|
+
];
|
|
14980
|
+
async function buildPrompt33(sourceFiles) {
|
|
14981
|
+
const parts = [
|
|
14982
|
+
"Check GitHub Actions workflow files for excessive or undeclared permission grants.",
|
|
14983
|
+
`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).`,
|
|
14984
|
+
'A well-secured workflow adds "permissions: {}" at the top level and grants only the minimum needed per job, e.g. "permissions: { contents: read }".',
|
|
14985
|
+
"Do NOT flag workflows that already have explicitly scoped, narrow permissions defined."
|
|
14986
|
+
];
|
|
14987
|
+
for (const file of sourceFiles) {
|
|
14988
|
+
const content = await file.content();
|
|
14989
|
+
parts.push(`--- ${file.relativePath} ---
|
|
14990
|
+
${content.slice(0, 4e3)}`);
|
|
14991
|
+
}
|
|
14992
|
+
return parts.join("\n\n");
|
|
14993
|
+
}
|
|
14994
|
+
function parseResponse33(response, checkId) {
|
|
14995
|
+
return parseLLMResponse(response, checkId, "low");
|
|
14996
|
+
}
|
|
14997
|
+
var promptTemplate33 = {
|
|
14998
|
+
checkId: "security.gh-actions-excessive-permissions",
|
|
14999
|
+
promptVersion: PROMPT_VERSION33,
|
|
15000
|
+
buildPrompt: buildPrompt33,
|
|
15001
|
+
parseResponse: parseResponse33,
|
|
15002
|
+
fallbackPatterns: fallbackPatterns33
|
|
15003
|
+
};
|
|
15004
|
+
registry.register({
|
|
15005
|
+
id: "security.gh-actions-excessive-permissions",
|
|
15006
|
+
name: "Excessive GitHub Actions permissions",
|
|
15007
|
+
description: "GitHub Actions workflow grants overly broad or undeclared permissions",
|
|
15008
|
+
category: "security",
|
|
15009
|
+
severity: "low",
|
|
15010
|
+
minTier: "pro",
|
|
15011
|
+
async run(ctx) {
|
|
15012
|
+
return runLLMCheck(ctx, promptTemplate33);
|
|
15013
|
+
}
|
|
15014
|
+
});
|
|
15015
|
+
var PROMPT_VERSION34 = "1.0.0";
|
|
15016
|
+
var fallbackPatterns34 = [
|
|
15017
|
+
{
|
|
15018
|
+
regex: /introspection:\s*true/i,
|
|
15019
|
+
message: "GraphQL introspection is explicitly enabled \u2014 disable in production",
|
|
15020
|
+
severity: "medium"
|
|
15021
|
+
}
|
|
15022
|
+
];
|
|
15023
|
+
async function buildPrompt34(sourceFiles) {
|
|
15024
|
+
const parts = [
|
|
15025
|
+
"Check if a GraphQL server has introspection enabled for production traffic.",
|
|
15026
|
+
'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.',
|
|
15027
|
+
"In GraphQL Yoga or Mercurius, check for equivalent introspection settings.",
|
|
15028
|
+
"In Python (Strawberry, graphene), flag schema servers without a mechanism to disable introspection in non-development environments.",
|
|
15029
|
+
"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."
|
|
15030
|
+
];
|
|
15031
|
+
for (const file of sourceFiles) {
|
|
15032
|
+
const content = await file.content();
|
|
15033
|
+
parts.push(`--- ${file.relativePath} ---
|
|
15034
|
+
${content.slice(0, 3e3)}`);
|
|
15035
|
+
}
|
|
15036
|
+
return parts.join("\n\n");
|
|
15037
|
+
}
|
|
15038
|
+
function parseResponse34(response, checkId) {
|
|
15039
|
+
return parseLLMResponse(response, checkId, "medium");
|
|
15040
|
+
}
|
|
15041
|
+
var promptTemplate34 = {
|
|
15042
|
+
checkId: "security.graphql-introspection-prod",
|
|
15043
|
+
promptVersion: PROMPT_VERSION34,
|
|
15044
|
+
buildPrompt: buildPrompt34,
|
|
15045
|
+
parseResponse: parseResponse34,
|
|
15046
|
+
fallbackPatterns: fallbackPatterns34
|
|
15047
|
+
};
|
|
15048
|
+
registry.register({
|
|
15049
|
+
id: "security.graphql-introspection-prod",
|
|
15050
|
+
name: "GraphQL introspection in production",
|
|
15051
|
+
description: "GraphQL server exposes introspection without disabling it for production",
|
|
15052
|
+
category: "security",
|
|
15053
|
+
severity: "medium",
|
|
15054
|
+
minTier: "pro",
|
|
15055
|
+
async run(ctx) {
|
|
15056
|
+
return runLLMCheck(ctx, promptTemplate34);
|
|
15057
|
+
}
|
|
15058
|
+
});
|
|
15059
|
+
var PROMPT_VERSION35 = "1.0.0";
|
|
15060
|
+
var fallbackPatterns35 = [];
|
|
15061
|
+
async function buildPrompt35(sourceFiles) {
|
|
15062
|
+
const parts = [
|
|
15063
|
+
"Check if a GraphQL server enforces query depth or complexity limits.",
|
|
15064
|
+
"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.",
|
|
15065
|
+
"Without depth limiting, deeply nested queries cause exponential resolver work and can be used as a denial-of-service vector.",
|
|
15066
|
+
"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."
|
|
15067
|
+
];
|
|
15068
|
+
for (const file of sourceFiles) {
|
|
15069
|
+
const content = await file.content();
|
|
15070
|
+
parts.push(`--- ${file.relativePath} ---
|
|
15071
|
+
${content.slice(0, 3e3)}`);
|
|
15072
|
+
}
|
|
15073
|
+
return parts.join("\n\n");
|
|
15074
|
+
}
|
|
15075
|
+
function parseResponse35(response, checkId) {
|
|
15076
|
+
return parseLLMResponse(response, checkId, "medium");
|
|
15077
|
+
}
|
|
15078
|
+
var promptTemplate35 = {
|
|
15079
|
+
checkId: "security.graphql-no-depth-limit",
|
|
15080
|
+
promptVersion: PROMPT_VERSION35,
|
|
15081
|
+
projectScoped: true,
|
|
15082
|
+
buildPrompt: buildPrompt35,
|
|
15083
|
+
parseResponse: parseResponse35,
|
|
15084
|
+
fallbackPatterns: fallbackPatterns35
|
|
15085
|
+
};
|
|
15086
|
+
registry.register({
|
|
15087
|
+
id: "security.graphql-no-depth-limit",
|
|
15088
|
+
name: "GraphQL no depth limit",
|
|
15089
|
+
description: "GraphQL server has no query depth or complexity limiting",
|
|
15090
|
+
category: "security",
|
|
15091
|
+
severity: "medium",
|
|
15092
|
+
minTier: "pro",
|
|
15093
|
+
async run(ctx) {
|
|
15094
|
+
return runLLMCheck(ctx, promptTemplate35);
|
|
15095
|
+
}
|
|
15096
|
+
});
|
|
15097
|
+
var PROMPT_VERSION36 = "1.0.0";
|
|
15098
|
+
var fallbackPatterns36 = [
|
|
15099
|
+
{
|
|
15100
|
+
regex: /algorithm[s'"`]*\s*[=:]\s*['"`]none['"`]/i,
|
|
15101
|
+
message: 'JWT algorithm set to "none" \u2014 token signatures are not verified',
|
|
15102
|
+
severity: "high"
|
|
15103
|
+
}
|
|
15104
|
+
];
|
|
15105
|
+
async function buildPrompt36(sourceFiles) {
|
|
15106
|
+
const parts = [
|
|
15107
|
+
"Check for insecure JWT algorithm configurations.",
|
|
15108
|
+
'Flag any explicit use of algorithm "none" in JWT signing or verification (jsonwebtoken, jose, PyJWT, ruby-jwt, etc.).',
|
|
15109
|
+
'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.',
|
|
15110
|
+
"In Python PyJWT: flag jwt.decode() missing the algorithms=[...] parameter.",
|
|
15111
|
+
'In Node.js jsonwebtoken: flag jwt.verify() without an algorithms option, or with algorithm/algorithms set to "none".',
|
|
15112
|
+
"In jose: flag jwtVerify() without explicit algorithms.",
|
|
15113
|
+
"Return [] if all verify/decode calls have an explicit non-empty algorithms allowlist."
|
|
15114
|
+
];
|
|
15115
|
+
for (const file of sourceFiles) {
|
|
15116
|
+
const content = await file.content();
|
|
15117
|
+
parts.push(`--- ${file.relativePath} ---
|
|
15118
|
+
${content.slice(0, 3e3)}`);
|
|
15119
|
+
}
|
|
15120
|
+
return parts.join("\n\n");
|
|
15121
|
+
}
|
|
15122
|
+
function parseResponse36(response, checkId) {
|
|
15123
|
+
return parseLLMResponse(response, checkId, "high");
|
|
15124
|
+
}
|
|
15125
|
+
var promptTemplate36 = {
|
|
15126
|
+
checkId: "security.jwt-alg-none",
|
|
15127
|
+
promptVersion: PROMPT_VERSION36,
|
|
15128
|
+
buildPrompt: buildPrompt36,
|
|
15129
|
+
parseResponse: parseResponse36,
|
|
15130
|
+
fallbackPatterns: fallbackPatterns36
|
|
15131
|
+
};
|
|
15132
|
+
registry.register({
|
|
15133
|
+
id: "security.jwt-alg-none",
|
|
15134
|
+
name: "JWT algorithm none",
|
|
15135
|
+
description: 'JWT verification allows the "none" algorithm or lacks an algorithms allowlist',
|
|
15136
|
+
category: "security",
|
|
15137
|
+
severity: "high",
|
|
15138
|
+
minTier: "pro",
|
|
15139
|
+
async run(ctx) {
|
|
15140
|
+
return runLLMCheck(ctx, promptTemplate36);
|
|
15141
|
+
}
|
|
15142
|
+
});
|
|
15143
|
+
var PROMPT_VERSION37 = "1.0.1";
|
|
15144
|
+
var fallbackPatterns37 = [];
|
|
15145
|
+
async function buildPrompt37(sourceFiles) {
|
|
15146
|
+
const parts = [
|
|
15147
|
+
"Check for JWT tokens issued without an expiry claim.",
|
|
15148
|
+
'Flag jwt.sign(payload, secret) calls (jsonwebtoken) that have no "expiresIn" in the options argument.',
|
|
15149
|
+
"Flag new SignJWT(payload).sign(key) calls (jose) that do not chain .setExpirationTime(...).",
|
|
15150
|
+
'Flag jwt.encode(payload, key) (PyJWT) where the payload dict has no "exp" key.',
|
|
15151
|
+
"Tokens without expiry are valid indefinitely \u2014 a stolen token cannot be invalidated through expiry.",
|
|
15152
|
+
'Do NOT flag if the payload object itself already contains an "exp" property set before the sign call.',
|
|
15153
|
+
"Do NOT flag short-lived access tokens that have expiresIn set."
|
|
15154
|
+
];
|
|
15155
|
+
for (const file of sourceFiles) {
|
|
15156
|
+
const content = await file.content();
|
|
15157
|
+
parts.push(`--- ${file.relativePath} ---
|
|
15158
|
+
${content.slice(0, 3e3)}`);
|
|
15159
|
+
}
|
|
15160
|
+
return parts.join("\n\n");
|
|
15161
|
+
}
|
|
15162
|
+
function parseResponse37(response, checkId) {
|
|
15163
|
+
return parseLLMResponse(response, checkId, "medium");
|
|
15164
|
+
}
|
|
15165
|
+
var promptTemplate37 = {
|
|
15166
|
+
checkId: "security.jwt-no-expiry",
|
|
15167
|
+
promptVersion: PROMPT_VERSION37,
|
|
15168
|
+
buildPrompt: buildPrompt37,
|
|
15169
|
+
parseResponse: parseResponse37,
|
|
15170
|
+
fallbackPatterns: fallbackPatterns37
|
|
15171
|
+
};
|
|
15172
|
+
registry.register({
|
|
15173
|
+
id: "security.jwt-no-expiry",
|
|
15174
|
+
name: "JWT issued without expiry",
|
|
15175
|
+
description: "JWT tokens are signed without an expiry claim, making them valid indefinitely",
|
|
15176
|
+
category: "security",
|
|
15177
|
+
severity: "medium",
|
|
15178
|
+
minTier: "pro",
|
|
15179
|
+
async run(ctx) {
|
|
15180
|
+
return runLLMCheck(ctx, promptTemplate37);
|
|
15181
|
+
}
|
|
15182
|
+
});
|
|
15183
|
+
var PROMPT_VERSION38 = "1.0.0";
|
|
15184
|
+
var fallbackPatterns38 = [
|
|
15185
|
+
{
|
|
15186
|
+
regex: /(?:localStorage|sessionStorage)\.setItem\s*\(\s*['"`][^'"`]*(?:refresh|refreshToken|refresh_token)[^'"`]*['"`]/i,
|
|
15187
|
+
message: "Refresh token or long-lived credential stored in localStorage/sessionStorage",
|
|
15188
|
+
severity: "high"
|
|
15189
|
+
}
|
|
15190
|
+
];
|
|
15191
|
+
async function buildPrompt38(sourceFiles) {
|
|
15192
|
+
const parts = [
|
|
15193
|
+
"Check for refresh tokens or long-lived authentication credentials written to browser localStorage or sessionStorage.",
|
|
15194
|
+
'Flag any code that calls localStorage.setItem() or sessionStorage.setItem() (or equivalent bracket notation assignments) with a key containing "refresh", "refreshToken", or "refresh_token".',
|
|
15195
|
+
"Also flag storing full auth/session tokens intended for reuse across browser sessions in localStorage.",
|
|
15196
|
+
"XSS attacks can read all localStorage values \u2014 refresh tokens should be in httpOnly cookies instead.",
|
|
15197
|
+
"Short-lived access tokens held in JavaScript memory variables (not persisted to storage) are acceptable. Only flag explicit writes to localStorage/sessionStorage."
|
|
15198
|
+
];
|
|
15199
|
+
for (const file of sourceFiles) {
|
|
15200
|
+
const content = await file.content();
|
|
15201
|
+
parts.push(`--- ${file.relativePath} ---
|
|
15202
|
+
${content.slice(0, 3e3)}`);
|
|
15203
|
+
}
|
|
15204
|
+
return parts.join("\n\n");
|
|
15205
|
+
}
|
|
15206
|
+
function parseResponse38(response, checkId) {
|
|
15207
|
+
return parseLLMResponse(response, checkId, "high");
|
|
15208
|
+
}
|
|
15209
|
+
var promptTemplate38 = {
|
|
15210
|
+
checkId: "security.refresh-token-in-localstorage",
|
|
15211
|
+
promptVersion: PROMPT_VERSION38,
|
|
15212
|
+
buildPrompt: buildPrompt38,
|
|
15213
|
+
parseResponse: parseResponse38,
|
|
15214
|
+
fallbackPatterns: fallbackPatterns38
|
|
15215
|
+
};
|
|
15216
|
+
registry.register({
|
|
15217
|
+
id: "security.refresh-token-in-localstorage",
|
|
15218
|
+
name: "Refresh token in localStorage",
|
|
15219
|
+
description: "Refresh tokens or long-lived credentials stored in localStorage are vulnerable to XSS theft",
|
|
15220
|
+
category: "security",
|
|
15221
|
+
severity: "high",
|
|
15222
|
+
minTier: "pro",
|
|
15223
|
+
async run(ctx) {
|
|
15224
|
+
return runLLMCheck(ctx, promptTemplate38);
|
|
15225
|
+
}
|
|
15226
|
+
});
|
|
15227
|
+
var PROMPT_VERSION39 = "1.0.0";
|
|
15228
|
+
var fallbackPatterns39 = [
|
|
15229
|
+
{
|
|
15230
|
+
regex: /^\s*USER\s+(?!root\b|0\b)/im,
|
|
15231
|
+
message: "Dockerfile has no non-root USER directive \u2014 container runs as root",
|
|
15232
|
+
severity: "medium",
|
|
15233
|
+
absenceBased: true
|
|
15234
|
+
}
|
|
15235
|
+
];
|
|
15236
|
+
async function buildPrompt39(sourceFiles) {
|
|
15237
|
+
const parts = [
|
|
15238
|
+
"Check Dockerfiles for containers running as root.",
|
|
15239
|
+
"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).",
|
|
15240
|
+
"A secure Dockerfile creates a non-privileged user and switches to it, e.g.: RUN adduser --disabled-password appuser followed by USER appuser.",
|
|
15241
|
+
"Running containers as root means any process escape gains host root privileges.",
|
|
15242
|
+
"Return [] if a non-root USER is set before CMD/ENTRYPOINT."
|
|
15243
|
+
];
|
|
15244
|
+
for (const file of sourceFiles) {
|
|
15245
|
+
const content = await file.content();
|
|
15246
|
+
parts.push(`--- ${file.relativePath} ---
|
|
15247
|
+
${content}`);
|
|
15248
|
+
}
|
|
15249
|
+
return parts.join("\n\n");
|
|
15250
|
+
}
|
|
15251
|
+
function parseResponse39(response, checkId) {
|
|
15252
|
+
return parseLLMResponse(response, checkId, "medium");
|
|
15253
|
+
}
|
|
15254
|
+
var promptTemplate39 = {
|
|
15255
|
+
checkId: "ops.dockerfile-runs-as-root",
|
|
15256
|
+
promptVersion: PROMPT_VERSION39,
|
|
15257
|
+
buildPrompt: buildPrompt39,
|
|
15258
|
+
parseResponse: parseResponse39,
|
|
15259
|
+
fallbackPatterns: fallbackPatterns39
|
|
15260
|
+
};
|
|
15261
|
+
registry.register({
|
|
15262
|
+
id: "ops.dockerfile-runs-as-root",
|
|
15263
|
+
name: "Dockerfile runs as root",
|
|
15264
|
+
description: "Container image has no non-root USER directive",
|
|
15265
|
+
category: "ops",
|
|
15266
|
+
severity: "medium",
|
|
15267
|
+
minTier: "pro",
|
|
15268
|
+
async run(ctx) {
|
|
15269
|
+
return runLLMCheck(ctx, promptTemplate39);
|
|
15270
|
+
}
|
|
15271
|
+
});
|
|
15272
|
+
var PROMPT_VERSION40 = "1.0.0";
|
|
15273
|
+
var fallbackPatterns40 = [
|
|
15274
|
+
{
|
|
15275
|
+
regex: /^\s*(?:ENV|ARG)\s+\S*(?:KEY|TOKEN|SECRET|PASSWORD|PASS|CREDENTIAL|API_KEY|PRIVATE)\s*=\s*\S+/im,
|
|
15276
|
+
message: "Dockerfile bakes a secret-shaped value into the image via ENV or ARG",
|
|
15277
|
+
severity: "high"
|
|
15278
|
+
}
|
|
15279
|
+
];
|
|
15280
|
+
async function buildPrompt40(sourceFiles) {
|
|
15281
|
+
const parts = [
|
|
15282
|
+
"Check Dockerfiles for secrets or credentials baked into image layers via ENV or ARG instructions.",
|
|
15283
|
+
"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.",
|
|
15284
|
+
'ENV values are baked into every image layer and visible via "docker inspect". ARG values are also stored in the build history.',
|
|
15285
|
+
"Flag: ENV DATABASE_URL=postgres://user:password@host/db, ENV API_KEY=sk-abc123, ARG STRIPE_SECRET=sk_live_...",
|
|
15286
|
+
"Do NOT flag: ENV PORT=3000, ENV NODE_ENV=production, ARG BUILD_DATE, or ENV DATABASE_URL= (empty assignment)."
|
|
15287
|
+
];
|
|
15288
|
+
for (const file of sourceFiles) {
|
|
15289
|
+
const content = await file.content();
|
|
15290
|
+
parts.push(`--- ${file.relativePath} ---
|
|
15291
|
+
${content}`);
|
|
15292
|
+
}
|
|
15293
|
+
return parts.join("\n\n");
|
|
15294
|
+
}
|
|
15295
|
+
function parseResponse40(response, checkId) {
|
|
15296
|
+
return parseLLMResponse(response, checkId, "high");
|
|
15297
|
+
}
|
|
15298
|
+
var promptTemplate40 = {
|
|
15299
|
+
checkId: "ops.dockerfile-secrets-in-env",
|
|
15300
|
+
promptVersion: PROMPT_VERSION40,
|
|
15301
|
+
buildPrompt: buildPrompt40,
|
|
15302
|
+
parseResponse: parseResponse40,
|
|
15303
|
+
fallbackPatterns: fallbackPatterns40
|
|
15304
|
+
};
|
|
15305
|
+
registry.register({
|
|
15306
|
+
id: "ops.dockerfile-secrets-in-env",
|
|
15307
|
+
name: "Secrets in Dockerfile ENV",
|
|
15308
|
+
description: "Dockerfile bakes secrets into image layers via ENV or ARG instructions",
|
|
15309
|
+
category: "ops",
|
|
15310
|
+
severity: "high",
|
|
15311
|
+
minTier: "pro",
|
|
15312
|
+
async run(ctx) {
|
|
15313
|
+
return runLLMCheck(ctx, promptTemplate40);
|
|
15314
|
+
}
|
|
15315
|
+
});
|
|
15316
|
+
var PROMPT_VERSION41 = "1.0.0";
|
|
15317
|
+
var fallbackPatterns41 = [
|
|
15318
|
+
{
|
|
15319
|
+
regex: /^\s*HEALTHCHECK\b/im,
|
|
15320
|
+
message: "Dockerfile has no HEALTHCHECK instruction \u2014 container orchestrators cannot detect unhealthy state",
|
|
15321
|
+
severity: "low",
|
|
15322
|
+
absenceBased: true
|
|
15323
|
+
}
|
|
15324
|
+
];
|
|
15325
|
+
async function buildPrompt41(sourceFiles) {
|
|
15326
|
+
const parts = [
|
|
15327
|
+
"Check if a Dockerfile includes a HEALTHCHECK instruction.",
|
|
15328
|
+
"Without HEALTHCHECK, Docker and container orchestrators like Kubernetes cannot detect an unhealthy container and will continue routing traffic to it.",
|
|
15329
|
+
"Flag Dockerfiles that have no HEALTHCHECK instruction at all.",
|
|
15330
|
+
"A minimal HEALTHCHECK example: HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8080/health || exit 1",
|
|
15331
|
+
"Return [] if a HEALTHCHECK instruction is present."
|
|
15332
|
+
];
|
|
15333
|
+
for (const file of sourceFiles) {
|
|
15334
|
+
const content = await file.content();
|
|
15335
|
+
parts.push(`--- ${file.relativePath} ---
|
|
15336
|
+
${content}`);
|
|
15337
|
+
}
|
|
15338
|
+
return parts.join("\n\n");
|
|
15339
|
+
}
|
|
15340
|
+
function parseResponse41(response, checkId) {
|
|
15341
|
+
return parseLLMResponse(response, checkId, "low");
|
|
15342
|
+
}
|
|
15343
|
+
var promptTemplate41 = {
|
|
15344
|
+
checkId: "ops.dockerfile-no-healthcheck",
|
|
15345
|
+
promptVersion: PROMPT_VERSION41,
|
|
15346
|
+
buildPrompt: buildPrompt41,
|
|
15347
|
+
parseResponse: parseResponse41,
|
|
15348
|
+
fallbackPatterns: fallbackPatterns41
|
|
15349
|
+
};
|
|
15350
|
+
registry.register({
|
|
15351
|
+
id: "ops.dockerfile-no-healthcheck",
|
|
15352
|
+
name: "Dockerfile missing HEALTHCHECK",
|
|
15353
|
+
description: "Dockerfile has no HEALTHCHECK instruction \u2014 orchestrators cannot detect unhealthy containers",
|
|
15354
|
+
category: "ops",
|
|
15355
|
+
severity: "low",
|
|
15356
|
+
minTier: "pro",
|
|
15357
|
+
async run(ctx) {
|
|
15358
|
+
return runLLMCheck(ctx, promptTemplate41);
|
|
15359
|
+
}
|
|
15360
|
+
});
|
|
15361
|
+
registry.register({
|
|
15362
|
+
id: "ops.missing-dockerignore",
|
|
15363
|
+
name: "Missing .dockerignore",
|
|
15364
|
+
description: "Dockerfile present without a .dockerignore \u2014 build context may leak sensitive files",
|
|
15365
|
+
category: "ops",
|
|
15366
|
+
severity: "low",
|
|
15367
|
+
minTier: "pro",
|
|
15368
|
+
async run(ctx) {
|
|
15369
|
+
const dockerfile = ctx.files.find((f) => f.language === "dockerfile");
|
|
15370
|
+
if (!dockerfile) return [];
|
|
15371
|
+
const hasDockerignore = ctx.files.some((f) => /(?:^|[\\/])\.dockerignore$/.test(f.relativePath));
|
|
15372
|
+
if (hasDockerignore) return [];
|
|
15373
|
+
let remediation;
|
|
15374
|
+
try {
|
|
15375
|
+
remediation = getCopy("remediation.ops.missing-dockerignore");
|
|
15376
|
+
} catch {
|
|
15377
|
+
}
|
|
15378
|
+
return [
|
|
15379
|
+
{
|
|
15380
|
+
id: "ops.missing-dockerignore-1",
|
|
15381
|
+
checkId: "ops.missing-dockerignore",
|
|
15382
|
+
category: "ops",
|
|
15383
|
+
severity: "low",
|
|
15384
|
+
message: "Dockerfile exists but no .dockerignore found \u2014 sensitive files (node_modules, .env, secrets) may be included in the build context",
|
|
15385
|
+
file: dockerfile.relativePath,
|
|
15386
|
+
line: 1,
|
|
15387
|
+
remediation
|
|
15388
|
+
}
|
|
15389
|
+
];
|
|
15390
|
+
}
|
|
15391
|
+
});
|
|
15392
|
+
var PROMPT_VERSION42 = "1.0.0";
|
|
15393
|
+
var fallbackPatterns42 = [];
|
|
15394
|
+
async function buildPrompt42(sourceFiles) {
|
|
15395
|
+
const parts = [
|
|
15396
|
+
"Check for N+1 query patterns where ORM results are iterated and a new database query is issued per row.",
|
|
15397
|
+
"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.",
|
|
15398
|
+
"In Sequelize (Node.js): flag findAll() results where model.find() or association.get() is called per element inside a loop.",
|
|
15399
|
+
"In TypeORM: flag find()/findBy() results with repository calls inside the iteration.",
|
|
15400
|
+
"In SQLAlchemy (Python): flag query.all() or session.scalars() results where session.get() or a new query is executed per row.",
|
|
15401
|
+
"In Active Record (Ruby): flag .all or .where results iterated with .each where an association is accessed without eager loading (use .includes instead).",
|
|
15402
|
+
"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."
|
|
15403
|
+
];
|
|
15404
|
+
for (const file of sourceFiles) {
|
|
15405
|
+
const content = await file.content();
|
|
15406
|
+
parts.push(`--- ${file.relativePath} ---
|
|
15407
|
+
${content.slice(0, 3e3)}`);
|
|
15408
|
+
}
|
|
15409
|
+
return parts.join("\n\n");
|
|
15410
|
+
}
|
|
15411
|
+
function parseResponse42(response, checkId) {
|
|
15412
|
+
return parseLLMResponse(response, checkId, "medium");
|
|
15413
|
+
}
|
|
15414
|
+
var promptTemplate42 = {
|
|
15415
|
+
checkId: "resilience.orm-n-plus-one",
|
|
15416
|
+
promptVersion: PROMPT_VERSION42,
|
|
15417
|
+
buildPrompt: buildPrompt42,
|
|
15418
|
+
parseResponse: parseResponse42,
|
|
15419
|
+
fallbackPatterns: fallbackPatterns42
|
|
15420
|
+
};
|
|
15421
|
+
registry.register({
|
|
15422
|
+
id: "resilience.orm-n-plus-one",
|
|
15423
|
+
name: "ORM N+1 query pattern",
|
|
15424
|
+
description: "ORM results are iterated with a per-row database query instead of a batch load",
|
|
15425
|
+
category: "resilience",
|
|
15426
|
+
severity: "medium",
|
|
15427
|
+
minTier: "pro",
|
|
15428
|
+
async run(ctx) {
|
|
15429
|
+
return runLLMCheck(ctx, promptTemplate42);
|
|
15430
|
+
}
|
|
15431
|
+
});
|
|
14735
15432
|
var LANGUAGE_CODES = [
|
|
14736
15433
|
"javascript",
|
|
14737
15434
|
"typescript",
|
|
@@ -15151,7 +15848,7 @@ var copy3 = {
|
|
|
15151
15848
|
"cli.setup.apiKeyPrompt": "API key for",
|
|
15152
15849
|
"cli.setup.modelPrompt": "Model (press Enter for default)",
|
|
15153
15850
|
"cli.setup.baseUrlPrompt": "Custom API base URL (press Enter to use default)",
|
|
15154
|
-
"cli.setup.done": "Config saved. Run `
|
|
15851
|
+
"cli.setup.done": "Config saved. Run `npx seaworthycode .` to scan your project.",
|
|
15155
15852
|
"cli.setup.skipped": "Setup skipped. Running free-tier scan.",
|
|
15156
15853
|
"cli.firstRun": "First time? Let's get you set up.",
|
|
15157
15854
|
"cli.category.security": "Security",
|
|
@@ -15177,7 +15874,19 @@ var copy3 = {
|
|
|
15177
15874
|
"cli.error.config.unknownKey": "Unknown config key. Valid keys: licenseKey, llmProvider, llmApiKey, llmModel, llmApiUrl",
|
|
15178
15875
|
"cli.error.config.invalidProvider": "Invalid provider. Valid providers: anthropic, openai, kimi, moonshot, deepseek, gemini, ollama, openrouter",
|
|
15179
15876
|
"cli.error.config.invalidAction": "Invalid config action. Usage: seaworthy config <set|get|list>",
|
|
15180
|
-
"cli.config.setSuccess": (key) => `${key} updated
|
|
15877
|
+
"cli.config.setSuccess": (key) => `${key} updated.`,
|
|
15878
|
+
"cli.licenseNotice": (licensedTo) => `Licensed to ${licensedTo}`,
|
|
15879
|
+
"cli.error.debugHint": (logPath) => `Re-run with --debug to write a diagnostic log: ${logPath}
|
|
15880
|
+
Report persistent issues: npx seaworthycode bugreport`,
|
|
15881
|
+
"cli.bugreport.intro": "Seaworthy Bug Report \u2014 send an anonymized diagnostic to the Seaworthy team.",
|
|
15882
|
+
"cli.bugreport.noLog": (logFile) => `No diagnostic log found at ${logFile}.
|
|
15883
|
+
Re-run the failing command with --debug first, then run bugreport again.`,
|
|
15884
|
+
"cli.bugreport.preview": "The following diagnostic data will be sent (home directory paths have been anonymized):",
|
|
15885
|
+
"cli.bugreport.confirm": "Send this report to the Seaworthy team?",
|
|
15886
|
+
"cli.bugreport.cancelled": "Cancelled \u2014 no data was sent.",
|
|
15887
|
+
"cli.bugreport.sent": (eventId) => `Report sent. Reference ID: ${eventId}
|
|
15888
|
+
Mention this ID when contacting support at ${SITE_URL}/support`,
|
|
15889
|
+
"cli.bugreport.failed": "Could not reach the error reporting service. Check your connection and try again."
|
|
15181
15890
|
};
|
|
15182
15891
|
function getCopy2(key, ...args) {
|
|
15183
15892
|
const value = copy3[key];
|
|
@@ -15202,7 +15911,7 @@ function createCommand(definition, options) {
|
|
|
15202
15911
|
return {
|
|
15203
15912
|
ok: false,
|
|
15204
15913
|
code: "errors.unexpected",
|
|
15205
|
-
detail: err instanceof Error ? err.message : String(err)
|
|
15914
|
+
detail: err instanceof Error ? err.stack ?? err.message : String(err)
|
|
15206
15915
|
};
|
|
15207
15916
|
}
|
|
15208
15917
|
}
|
|
@@ -15264,6 +15973,11 @@ async function executeScan(args, ctx) {
|
|
|
15264
15973
|
}
|
|
15265
15974
|
const showProgress = args.format === "terminal" && !args.sarifStdout;
|
|
15266
15975
|
if (showProgress) {
|
|
15976
|
+
if (ctx.tier === "free") {
|
|
15977
|
+
const skippedCount = registry.getSkippedChecks("free").length;
|
|
15978
|
+
process.stderr.write(getCopy("report.preScanNotice", skippedCount) + "\n");
|
|
15979
|
+
process.stderr.write(getCopy2("cli.licenseNotice", ctx.licensedTo ?? "free tier") + "\n");
|
|
15980
|
+
}
|
|
15267
15981
|
process.stderr.write(getCopy("report.scanProgress.starting", args.targetDir) + "\n");
|
|
15268
15982
|
}
|
|
15269
15983
|
const runner = new ScanRunner();
|
|
@@ -15518,6 +16232,8 @@ async function checkUpdate() {
|
|
|
15518
16232
|
}
|
|
15519
16233
|
|
|
15520
16234
|
// src/cli-action.ts
|
|
16235
|
+
var _require = createRequire2(import.meta.url);
|
|
16236
|
+
var { version: CLI_VERSION } = _require("../package.json");
|
|
15521
16237
|
function getConfigFile2() {
|
|
15522
16238
|
return join15(process.env.SEAWORTHY_CONFIG_DIR ?? join15(homedir3(), ".seaworthy"), "config");
|
|
15523
16239
|
}
|
|
@@ -15594,7 +16310,7 @@ async function runCli(options) {
|
|
|
15594
16310
|
await setupCommand.handler({ action: "run" }, { tier: "free", licensedTo: "free tier" });
|
|
15595
16311
|
}
|
|
15596
16312
|
const provider = new FsCredentialProvider();
|
|
15597
|
-
const license = new LicenseClient({ baseUrl: LICENSE_SERVER_URL });
|
|
16313
|
+
const license = new LicenseClient({ baseUrl: LICENSE_SERVER_URL, userAgent: `seaworthycode/${CLI_VERSION}` });
|
|
15598
16314
|
const authResult = await resolveAuth(provider, license);
|
|
15599
16315
|
if (!authResult.ok) {
|
|
15600
16316
|
const normalized = authResult.code.replace(/^errors\./, "");
|
|
@@ -15647,7 +16363,21 @@ async function runCli(options) {
|
|
|
15647
16363
|
return { code: resolveExitCode("scan.failed"), error: message };
|
|
15648
16364
|
}
|
|
15649
16365
|
if (!result.ok) {
|
|
15650
|
-
|
|
16366
|
+
let message = resolveErrorMessage(result.code);
|
|
16367
|
+
if (result.code === "errors.unexpected") {
|
|
16368
|
+
const logFile = join15(LOG_DIR, "seaworthy.log");
|
|
16369
|
+
message += "\n" + getCopy2("cli.error.debugHint", logFile);
|
|
16370
|
+
if (options.debug && result.detail) {
|
|
16371
|
+
try {
|
|
16372
|
+
await mkdir(LOG_DIR, { recursive: true });
|
|
16373
|
+
await appendFile(logFile, `[${(/* @__PURE__ */ new Date()).toISOString()}] unexpected error
|
|
16374
|
+
${result.detail}
|
|
16375
|
+
|
|
16376
|
+
`);
|
|
16377
|
+
} catch {
|
|
16378
|
+
}
|
|
16379
|
+
}
|
|
16380
|
+
}
|
|
15651
16381
|
return { code: resolveExitCode(result.code), error: message };
|
|
15652
16382
|
}
|
|
15653
16383
|
const remaining = Math.max(0, 3e3 - (Date.now() - updateStart));
|
|
@@ -15712,13 +16442,89 @@ var configCommand = createCommand(
|
|
|
15712
16442
|
{ requireLicense: false }
|
|
15713
16443
|
);
|
|
15714
16444
|
|
|
16445
|
+
// src/commands/bugreport.ts
|
|
16446
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
16447
|
+
import { randomUUID } from "crypto";
|
|
16448
|
+
import { platform, arch, homedir as homedir4 } from "os";
|
|
16449
|
+
import { join as join16 } from "path";
|
|
16450
|
+
import { createRequire as createRequire3 } from "module";
|
|
16451
|
+
import { intro as intro2, outro as outro2, confirm, isCancel as isCancel2 } from "@clack/prompts";
|
|
16452
|
+
var _require2 = createRequire3(import.meta.url);
|
|
16453
|
+
var { version: CLI_VERSION2 } = _require2("../../package.json");
|
|
16454
|
+
var LOG_FILE = join16(LOG_DIR, "seaworthy.log");
|
|
16455
|
+
var SENTRY_PROJECT_ID = "4511394796666960";
|
|
16456
|
+
var SENTRY_INGEST = "https://o4508597942026240.ingest.de.sentry.io";
|
|
16457
|
+
var SENTRY_KEY = "3c8dc912769032c73a306cb571805658";
|
|
16458
|
+
function sanitize(text3) {
|
|
16459
|
+
return text3.split(homedir4()).join("<home>");
|
|
16460
|
+
}
|
|
16461
|
+
async function sendReport(log) {
|
|
16462
|
+
const eventId = randomUUID().replace(/-/g, "");
|
|
16463
|
+
const payload = {
|
|
16464
|
+
event_id: eventId,
|
|
16465
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16466
|
+
platform: "node",
|
|
16467
|
+
level: "error",
|
|
16468
|
+
environment: "cli",
|
|
16469
|
+
release: CLI_VERSION2,
|
|
16470
|
+
message: log.split("\n")[0] ?? "Unexpected error",
|
|
16471
|
+
extra: { log },
|
|
16472
|
+
contexts: {
|
|
16473
|
+
runtime: { name: "node", version: process.version },
|
|
16474
|
+
os: { name: platform(), arch: arch() }
|
|
16475
|
+
},
|
|
16476
|
+
tags: { source: "cli-bugreport" }
|
|
16477
|
+
};
|
|
16478
|
+
try {
|
|
16479
|
+
const res = await fetch(`${SENTRY_INGEST}/api/${SENTRY_PROJECT_ID}/store/`, {
|
|
16480
|
+
method: "POST",
|
|
16481
|
+
headers: {
|
|
16482
|
+
"Content-Type": "application/json",
|
|
16483
|
+
"X-Sentry-Auth": `Sentry sentry_version=7, sentry_key=${SENTRY_KEY}, sentry_client=seaworthycode/${CLI_VERSION2}`
|
|
16484
|
+
},
|
|
16485
|
+
body: JSON.stringify(payload),
|
|
16486
|
+
signal: AbortSignal.timeout(1e4)
|
|
16487
|
+
});
|
|
16488
|
+
return res.ok ? eventId : null;
|
|
16489
|
+
} catch {
|
|
16490
|
+
return null;
|
|
16491
|
+
}
|
|
16492
|
+
}
|
|
16493
|
+
async function executeBugReport() {
|
|
16494
|
+
intro2(getCopy2("cli.bugreport.intro"));
|
|
16495
|
+
let log;
|
|
16496
|
+
try {
|
|
16497
|
+
const raw = await readFile2(LOG_FILE, "utf-8");
|
|
16498
|
+
if (!raw.trim()) throw new Error("empty");
|
|
16499
|
+
log = sanitize(raw.length > 8192 ? raw.slice(-8192) : raw);
|
|
16500
|
+
} catch {
|
|
16501
|
+
return { ok: true, output: getCopy2("cli.bugreport.noLog", LOG_FILE) };
|
|
16502
|
+
}
|
|
16503
|
+
process.stderr.write(getCopy2("cli.bugreport.preview") + "\n---\n" + log + "---\n\n");
|
|
16504
|
+
const confirmed = await confirm({ message: getCopy2("cli.bugreport.confirm") });
|
|
16505
|
+
if (isCancel2(confirmed) || !confirmed) {
|
|
16506
|
+
outro2(getCopy2("cli.bugreport.cancelled"));
|
|
16507
|
+
return { ok: true };
|
|
16508
|
+
}
|
|
16509
|
+
const eventId = await sendReport(log);
|
|
16510
|
+
outro2(eventId ? getCopy2("cli.bugreport.sent", eventId) : getCopy2("cli.bugreport.failed"));
|
|
16511
|
+
return { ok: true };
|
|
16512
|
+
}
|
|
16513
|
+
var bugReportCommand = createCommand(
|
|
16514
|
+
{
|
|
16515
|
+
name: "bugreport",
|
|
16516
|
+
description: "Send an anonymized bug report to the Seaworthy team",
|
|
16517
|
+
handler: async () => executeBugReport()
|
|
16518
|
+
},
|
|
16519
|
+
{ requireLicense: false }
|
|
16520
|
+
);
|
|
16521
|
+
|
|
15715
16522
|
// src/index.ts
|
|
15716
|
-
loadEnv({ path: resolve4(process.cwd(), ".env") });
|
|
15717
16523
|
var repoRootEnv = resolve4(dirname7(fileURLToPath3(import.meta.url)), "../../..", ".env");
|
|
15718
16524
|
if (existsSync6(repoRootEnv)) {
|
|
15719
16525
|
loadEnv({ path: repoRootEnv, override: false });
|
|
15720
16526
|
}
|
|
15721
|
-
var require5 =
|
|
16527
|
+
var require5 = createRequire4(import.meta.url);
|
|
15722
16528
|
var { version } = require5("../package.json");
|
|
15723
16529
|
var cli = cac("seaworthy");
|
|
15724
16530
|
cli.version(version);
|
|
@@ -15758,6 +16564,16 @@ cli.command("setup", "Interactive setup for license key and LLM provider").actio
|
|
|
15758
16564
|
process.exit(2);
|
|
15759
16565
|
}
|
|
15760
16566
|
});
|
|
16567
|
+
cli.command("bugreport", "Send an anonymized bug report to the Seaworthy team").action(async () => {
|
|
16568
|
+
const result = await bugReportCommand.handler({ action: "run" }, { tier: "free", licensedTo: "free tier" });
|
|
16569
|
+
if (!result.ok) {
|
|
16570
|
+
console.error(resolveErrorMessage(result.code));
|
|
16571
|
+
process.exit(2);
|
|
16572
|
+
}
|
|
16573
|
+
if (result.output) {
|
|
16574
|
+
console.log(result.output);
|
|
16575
|
+
}
|
|
16576
|
+
});
|
|
15761
16577
|
cli.help(() => {
|
|
15762
16578
|
return [
|
|
15763
16579
|
{
|
|
@@ -15770,7 +16586,8 @@ cli.help(() => {
|
|
|
15770
16586
|
},
|
|
15771
16587
|
{
|
|
15772
16588
|
title: "Commands",
|
|
15773
|
-
body: ` npx seaworthycode setup
|
|
16589
|
+
body: ` npx seaworthycode setup Interactive setup for license key and LLM provider
|
|
16590
|
+
npx seaworthycode bugreport Send an anonymized bug report to the Seaworthy team`
|
|
15774
16591
|
},
|
|
15775
16592
|
{
|
|
15776
16593
|
title: "Options",
|