seaworthycode 1.1.1 → 1.1.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 +269 -59
- package/dist/index.js.map +1 -1
- package/package.json +16 -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
|
@@ -9,8 +9,10 @@ import { cac } from "cac";
|
|
|
9
9
|
import { createRequire as createRequire2 } from "module";
|
|
10
10
|
|
|
11
11
|
// src/cli-action.ts
|
|
12
|
-
import { stat } from "fs/promises";
|
|
12
|
+
import { stat, access as access2 } from "fs/promises";
|
|
13
13
|
import { resolve as resolve3 } from "path";
|
|
14
|
+
import { homedir as homedir3 } from "os";
|
|
15
|
+
import { join as join15 } from "path";
|
|
14
16
|
|
|
15
17
|
// ../core/dist/index.js
|
|
16
18
|
import { readFileSync } from "fs";
|
|
@@ -296,8 +298,8 @@ async function peekFileHead(filePath) {
|
|
|
296
298
|
break;
|
|
297
299
|
}
|
|
298
300
|
}
|
|
299
|
-
const
|
|
300
|
-
const isGenerated = GENERATED_MARKERS.some((marker) =>
|
|
301
|
+
const text22 = buf.toString("utf-8", 0, bytesRead).toLowerCase();
|
|
302
|
+
const isGenerated = GENERATED_MARKERS.some((marker) => text22.includes(marker));
|
|
301
303
|
return { isBinary, isGenerated };
|
|
302
304
|
} finally {
|
|
303
305
|
await handle.close();
|
|
@@ -513,7 +515,9 @@ function sortFindings(findings) {
|
|
|
513
515
|
const fileA = a.file ?? "";
|
|
514
516
|
const fileB = b.file ?? "";
|
|
515
517
|
if (fileA !== fileB) return fileA.localeCompare(fileB);
|
|
516
|
-
|
|
518
|
+
const lineDiff = (a.line ?? 0) - (b.line ?? 0);
|
|
519
|
+
if (lineDiff !== 0) return lineDiff;
|
|
520
|
+
return a.checkId.localeCompare(b.checkId);
|
|
517
521
|
});
|
|
518
522
|
}
|
|
519
523
|
function dedupeDegradedFindings(findings) {
|
|
@@ -769,9 +773,13 @@ var copy = {
|
|
|
769
773
|
"report.scannedFiles": (count) => `Scanned ${count} files.`,
|
|
770
774
|
"report.summary": "Summary",
|
|
771
775
|
"report.bySeverity": "By severity",
|
|
772
|
-
"report.tierWatermark": (
|
|
773
|
-
"report.
|
|
774
|
-
"report.
|
|
776
|
+
"report.tierWatermark": (licensedTo) => `\u2693 Pro \xB7 ${licensedTo}`,
|
|
777
|
+
"report.scanProgress.starting": (targetDir) => `\u2693 Scanning ${targetDir}...`,
|
|
778
|
+
"report.scanProgress.category": (label, count) => count > 0 ? ` \u2693 ${label} \u2014 ${count} issue(s)` : ` \u2693 ${label} \u2713`,
|
|
779
|
+
"report.cta.clean": `\u2713 All clear \u2014 your project is shipshape!`,
|
|
780
|
+
"report.cta.hasFindings": `Fix these before you ship. ${SITE_URL}/docs`,
|
|
781
|
+
"report.cta.critical": `\u26A0 Critical issues found \u2014 patch before you deploy. ${SITE_URL}/docs`,
|
|
782
|
+
"report.proUpsellNamed": (names, count) => `\u2693 Pro unlocks ${count} more check${count === 1 ? "" : "s"} (${names}). ${SITE_URL}`,
|
|
775
783
|
"report.llmPrivacyWarning": "Pro checks will send code context to your LLM provider via your API key. No data is sent to Seaworthy servers.",
|
|
776
784
|
"report.confidenceHigh": "high confidence",
|
|
777
785
|
"report.confidenceMedium": "medium confidence",
|
|
@@ -1540,9 +1548,6 @@ var ScanRunner = class {
|
|
|
1540
1548
|
const { files, meta } = await crawl({ targetDir: options.targetDir, maxFileSize: options.maxFileSize, debug: options.debug });
|
|
1541
1549
|
const checks = registry.getForTier(options.tier);
|
|
1542
1550
|
const skippedChecks = registry.getSkippedChecks(options.tier);
|
|
1543
|
-
if (skippedChecks.length > 0) {
|
|
1544
|
-
console.error(getCopy("report.preScanNotice", skippedChecks.length));
|
|
1545
|
-
}
|
|
1546
1551
|
if (options.llm) {
|
|
1547
1552
|
console.error(getCopy("report.llmPrivacyWarning"));
|
|
1548
1553
|
}
|
|
@@ -1556,20 +1561,36 @@ var ScanRunner = class {
|
|
|
1556
1561
|
astCache: /* @__PURE__ */ new Map()
|
|
1557
1562
|
};
|
|
1558
1563
|
const results = [];
|
|
1559
|
-
const
|
|
1564
|
+
const checkResults = /* @__PURE__ */ new Map();
|
|
1560
1565
|
await runWithConcurrency(
|
|
1561
1566
|
checks,
|
|
1562
1567
|
async (check2) => {
|
|
1563
1568
|
try {
|
|
1564
1569
|
const findings2 = await check2.run(ctx);
|
|
1565
|
-
|
|
1570
|
+
checkResults.set(check2.id, findings2);
|
|
1566
1571
|
} catch (err) {
|
|
1567
1572
|
console.error(`Check "${check2.id}" failed: ${String(err)}`);
|
|
1568
1573
|
}
|
|
1569
1574
|
},
|
|
1570
1575
|
concurrency
|
|
1571
1576
|
);
|
|
1572
|
-
|
|
1577
|
+
for (const check2 of checks) {
|
|
1578
|
+
results.push(...checkResults.get(check2.id) ?? []);
|
|
1579
|
+
}
|
|
1580
|
+
if (options.onCategoryComplete) {
|
|
1581
|
+
const findingsPerCategory = /* @__PURE__ */ new Map();
|
|
1582
|
+
for (const check2 of checks) {
|
|
1583
|
+
const count = (checkResults.get(check2.id) ?? []).length;
|
|
1584
|
+
findingsPerCategory.set(check2.category, (findingsPerCategory.get(check2.category) ?? 0) + count);
|
|
1585
|
+
}
|
|
1586
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1587
|
+
for (const check2 of checks) {
|
|
1588
|
+
if (!seen.has(check2.category)) {
|
|
1589
|
+
seen.add(check2.category);
|
|
1590
|
+
options.onCategoryComplete(check2.category, findingsPerCategory.get(check2.category) ?? 0);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1573
1594
|
if (ctx.astCache) {
|
|
1574
1595
|
for (const promise of ctx.astCache.values()) {
|
|
1575
1596
|
try {
|
|
@@ -1601,7 +1622,7 @@ var ScanRunner = class {
|
|
|
1601
1622
|
return {
|
|
1602
1623
|
findings,
|
|
1603
1624
|
scannedFileCount: files.length,
|
|
1604
|
-
|
|
1625
|
+
skippedChecks
|
|
1605
1626
|
};
|
|
1606
1627
|
}
|
|
1607
1628
|
};
|
|
@@ -2351,15 +2372,31 @@ var text = {
|
|
|
2351
2372
|
success: (s) => source_default.hex(tokens.success)(s)
|
|
2352
2373
|
};
|
|
2353
2374
|
var SEVERITY_ORDER2 = ["critical", "high", "medium", "low", "info"];
|
|
2375
|
+
function checkNameOf(checkId) {
|
|
2376
|
+
const short = checkId.replace(/^[^.]+\./, "");
|
|
2377
|
+
try {
|
|
2378
|
+
return getCopy(`checks.${short}.name`);
|
|
2379
|
+
} catch {
|
|
2380
|
+
return short;
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2354
2383
|
var TerminalReporter = class {
|
|
2355
2384
|
generate(input) {
|
|
2356
|
-
const { findings: rawFindings, targetDir, tier, licensedTo, scannedFileCount } = input;
|
|
2385
|
+
const { findings: rawFindings, targetDir, tier, licensedTo, scannedFileCount, skippedChecks = [], showIds = false } = input;
|
|
2357
2386
|
const findings = dedupeDegradedFindings(rawFindings);
|
|
2358
2387
|
if (findings.length === 0) {
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2388
|
+
const lines2 = [];
|
|
2389
|
+
lines2.push(text.success(getCopy("report.cta.clean")));
|
|
2390
|
+
lines2.push(text.meta(getCopy("report.scannedFiles", scannedFileCount)));
|
|
2391
|
+
if (tier === "pro") {
|
|
2392
|
+
lines2.push("");
|
|
2393
|
+
lines2.push(text.meta(getCopy("report.tierWatermark", licensedTo)));
|
|
2394
|
+
}
|
|
2395
|
+
if (skippedChecks.length > 0) {
|
|
2396
|
+
lines2.push("");
|
|
2397
|
+
lines2.push(text.link(buildProUpsell(skippedChecks)));
|
|
2398
|
+
}
|
|
2399
|
+
return lines2.join("\n");
|
|
2363
2400
|
}
|
|
2364
2401
|
const lines = [];
|
|
2365
2402
|
lines.push(text.heading(getCopy("report.header", targetDir, findings.length)));
|
|
@@ -2372,7 +2409,9 @@ ${text.link(getCopy("report.postScanCta", SITE_URL))}`;
|
|
|
2372
2409
|
const rawAnalysisType = f.properties?.analysisType;
|
|
2373
2410
|
const analysisTypeValue = rawAnalysisType === "pattern" || rawAnalysisType === "semantic" ? rawAnalysisType : void 0;
|
|
2374
2411
|
const analysisType = analysisTypeValue ? text.meta(` [${getCopy(`reporting.analysisType.${analysisTypeValue}`)}]`) : "";
|
|
2375
|
-
|
|
2412
|
+
const name = checkNameOf(f.checkId);
|
|
2413
|
+
const idSuffix = showIds ? text.meta(` \xB7 ${f.checkId}`) : "";
|
|
2414
|
+
lines.push(`${severityLabel}${baselineLabel} ${name}${idSuffix}${analysisType} \u2014 ${f.message}${loc}${confidenceLabel}`);
|
|
2376
2415
|
if (f.remediation) {
|
|
2377
2416
|
lines.push(` ${text.meta(`\u2192 ${f.remediation}`)}`);
|
|
2378
2417
|
}
|
|
@@ -2390,11 +2429,23 @@ ${text.link(getCopy("report.postScanCta", SITE_URL))}`;
|
|
|
2390
2429
|
}
|
|
2391
2430
|
}
|
|
2392
2431
|
lines.push("");
|
|
2393
|
-
|
|
2394
|
-
lines.push(text.link(getCopy("report.
|
|
2432
|
+
const hasCriticalOrHigh = findings.some((f) => f.severity === "critical" || f.severity === "high");
|
|
2433
|
+
lines.push(text.link(hasCriticalOrHigh ? getCopy("report.cta.critical") : getCopy("report.cta.hasFindings")));
|
|
2434
|
+
if (tier === "pro") {
|
|
2435
|
+
lines.push(text.meta(getCopy("report.tierWatermark", licensedTo)));
|
|
2436
|
+
}
|
|
2437
|
+
if (skippedChecks.length > 0) {
|
|
2438
|
+
lines.push(text.link(buildProUpsell(skippedChecks)));
|
|
2439
|
+
}
|
|
2395
2440
|
return lines.join("\n");
|
|
2396
2441
|
}
|
|
2397
2442
|
};
|
|
2443
|
+
function buildProUpsell(skippedChecks) {
|
|
2444
|
+
const count = skippedChecks.length;
|
|
2445
|
+
const names = skippedChecks.slice(0, 2).map((c) => c.name.toLowerCase()).join(", ");
|
|
2446
|
+
const suffix = count > 2 ? ", \u2026" : "";
|
|
2447
|
+
return getCopy("report.proUpsellNamed", names + suffix, count);
|
|
2448
|
+
}
|
|
2398
2449
|
function textForSeverity(severity) {
|
|
2399
2450
|
switch (severity) {
|
|
2400
2451
|
case "critical":
|
|
@@ -5640,8 +5691,8 @@ function downgradeSeverity(severity) {
|
|
|
5640
5691
|
if (idx < 0 || idx >= SEVERITY_ORDER3.length - 1) return severity;
|
|
5641
5692
|
return SEVERITY_ORDER3[idx + 1];
|
|
5642
5693
|
}
|
|
5643
|
-
function estimateTokens(
|
|
5644
|
-
return Math.ceil(
|
|
5694
|
+
function estimateTokens(text22) {
|
|
5695
|
+
return Math.ceil(text22.length / 4);
|
|
5645
5696
|
}
|
|
5646
5697
|
async function filterFilesByBudget(files, budget) {
|
|
5647
5698
|
if (budget === void 0 || budget === Infinity) {
|
|
@@ -7087,8 +7138,8 @@ function unquoteString(value) {
|
|
|
7087
7138
|
}
|
|
7088
7139
|
return trimmed;
|
|
7089
7140
|
}
|
|
7090
|
-
function isDefaultCredential(
|
|
7091
|
-
const normalized = unquoteString(
|
|
7141
|
+
function isDefaultCredential(password2) {
|
|
7142
|
+
const normalized = unquoteString(password2).toLowerCase();
|
|
7092
7143
|
const defaults = /* @__PURE__ */ new Set([
|
|
7093
7144
|
"admin",
|
|
7094
7145
|
"password",
|
|
@@ -7190,8 +7241,8 @@ async function scanSqlDefaultCredentials(ctx) {
|
|
|
7190
7241
|
let roleMatch = createRoleRegex.exec(content);
|
|
7191
7242
|
while (roleMatch) {
|
|
7192
7243
|
const roleName = roleMatch[1].replace(/["`[\]]/g, "");
|
|
7193
|
-
const
|
|
7194
|
-
if (!looksLikeHash(
|
|
7244
|
+
const password2 = roleMatch[2];
|
|
7245
|
+
if (!looksLikeHash(password2) && isDefaultCredential(password2)) {
|
|
7195
7246
|
const beforeMatch = content.slice(0, roleMatch.index);
|
|
7196
7247
|
const lineNum = beforeMatch.split("\n").length;
|
|
7197
7248
|
findings.push({
|
|
@@ -7757,8 +7808,8 @@ registry.register(
|
|
|
7757
7808
|
})
|
|
7758
7809
|
);
|
|
7759
7810
|
var CHECK_ID11 = "security.os-command-injection";
|
|
7760
|
-
function isStaticShellLikeLiteral(
|
|
7761
|
-
const t =
|
|
7811
|
+
function isStaticShellLikeLiteral(text22) {
|
|
7812
|
+
const t = text22.trim();
|
|
7762
7813
|
if (t.length < 2) return false;
|
|
7763
7814
|
if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) return true;
|
|
7764
7815
|
if (t.startsWith("`") && t.endsWith("`") && !/(?<!\\)\${/.test(t)) return true;
|
|
@@ -9176,10 +9227,10 @@ var CSS_COMMENT = /\/\*([\s\S]*?)\*\//g;
|
|
|
9176
9227
|
var JSON_SCRIPT = /<script\b[^>]*type\s*=\s*["']application\/json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
9177
9228
|
function scanPattern(content, filePath, checkPrefix, pattern, messageText, findings) {
|
|
9178
9229
|
for (const match of content.matchAll(pattern)) {
|
|
9179
|
-
const
|
|
9180
|
-
if (!SECRET_MARKER.test(
|
|
9181
|
-
const valueMatch =
|
|
9182
|
-
const value = valueMatch?.[1] ??
|
|
9230
|
+
const text22 = match[1] ?? match[0] ?? "";
|
|
9231
|
+
if (!SECRET_MARKER.test(text22)) continue;
|
|
9232
|
+
const valueMatch = text22.match(SECRET_VALUE);
|
|
9233
|
+
const value = valueMatch?.[1] ?? text22;
|
|
9183
9234
|
const { line, column } = lineAndColumn5(content, match.index ?? 0);
|
|
9184
9235
|
findings.push({
|
|
9185
9236
|
id: `${CHECK_ID23}:${filePath}:${line}:${column}:${checkPrefix}`,
|
|
@@ -9830,11 +9881,11 @@ function pushFinding6(findings, filePath, content, index, matchText) {
|
|
|
9830
9881
|
}
|
|
9831
9882
|
function scanCss(findings, filePath, scanContent, originalContent, offset = 0) {
|
|
9832
9883
|
for (const match of scanContent.matchAll(BRACE_RULE_PATTERN)) {
|
|
9833
|
-
const
|
|
9834
|
-
if (!ATTR_SELECTOR_PATTERN.test(
|
|
9835
|
-
if (!EXTERNAL_HTTP_URL_PATTERN.test(
|
|
9836
|
-
const leadingWhitespace =
|
|
9837
|
-
pushFinding6(findings, filePath, originalContent, (match.index ?? 0) + offset + leadingWhitespace.length,
|
|
9884
|
+
const text22 = match[0] ?? "";
|
|
9885
|
+
if (!ATTR_SELECTOR_PATTERN.test(text22)) continue;
|
|
9886
|
+
if (!EXTERNAL_HTTP_URL_PATTERN.test(text22)) continue;
|
|
9887
|
+
const leadingWhitespace = text22.match(/^\s*/)?.[0] ?? "";
|
|
9888
|
+
pushFinding6(findings, filePath, originalContent, (match.index ?? 0) + offset + leadingWhitespace.length, text22);
|
|
9838
9889
|
}
|
|
9839
9890
|
}
|
|
9840
9891
|
registry.register({
|
|
@@ -10185,7 +10236,7 @@ registry.register({
|
|
|
10185
10236
|
});
|
|
10186
10237
|
var CHECK_ID40 = "security.weak-email-validation";
|
|
10187
10238
|
var REMEDIATION34 = getCopy("remediation.security.weak-email-validation");
|
|
10188
|
-
var WEAK_EMAIL_REGEX_LINE = /\/\^[
|
|
10239
|
+
var WEAK_EMAIL_REGEX_LINE = /\/\^[^/]*@[^/]*\\\.[^/]{0,4}\$\/[gimsuy]*/g;
|
|
10189
10240
|
var LOOSE_REGEX_CLASS = /\[\^?\\?s@\]\+@\[\^?\\?s@\]\+\\\.\[\^?\\?s@\]\+/g;
|
|
10190
10241
|
var EMAIL_CONTEXT = /email|e-?mail|validateEmail|isEmail|checkEmail|verifyEmail|deliverable/is;
|
|
10191
10242
|
function lineAndColumn15(content, index) {
|
|
@@ -10241,7 +10292,7 @@ registry.register({
|
|
|
10241
10292
|
var CHECK_ID41 = "security.idempotency-key-leak";
|
|
10242
10293
|
var REMEDIATION35 = getCopy("remediation.security.idempotency-key-leak");
|
|
10243
10294
|
var IDEMPOTENCY_CONTEXT = /idempotency[_-]?key|idempotency[_-]?token/i;
|
|
10244
|
-
var TEMPLATE_LITERAL_INTERPOLATION =
|
|
10295
|
+
var TEMPLATE_LITERAL_INTERPOLATION = new RegExp("`[^`]*\\$\\{[^}]+\\}[^`]*`", "g");
|
|
10245
10296
|
var STRUCTURED_ID_PATTERN = /(?:lic(?:ense|ence)|sub(?:scription)?|order|cust(?:omer)?|user|tx|pay(?:ment)?|inv(?:oice)?)[-/]/i;
|
|
10246
10297
|
function lineAndColumn16(content, index) {
|
|
10247
10298
|
const prefix = content.slice(0, index);
|
|
@@ -10491,11 +10542,11 @@ function applyTaintToBashReadTarget(command, scopeRoot, s, cleanseFromId) {
|
|
|
10491
10542
|
for (let i = 0; i < command.namedChildCount; i++) {
|
|
10492
10543
|
const child = command.namedChild(i);
|
|
10493
10544
|
if (child.type !== "word") continue;
|
|
10494
|
-
const
|
|
10495
|
-
if (
|
|
10545
|
+
const text22 = child.text;
|
|
10546
|
+
if (text22 === "read" || text22.startsWith("-")) continue;
|
|
10496
10547
|
markTaint(s, child.id);
|
|
10497
10548
|
if (cleanseFromId !== void 0) mergeCleanse(s, cleanseFromId, child.id);
|
|
10498
|
-
taintVarUses(
|
|
10549
|
+
taintVarUses(text22, scopeRoot, s);
|
|
10499
10550
|
}
|
|
10500
10551
|
}
|
|
10501
10552
|
function collectEntries(table) {
|
|
@@ -15092,7 +15143,23 @@ var copy3 = {
|
|
|
15092
15143
|
"cli.help.usage": "Usage: seaworthy [path] [options]",
|
|
15093
15144
|
"cli.help.options": "Options",
|
|
15094
15145
|
"cli.help.learnMore": `Documentation: ${SITE_URL}/docs`,
|
|
15095
|
-
"cli.
|
|
15146
|
+
"cli.setup.intro": "Welcome to Seaworthy! Let's get you set up.",
|
|
15147
|
+
"cli.setup.licensePrompt": "License key (press Enter to skip and scan free)",
|
|
15148
|
+
"cli.setup.licenseHint": "Buy a key at seaworthycode.com \xB7 Enter skips to free tier",
|
|
15149
|
+
"cli.setup.providerPrompt": "LLM provider for Pro checks",
|
|
15150
|
+
"cli.setup.providerSkip": "Skip \u2014 no LLM checks",
|
|
15151
|
+
"cli.setup.apiKeyPrompt": "API key for",
|
|
15152
|
+
"cli.setup.modelPrompt": "Model (press Enter for default)",
|
|
15153
|
+
"cli.setup.baseUrlPrompt": "Custom API base URL (press Enter to use default)",
|
|
15154
|
+
"cli.setup.done": "Config saved. Run `seaworthy .` to scan your project.",
|
|
15155
|
+
"cli.setup.skipped": "Setup skipped. Running free-tier scan.",
|
|
15156
|
+
"cli.firstRun": "First time? Let's get you set up.",
|
|
15157
|
+
"cli.category.security": "Security",
|
|
15158
|
+
"cli.category.resilience": "Resilience",
|
|
15159
|
+
"cli.category.ops": "Ops Basics",
|
|
15160
|
+
"cli.category.dependencies": "Dependencies",
|
|
15161
|
+
"cli.category.configuration": "Configuration",
|
|
15162
|
+
"cli.category.data-exposure": "Data Exposure",
|
|
15096
15163
|
"cli.reportWritten": (path) => `Report written to ${path}`,
|
|
15097
15164
|
"cli.error.notADirectory": (path) => `Path is not a directory: ${path}`,
|
|
15098
15165
|
"cli.error.notFound": (path) => `Path not found: ${path}`,
|
|
@@ -15104,6 +15171,7 @@ var copy3 = {
|
|
|
15104
15171
|
"cli.sarif.stdout_conflict": "Flags --sarif-stdout and --json cannot be used together. Use --output to send JSON to a file.",
|
|
15105
15172
|
"cli.error.llmRequired": "LLM API key required for Pro checks. Set it with seaworthy config or the SEAWORTHY_LLM_API_KEY environment variable.",
|
|
15106
15173
|
"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.",
|
|
15174
|
+
"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",
|
|
15107
15175
|
"cli.error.config.missingKey": "Missing config key. Usage: seaworthy config <set|get> <key>",
|
|
15108
15176
|
"cli.error.config.missingValue": "Missing config value. Usage: seaworthy config set <key> <value>",
|
|
15109
15177
|
"cli.error.config.unknownKey": "Unknown config key. Valid keys: licenseKey, llmProvider, llmApiKey, llmModel, llmApiUrl",
|
|
@@ -15154,6 +15222,14 @@ function resolveScanTimeoutMs(tier) {
|
|
|
15154
15222
|
}
|
|
15155
15223
|
return tier === "free" ? FREE_SCAN_TIMEOUT_MS : PRO_SCAN_TIMEOUT_MS;
|
|
15156
15224
|
}
|
|
15225
|
+
var CATEGORY_LABELS = {
|
|
15226
|
+
security: getCopy2("cli.category.security"),
|
|
15227
|
+
resilience: getCopy2("cli.category.resilience"),
|
|
15228
|
+
ops: getCopy2("cli.category.ops"),
|
|
15229
|
+
dependencies: getCopy2("cli.category.dependencies"),
|
|
15230
|
+
configuration: getCopy2("cli.category.configuration"),
|
|
15231
|
+
"data-exposure": getCopy2("cli.category.data-exposure")
|
|
15232
|
+
};
|
|
15157
15233
|
function withTimeout(promise, ms, message) {
|
|
15158
15234
|
return new Promise((resolve5, reject) => {
|
|
15159
15235
|
const timer = setTimeout(() => reject(new Error(message)), ms);
|
|
@@ -15186,6 +15262,10 @@ async function executeScan(args, ctx) {
|
|
|
15186
15262
|
if (args.llmApiKey && args.llmProvider && !process.env.SEAWORTHY_SKIP_LLM_CHECK) {
|
|
15187
15263
|
llm = createProvider({ provider: args.llmProvider, apiKey: args.llmApiKey, model: args.llmModel, baseUrl: args.llmApiUrl });
|
|
15188
15264
|
}
|
|
15265
|
+
const showProgress = args.format === "terminal" && !args.sarifStdout;
|
|
15266
|
+
if (showProgress) {
|
|
15267
|
+
process.stderr.write(getCopy("report.scanProgress.starting", args.targetDir) + "\n");
|
|
15268
|
+
}
|
|
15189
15269
|
const runner = new ScanRunner();
|
|
15190
15270
|
const scanTimeoutMs = resolveScanTimeoutMs(ctx.tier);
|
|
15191
15271
|
const result = await withTimeout(
|
|
@@ -15196,7 +15276,11 @@ async function executeScan(args, ctx) {
|
|
|
15196
15276
|
maxFileSize: args.maxFileSize,
|
|
15197
15277
|
debug: args.debug,
|
|
15198
15278
|
baseline: args.baseline,
|
|
15199
|
-
minConfidence: args.minConfidence
|
|
15279
|
+
minConfidence: args.minConfidence,
|
|
15280
|
+
onCategoryComplete: showProgress ? (category, count) => {
|
|
15281
|
+
const label = CATEGORY_LABELS[category] ?? category;
|
|
15282
|
+
process.stderr.write(getCopy("report.scanProgress.category", label, count) + "\n");
|
|
15283
|
+
} : void 0
|
|
15200
15284
|
}),
|
|
15201
15285
|
scanTimeoutMs,
|
|
15202
15286
|
getCopy("errors.scan.timed_out", Math.round(scanTimeoutMs / 1e3))
|
|
@@ -15206,7 +15290,9 @@ async function executeScan(args, ctx) {
|
|
|
15206
15290
|
targetDir: args.targetDir,
|
|
15207
15291
|
tier: ctx.tier,
|
|
15208
15292
|
licensedTo: ctx.licensedTo ?? "free tier",
|
|
15209
|
-
scannedFileCount: result.scannedFileCount
|
|
15293
|
+
scannedFileCount: result.scannedFileCount,
|
|
15294
|
+
skippedChecks: result.skippedChecks,
|
|
15295
|
+
showIds: args.showIds ?? args.debug
|
|
15210
15296
|
};
|
|
15211
15297
|
let primaryOutput;
|
|
15212
15298
|
switch (args.format) {
|
|
@@ -15257,6 +15343,9 @@ var scanCommand = createCommand({
|
|
|
15257
15343
|
handler: executeScan
|
|
15258
15344
|
});
|
|
15259
15345
|
|
|
15346
|
+
// src/commands/setup.ts
|
|
15347
|
+
import { intro, outro, text as text2, password, select, isCancel } from "@clack/prompts";
|
|
15348
|
+
|
|
15260
15349
|
// src/config/store.ts
|
|
15261
15350
|
import { promises as fs9 } from "fs";
|
|
15262
15351
|
import { homedir as homedir2 } from "os";
|
|
@@ -15283,6 +15372,91 @@ async function writeConfig(config) {
|
|
|
15283
15372
|
await fs9.chmod(getConfigFile(), 384);
|
|
15284
15373
|
}
|
|
15285
15374
|
|
|
15375
|
+
// src/commands/setup.ts
|
|
15376
|
+
var PROVIDERS_WITH_CUSTOM_URL = /* @__PURE__ */ new Set(["ollama", "openrouter"]);
|
|
15377
|
+
async function executeSetup() {
|
|
15378
|
+
intro(getCopy2("cli.setup.intro"));
|
|
15379
|
+
const config = await readConfig();
|
|
15380
|
+
const licenseKeyResult = await text2({
|
|
15381
|
+
message: getCopy2("cli.setup.licensePrompt"),
|
|
15382
|
+
placeholder: "SW-XXXXXX",
|
|
15383
|
+
defaultValue: config.licenseKey ?? "",
|
|
15384
|
+
validate: () => void 0
|
|
15385
|
+
});
|
|
15386
|
+
if (isCancel(licenseKeyResult)) {
|
|
15387
|
+
outro(getCopy2("cli.setup.skipped"));
|
|
15388
|
+
return { ok: true };
|
|
15389
|
+
}
|
|
15390
|
+
const licenseKey = String(licenseKeyResult).trim() || void 0;
|
|
15391
|
+
const providers = listPresetProviders();
|
|
15392
|
+
const providerOptions = [
|
|
15393
|
+
{ value: "", label: getCopy2("cli.setup.providerSkip") },
|
|
15394
|
+
...providers.map((p) => ({ value: p, label: p }))
|
|
15395
|
+
];
|
|
15396
|
+
const providerResult = await select({
|
|
15397
|
+
message: getCopy2("cli.setup.providerPrompt"),
|
|
15398
|
+
options: providerOptions,
|
|
15399
|
+
initialValue: config.llmProvider ?? ""
|
|
15400
|
+
});
|
|
15401
|
+
if (isCancel(providerResult)) {
|
|
15402
|
+
outro(getCopy2("cli.setup.skipped"));
|
|
15403
|
+
return { ok: true };
|
|
15404
|
+
}
|
|
15405
|
+
const llmProvider = String(providerResult) || void 0;
|
|
15406
|
+
let llmApiKey = config.llmApiKey;
|
|
15407
|
+
let llmModel = config.llmModel;
|
|
15408
|
+
let llmApiUrl = config.llmApiUrl;
|
|
15409
|
+
if (llmProvider) {
|
|
15410
|
+
const apiKeyResult = await password({
|
|
15411
|
+
message: `${getCopy2("cli.setup.apiKeyPrompt")} ${llmProvider}`,
|
|
15412
|
+
validate: () => void 0
|
|
15413
|
+
});
|
|
15414
|
+
if (isCancel(apiKeyResult)) {
|
|
15415
|
+
outro(getCopy2("cli.setup.skipped"));
|
|
15416
|
+
return { ok: true };
|
|
15417
|
+
}
|
|
15418
|
+
llmApiKey = String(apiKeyResult).trim() || void 0;
|
|
15419
|
+
const preset = resolvePreset(llmProvider);
|
|
15420
|
+
const modelResult = await text2({
|
|
15421
|
+
message: getCopy2("cli.setup.modelPrompt"),
|
|
15422
|
+
placeholder: preset?.defaultModel ?? "",
|
|
15423
|
+
defaultValue: config.llmModel ?? "",
|
|
15424
|
+
validate: () => void 0
|
|
15425
|
+
});
|
|
15426
|
+
if (isCancel(modelResult)) {
|
|
15427
|
+
outro(getCopy2("cli.setup.skipped"));
|
|
15428
|
+
return { ok: true };
|
|
15429
|
+
}
|
|
15430
|
+
llmModel = String(modelResult).trim() || void 0;
|
|
15431
|
+
if (PROVIDERS_WITH_CUSTOM_URL.has(llmProvider)) {
|
|
15432
|
+
const urlResult = await text2({
|
|
15433
|
+
message: getCopy2("cli.setup.baseUrlPrompt"),
|
|
15434
|
+
placeholder: preset?.baseUrl ?? "",
|
|
15435
|
+
defaultValue: config.llmApiUrl ?? "",
|
|
15436
|
+
validate: () => void 0
|
|
15437
|
+
});
|
|
15438
|
+
if (isCancel(urlResult)) {
|
|
15439
|
+
outro(getCopy2("cli.setup.skipped"));
|
|
15440
|
+
return { ok: true };
|
|
15441
|
+
}
|
|
15442
|
+
llmApiUrl = String(urlResult).trim() || void 0;
|
|
15443
|
+
} else {
|
|
15444
|
+
llmApiUrl = void 0;
|
|
15445
|
+
}
|
|
15446
|
+
}
|
|
15447
|
+
await writeConfig({ licenseKey, llmProvider, llmApiKey, llmModel, llmApiUrl });
|
|
15448
|
+
outro(getCopy2("cli.setup.done"));
|
|
15449
|
+
return { ok: true };
|
|
15450
|
+
}
|
|
15451
|
+
var setupCommand = createCommand(
|
|
15452
|
+
{
|
|
15453
|
+
name: "setup",
|
|
15454
|
+
description: "Interactive first-run setup for license key and LLM provider",
|
|
15455
|
+
handler: async () => executeSetup()
|
|
15456
|
+
},
|
|
15457
|
+
{ requireLicense: false }
|
|
15458
|
+
);
|
|
15459
|
+
|
|
15286
15460
|
// src/auth/fs-credential-provider.ts
|
|
15287
15461
|
var FsCredentialProvider = class {
|
|
15288
15462
|
async getCredentials() {
|
|
@@ -15344,6 +15518,20 @@ async function checkUpdate() {
|
|
|
15344
15518
|
}
|
|
15345
15519
|
|
|
15346
15520
|
// src/cli-action.ts
|
|
15521
|
+
function getConfigFile2() {
|
|
15522
|
+
return join15(process.env.SEAWORTHY_CONFIG_DIR ?? join15(homedir3(), ".seaworthy"), "config");
|
|
15523
|
+
}
|
|
15524
|
+
async function isFirstRun(options) {
|
|
15525
|
+
if (options.json || options.output || options.sarif || options.sarifStdout || process.env.CI || process.env.SEAWORTHY_LICENSE_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
15526
|
+
return false;
|
|
15527
|
+
}
|
|
15528
|
+
try {
|
|
15529
|
+
await access2(getConfigFile2());
|
|
15530
|
+
return false;
|
|
15531
|
+
} catch {
|
|
15532
|
+
return true;
|
|
15533
|
+
}
|
|
15534
|
+
}
|
|
15347
15535
|
function resolveExitCode(code) {
|
|
15348
15536
|
const normalized = code.replace(/^errors\./, "");
|
|
15349
15537
|
if (["license.invalid", "license.expired"].includes(normalized)) return 1;
|
|
@@ -15401,14 +15589,23 @@ async function runCli(options) {
|
|
|
15401
15589
|
error: getCopy2("cli.error.notFound", dir)
|
|
15402
15590
|
};
|
|
15403
15591
|
}
|
|
15592
|
+
if (await isFirstRun(options)) {
|
|
15593
|
+
console.error(getCopy2("cli.firstRun"));
|
|
15594
|
+
await setupCommand.handler({ action: "run" }, { tier: "free", licensedTo: "free tier" });
|
|
15595
|
+
}
|
|
15404
15596
|
const provider = new FsCredentialProvider();
|
|
15405
15597
|
const license = new LicenseClient({ baseUrl: LICENSE_SERVER_URL });
|
|
15406
15598
|
const authResult = await resolveAuth(provider, license);
|
|
15407
15599
|
if (!authResult.ok) {
|
|
15408
|
-
const
|
|
15409
|
-
|
|
15600
|
+
const normalized = authResult.code.replace(/^errors\./, "");
|
|
15601
|
+
const softCodes = ["license.invalid", "license.expired"];
|
|
15602
|
+
if (!softCodes.includes(normalized)) {
|
|
15603
|
+
const message = resolveErrorMessage(authResult.code);
|
|
15604
|
+
return { code: resolveExitCode(authResult.code), error: message };
|
|
15605
|
+
}
|
|
15606
|
+
console.error(getCopy2("cli.warn.licenseInvalid"));
|
|
15410
15607
|
}
|
|
15411
|
-
const auth = authResult.value;
|
|
15608
|
+
const auth = authResult.ok ? authResult.value : { tier: "free", token: "", licensedTo: "free tier" };
|
|
15412
15609
|
const allowedTiers = {
|
|
15413
15610
|
free: ["free"],
|
|
15414
15611
|
pro: ["free", "pro"]
|
|
@@ -15437,7 +15634,8 @@ async function runCli(options) {
|
|
|
15437
15634
|
maxFileSize: options.maxFileSize,
|
|
15438
15635
|
debug: options.debug,
|
|
15439
15636
|
baseline: options.baseline,
|
|
15440
|
-
minConfidence: options.minConfidence
|
|
15637
|
+
minConfidence: options.minConfidence,
|
|
15638
|
+
showIds: options.showIds
|
|
15441
15639
|
};
|
|
15442
15640
|
const updateStart = Date.now();
|
|
15443
15641
|
const updatePromise = checkUpdate().catch(() => void 0);
|
|
@@ -15524,12 +15722,12 @@ var require5 = createRequire2(import.meta.url);
|
|
|
15524
15722
|
var { version } = require5("../package.json");
|
|
15525
15723
|
var cli = cac("seaworthy");
|
|
15526
15724
|
cli.version(version);
|
|
15527
|
-
cli.command("[path]", "Scan a directory for issues").option("--json", "Output JSON to stdout").option("--output <path>", "Write HTML report to file").option("--sarif <path>", "Write SARIF report to file").option("--sarif-stdout", "Write SARIF report to stdout").option("--fail-on-findings", "Exit with code 5 when findings are found").option("--tier <tier>", "Select tier (free, pro)").option("--provider <provider>", "LLM provider (anthropic, openai, kimi, deepseek, gemini, minimax, ollama, openrouter)").option("--max-file-size <bytes>", "Skip files larger than this (default: 1 MB)").option("--debug", "Enable debug logging").option("--baseline [ref]", "Compare against a baseline file or save current findings as baseline (--baseline save)").option("--min-confidence <level>", "Hide findings below this confidence (high, medium, low)").action(async (targetDir, options) => {
|
|
15725
|
+
cli.command("[path]", "Scan a directory for issues").option("--json", "Output JSON to stdout").option("--output <path>", "Write HTML report to file").option("--sarif <path>", "Write SARIF report to file").option("--sarif-stdout", "Write SARIF report to stdout").option("--fail-on-findings", "Exit with code 5 when findings are found").option("--tier <tier>", "Select tier (free, pro)").option("--provider <provider>", "LLM provider (anthropic, openai, kimi, moonshot, deepseek, gemini, minimax, ollama, openrouter)").option("--max-file-size <bytes>", "Skip files larger than this (default: 1 MB)").option("--show-ids", "Show check IDs alongside names in terminal output").option("--debug", "Enable debug logging").option("--baseline [ref]", "Compare against a baseline file or save current findings as baseline (--baseline save)").option("--min-confidence <level>", "Hide findings below this confidence (high, medium, low)").action(async (targetDir, options) => {
|
|
15528
15726
|
const maxFileSize = options.maxFileSize ? Number(options.maxFileSize) : void 0;
|
|
15529
15727
|
const tier = options.tier;
|
|
15530
15728
|
const baseline = options.baseline === "save" ? "save" : options.baseline;
|
|
15531
15729
|
const minConfidence = options.minConfidence;
|
|
15532
|
-
const result = await runCli({ targetDir, json: options.json, output: options.output, sarif: options.sarif, sarifStdout: options.sarifStdout, failOnFindings: options.failOnFindings, tier, provider: options.provider, maxFileSize, debug: options.debug, baseline, minConfidence });
|
|
15730
|
+
const result = await runCli({ targetDir, json: options.json, output: options.output, sarif: options.sarif, sarifStdout: options.sarifStdout, failOnFindings: options.failOnFindings, tier, provider: options.provider, maxFileSize, showIds: options.showIds, debug: options.debug, baseline, minConfidence });
|
|
15533
15731
|
if (result.updateMessage) {
|
|
15534
15732
|
console.error(result.updateMessage);
|
|
15535
15733
|
}
|
|
@@ -15553,15 +15751,26 @@ cli.command("config <action> [key] [value]", "Manage configuration").action(asyn
|
|
|
15553
15751
|
console.log(result.output);
|
|
15554
15752
|
}
|
|
15555
15753
|
});
|
|
15754
|
+
cli.command("setup", "Interactive setup for license key and LLM provider").action(async () => {
|
|
15755
|
+
const result = await setupCommand.handler({ action: "run" }, { tier: "free", licensedTo: "free tier" });
|
|
15756
|
+
if (!result.ok) {
|
|
15757
|
+
console.error(resolveErrorMessage(result.code));
|
|
15758
|
+
process.exit(2);
|
|
15759
|
+
}
|
|
15760
|
+
});
|
|
15556
15761
|
cli.help(() => {
|
|
15557
15762
|
return [
|
|
15558
15763
|
{
|
|
15559
15764
|
title: "Examples",
|
|
15560
|
-
body: ` $ npx
|
|
15561
|
-
$ npx
|
|
15562
|
-
$ npx
|
|
15563
|
-
$ npx
|
|
15564
|
-
$ npx
|
|
15765
|
+
body: ` $ npx seaworthycode ./my-project
|
|
15766
|
+
$ npx seaworthycode ./my-project --json
|
|
15767
|
+
$ npx seaworthycode ./my-project --output report.html
|
|
15768
|
+
$ npx seaworthycode ./my-project --sarif report.sarif
|
|
15769
|
+
$ npx seaworthycode --provider kimi ./my-project`
|
|
15770
|
+
},
|
|
15771
|
+
{
|
|
15772
|
+
title: "Commands",
|
|
15773
|
+
body: ` npx seaworthycode setup Interactive setup for license key and LLM provider`
|
|
15565
15774
|
},
|
|
15566
15775
|
{
|
|
15567
15776
|
title: "Options",
|
|
@@ -15571,10 +15780,11 @@ cli.help(() => {
|
|
|
15571
15780
|
--sarif-stdout ${getCopy2("cli.help.flag.sarifStdout")}
|
|
15572
15781
|
--fail-on-findings Exit with code 5 when findings are found
|
|
15573
15782
|
--tier <tier> Override tier (free, pro)
|
|
15574
|
-
--provider <name> LLM provider (anthropic, openai, kimi, deepseek, gemini, minimax, ollama, openrouter)
|
|
15783
|
+
--provider <name> LLM provider (anthropic, openai, kimi, moonshot, deepseek, gemini, minimax, ollama, openrouter)
|
|
15575
15784
|
--max-file-size <n> Skip files larger than n bytes (default: 1 MB)
|
|
15576
15785
|
--baseline [ref] Compare against baseline file or save (--baseline save)
|
|
15577
15786
|
--min-confidence <level> Hide findings below confidence (high, medium, low)
|
|
15787
|
+
--show-ids Show check IDs alongside names
|
|
15578
15788
|
--debug Enable debug logging`
|
|
15579
15789
|
},
|
|
15580
15790
|
{
|