seaworthycode 1.2.6 → 1.2.8
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 +231 -158
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -64,7 +64,7 @@ import { createHash as createHash5, randomBytes } from "crypto";
|
|
|
64
64
|
import * as fs8 from "fs";
|
|
65
65
|
import { join as join11 } from "path";
|
|
66
66
|
import { createHash as createHash6 } from "crypto";
|
|
67
|
-
import { Query as TSQuery2 } from "web-tree-sitter";
|
|
67
|
+
import { Parser as TSParser, Query as TSQuery2 } from "web-tree-sitter";
|
|
68
68
|
import { readFileSync as readFileSync4, readdirSync, existsSync as existsSync4 } from "fs";
|
|
69
69
|
import { dirname as dirname3, resolve as pathResolve3 } from "path";
|
|
70
70
|
import { parse as parseYaml } from "yaml";
|
|
@@ -1330,7 +1330,7 @@ var copy = {
|
|
|
1330
1330
|
"debug.cache_write_failed": (key) => `Cache write failed for key ${key}, skipping.`,
|
|
1331
1331
|
// CLI
|
|
1332
1332
|
"cli.help.description": "Seaworthy \u2014 static analysis for vibe coders",
|
|
1333
|
-
"cli.help.usage": "Usage:
|
|
1333
|
+
"cli.help.usage": "Usage: seaworthycode [path] [options]",
|
|
1334
1334
|
"cli.help.options": "Options",
|
|
1335
1335
|
"cli.scanning": "Scanning...",
|
|
1336
1336
|
"cli.reportWritten": (path) => `Report written to ${path}`,
|
|
@@ -1497,7 +1497,10 @@ var copy = {
|
|
|
1497
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).",
|
|
1498
1498
|
// Reporting labels
|
|
1499
1499
|
"reporting.analysisType.pattern": "pattern",
|
|
1500
|
-
"reporting.analysisType.semantic": "semantic"
|
|
1500
|
+
"reporting.analysisType.semantic": "semantic",
|
|
1501
|
+
// Collapsed low/info finding groups
|
|
1502
|
+
"reporting.collapsed.summary": (count, files) => `${count} findings across ${files} ${files === 1 ? "file" : "files"}`,
|
|
1503
|
+
"reporting.collapsed.hint": "Run with --all to list each finding, or --output report.html for full detail."
|
|
1501
1504
|
};
|
|
1502
1505
|
function getCopy(key, ...args) {
|
|
1503
1506
|
const value = copy[key];
|
|
@@ -1613,7 +1616,8 @@ var ScanRunner = class {
|
|
|
1613
1616
|
files,
|
|
1614
1617
|
meta,
|
|
1615
1618
|
taintCache: /* @__PURE__ */ new Map(),
|
|
1616
|
-
astCache: /* @__PURE__ */ new Map()
|
|
1619
|
+
astCache: /* @__PURE__ */ new Map(),
|
|
1620
|
+
bashAstCache: /* @__PURE__ */ new Map()
|
|
1617
1621
|
};
|
|
1618
1622
|
const results = [];
|
|
1619
1623
|
const checkResults = /* @__PURE__ */ new Map();
|
|
@@ -1646,18 +1650,15 @@ var ScanRunner = class {
|
|
|
1646
1650
|
}
|
|
1647
1651
|
}
|
|
1648
1652
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
}
|
|
1656
|
-
} catch {
|
|
1657
|
-
}
|
|
1653
|
+
ctx.astCache?.clear();
|
|
1654
|
+
for (const [, promise] of ctx.bashAstCache ?? []) {
|
|
1655
|
+
try {
|
|
1656
|
+
const parsed = await promise;
|
|
1657
|
+
parsed.tree.delete();
|
|
1658
|
+
} catch {
|
|
1658
1659
|
}
|
|
1659
|
-
ctx.astCache.clear();
|
|
1660
1660
|
}
|
|
1661
|
+
ctx.bashAstCache?.clear();
|
|
1661
1662
|
let findings = sortFindings(
|
|
1662
1663
|
dedupeDegradedFindings(dedupeCrossCheck(dedupeFindings(results)))
|
|
1663
1664
|
);
|
|
@@ -2427,6 +2428,7 @@ var text = {
|
|
|
2427
2428
|
success: (s) => source_default.hex(tokens.success)(s)
|
|
2428
2429
|
};
|
|
2429
2430
|
var SEVERITY_ORDER2 = ["critical", "high", "medium", "low", "info"];
|
|
2431
|
+
var COLLAPSIBLE_SEVERITIES = /* @__PURE__ */ new Set(["low", "info"]);
|
|
2430
2432
|
function checkNameOf(checkId) {
|
|
2431
2433
|
const short = checkId.replace(/^[^.]+\./, "");
|
|
2432
2434
|
try {
|
|
@@ -2435,9 +2437,49 @@ function checkNameOf(checkId) {
|
|
|
2435
2437
|
return short;
|
|
2436
2438
|
}
|
|
2437
2439
|
}
|
|
2440
|
+
function analysisTypeSuffix(f) {
|
|
2441
|
+
const rawAnalysisType = f.properties?.analysisType;
|
|
2442
|
+
const analysisTypeValue = rawAnalysisType === "pattern" || rawAnalysisType === "semantic" ? rawAnalysisType : void 0;
|
|
2443
|
+
return analysisTypeValue ? text.meta(` [${getCopy(`reporting.analysisType.${analysisTypeValue}`)}]`) : "";
|
|
2444
|
+
}
|
|
2445
|
+
function renderFinding(f, showIds) {
|
|
2446
|
+
const severityLabel = textForSeverity(f.severity);
|
|
2447
|
+
const baselineLabel = f.baselineState === "new" ? text.success(" [NEW]") : "";
|
|
2448
|
+
const loc = f.file ? text.meta(` (${f.file}:${f.line ?? "-"})`) : "";
|
|
2449
|
+
const confidenceLabel = f.confidence && f.confidence !== "high" ? text.meta(` [${f.confidence} confidence]`) : "";
|
|
2450
|
+
const analysisType = analysisTypeSuffix(f);
|
|
2451
|
+
const name = checkNameOf(f.checkId);
|
|
2452
|
+
const idSuffix = showIds ? text.meta(` \xB7 ${f.checkId}`) : "";
|
|
2453
|
+
const lines = [];
|
|
2454
|
+
lines.push(`${severityLabel}${baselineLabel} ${name}${idSuffix}${analysisType} \u2014 ${f.message}${loc}${confidenceLabel}`);
|
|
2455
|
+
if (f.remediation) {
|
|
2456
|
+
lines.push(` ${text.meta(`\u2192 ${f.remediation}`)}`);
|
|
2457
|
+
}
|
|
2458
|
+
if (f.properties?.suggestion) {
|
|
2459
|
+
lines.push(` ${text.meta(`\u{1F4A1} ${f.properties.suggestion}`)}`);
|
|
2460
|
+
}
|
|
2461
|
+
return lines;
|
|
2462
|
+
}
|
|
2463
|
+
function renderCollapsedGroup(group, showIds) {
|
|
2464
|
+
const first = group[0];
|
|
2465
|
+
const severityLabel = textForSeverity(first.severity);
|
|
2466
|
+
const analysisType = analysisTypeSuffix(first);
|
|
2467
|
+
const name = checkNameOf(first.checkId);
|
|
2468
|
+
const idSuffix = showIds ? text.meta(` \xB7 ${first.checkId}`) : "";
|
|
2469
|
+
const total = group.length;
|
|
2470
|
+
const fileCount = new Set(group.map((f) => f.file).filter(Boolean)).size;
|
|
2471
|
+
const countPhrase = getCopy("reporting.collapsed.summary", total, fileCount);
|
|
2472
|
+
const lines = [];
|
|
2473
|
+
lines.push(`${severityLabel} ${name}${idSuffix}${analysisType} \u2014 ${countPhrase}`);
|
|
2474
|
+
if (first.remediation) {
|
|
2475
|
+
lines.push(` ${text.meta(`\u2192 ${first.remediation}`)}`);
|
|
2476
|
+
}
|
|
2477
|
+
lines.push(` ${text.meta(`\u2192 ${getCopy("reporting.collapsed.hint")}`)}`);
|
|
2478
|
+
return lines;
|
|
2479
|
+
}
|
|
2438
2480
|
var TerminalReporter = class {
|
|
2439
2481
|
generate(input) {
|
|
2440
|
-
const { findings: rawFindings, targetDir, tier, licensedTo, scannedFileCount, skippedChecks = [], showIds = false } = input;
|
|
2482
|
+
const { findings: rawFindings, targetDir, tier, licensedTo, scannedFileCount, skippedChecks = [], showIds = false, all = false } = input;
|
|
2441
2483
|
const findings = dedupeDegradedFindings(rawFindings);
|
|
2442
2484
|
if (findings.length === 0) {
|
|
2443
2485
|
const lines2 = [];
|
|
@@ -2456,22 +2498,30 @@ var TerminalReporter = class {
|
|
|
2456
2498
|
const lines = [];
|
|
2457
2499
|
lines.push(text.heading(getCopy("report.header", targetDir, findings.length)));
|
|
2458
2500
|
lines.push("");
|
|
2459
|
-
|
|
2460
|
-
const
|
|
2461
|
-
|
|
2462
|
-
const loc = f.file ? text.meta(` (${f.file}:${f.line ?? "-"})`) : "";
|
|
2463
|
-
const confidenceLabel = f.confidence && f.confidence !== "high" ? text.meta(` [${f.confidence} confidence]`) : "";
|
|
2464
|
-
const rawAnalysisType = f.properties?.analysisType;
|
|
2465
|
-
const analysisTypeValue = rawAnalysisType === "pattern" || rawAnalysisType === "semantic" ? rawAnalysisType : void 0;
|
|
2466
|
-
const analysisType = analysisTypeValue ? text.meta(` [${getCopy(`reporting.analysisType.${analysisTypeValue}`)}]`) : "";
|
|
2467
|
-
const name = checkNameOf(f.checkId);
|
|
2468
|
-
const idSuffix = showIds ? text.meta(` \xB7 ${f.checkId}`) : "";
|
|
2469
|
-
lines.push(`${severityLabel}${baselineLabel} ${name}${idSuffix}${analysisType} \u2014 ${f.message}${loc}${confidenceLabel}`);
|
|
2470
|
-
if (f.remediation) {
|
|
2471
|
-
lines.push(` ${text.meta(`\u2192 ${f.remediation}`)}`);
|
|
2501
|
+
if (all) {
|
|
2502
|
+
for (const f of findings) {
|
|
2503
|
+
lines.push(...renderFinding(f, showIds));
|
|
2472
2504
|
}
|
|
2473
|
-
|
|
2474
|
-
|
|
2505
|
+
} else {
|
|
2506
|
+
const collapsibleGroups = /* @__PURE__ */ new Map();
|
|
2507
|
+
for (const f of findings) {
|
|
2508
|
+
if (COLLAPSIBLE_SEVERITIES.has(f.severity)) {
|
|
2509
|
+
const group = collapsibleGroups.get(f.checkId);
|
|
2510
|
+
if (group) {
|
|
2511
|
+
group.push(f);
|
|
2512
|
+
} else {
|
|
2513
|
+
collapsibleGroups.set(f.checkId, [f]);
|
|
2514
|
+
}
|
|
2515
|
+
} else {
|
|
2516
|
+
lines.push(...renderFinding(f, showIds));
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
for (const group of collapsibleGroups.values()) {
|
|
2520
|
+
if (group.length === 1) {
|
|
2521
|
+
lines.push(...renderFinding(group[0], showIds));
|
|
2522
|
+
} else {
|
|
2523
|
+
lines.push(...renderCollapsedGroup(group, showIds));
|
|
2524
|
+
}
|
|
2475
2525
|
}
|
|
2476
2526
|
}
|
|
2477
2527
|
lines.push("");
|
|
@@ -3582,7 +3632,56 @@ function getOrCreateParser(lang, langKey) {
|
|
|
3582
3632
|
}
|
|
3583
3633
|
return parser;
|
|
3584
3634
|
}
|
|
3585
|
-
async function
|
|
3635
|
+
async function resolveContent(file, contentCache) {
|
|
3636
|
+
if (contentCache) {
|
|
3637
|
+
const cacheKey = `${file.path}::${file.language}`;
|
|
3638
|
+
let contentPromise = contentCache.get(cacheKey);
|
|
3639
|
+
if (!contentPromise) {
|
|
3640
|
+
contentPromise = file.content().catch(() => {
|
|
3641
|
+
throw new Error("read_failed");
|
|
3642
|
+
});
|
|
3643
|
+
contentCache.set(cacheKey, contentPromise);
|
|
3644
|
+
}
|
|
3645
|
+
return contentPromise;
|
|
3646
|
+
}
|
|
3647
|
+
return file.content();
|
|
3648
|
+
}
|
|
3649
|
+
async function parseFile(file, contentCache) {
|
|
3650
|
+
if (!isLanguageSupported(file.language)) return null;
|
|
3651
|
+
const wasmFilename = LANGUAGE_WASM_MAP[file.language];
|
|
3652
|
+
if (!wasmFilename) return null;
|
|
3653
|
+
try {
|
|
3654
|
+
await ensureParserInit();
|
|
3655
|
+
} catch {
|
|
3656
|
+
return null;
|
|
3657
|
+
}
|
|
3658
|
+
let lang;
|
|
3659
|
+
try {
|
|
3660
|
+
lang = await loadLanguage(file.language);
|
|
3661
|
+
} catch {
|
|
3662
|
+
return null;
|
|
3663
|
+
}
|
|
3664
|
+
if (languageVerified.get(file.language) === false) return null;
|
|
3665
|
+
let source;
|
|
3666
|
+
try {
|
|
3667
|
+
source = await resolveContent(file, contentCache);
|
|
3668
|
+
} catch {
|
|
3669
|
+
return null;
|
|
3670
|
+
}
|
|
3671
|
+
if (source.length === 0 || source.length > MAX_PARSE_BYTES) return null;
|
|
3672
|
+
const freshParser = new Parser();
|
|
3673
|
+
freshParser.setLanguage(lang);
|
|
3674
|
+
try {
|
|
3675
|
+
const tree = freshParser.parse(source);
|
|
3676
|
+
freshParser.delete();
|
|
3677
|
+
if (!tree) return null;
|
|
3678
|
+
return { tree, source };
|
|
3679
|
+
} catch {
|
|
3680
|
+
freshParser.delete();
|
|
3681
|
+
return null;
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
async function queryFile(file, queryString, astCache, bashAstCache) {
|
|
3586
3685
|
if (!isLanguageSupported(file.language)) {
|
|
3587
3686
|
return { matches: [], degraded: true };
|
|
3588
3687
|
}
|
|
@@ -3610,32 +3709,30 @@ async function queryFile(file, queryString, astCache) {
|
|
|
3610
3709
|
return { matches: [], degraded: true, error: `languageVerified is false for ${file.language}` };
|
|
3611
3710
|
}
|
|
3612
3711
|
let source;
|
|
3712
|
+
try {
|
|
3713
|
+
source = await resolveContent(file, astCache);
|
|
3714
|
+
} catch {
|
|
3715
|
+
return { matches: [], degraded: true };
|
|
3716
|
+
}
|
|
3717
|
+
if (source.length === 0) {
|
|
3718
|
+
return { matches: [], degraded: false };
|
|
3719
|
+
}
|
|
3720
|
+
if (source.length > MAX_PARSE_BYTES) {
|
|
3721
|
+
return { matches: [], degraded: true };
|
|
3722
|
+
}
|
|
3613
3723
|
let tree;
|
|
3614
|
-
|
|
3724
|
+
let ownTree = false;
|
|
3725
|
+
if (file.language === "bash" && bashAstCache) {
|
|
3615
3726
|
const cacheKey = `${file.path}::${file.language}`;
|
|
3616
|
-
let parsePromise =
|
|
3727
|
+
let parsePromise = bashAstCache.get(cacheKey);
|
|
3617
3728
|
if (!parsePromise) {
|
|
3618
3729
|
parsePromise = (async () => {
|
|
3619
|
-
let s;
|
|
3620
|
-
try {
|
|
3621
|
-
s = await file.content();
|
|
3622
|
-
} catch {
|
|
3623
|
-
throw new Error("read_failed");
|
|
3624
|
-
}
|
|
3625
|
-
if (s.length === 0) {
|
|
3626
|
-
return { tree: null, source: s };
|
|
3627
|
-
}
|
|
3628
|
-
if (s.length > MAX_PARSE_BYTES) {
|
|
3629
|
-
throw new Error("file_too_large");
|
|
3630
|
-
}
|
|
3631
3730
|
const parser = getOrCreateParser(lang, file.language);
|
|
3632
|
-
const parsed2 = parser.parse(
|
|
3633
|
-
if (!parsed2)
|
|
3634
|
-
|
|
3635
|
-
}
|
|
3636
|
-
return { tree: parsed2, source: s };
|
|
3731
|
+
const parsed2 = parser.parse(source);
|
|
3732
|
+
if (!parsed2) throw new Error("parse_failed");
|
|
3733
|
+
return { tree: parsed2, source };
|
|
3637
3734
|
})();
|
|
3638
|
-
|
|
3735
|
+
bashAstCache.set(cacheKey, parsePromise);
|
|
3639
3736
|
}
|
|
3640
3737
|
let parsed;
|
|
3641
3738
|
try {
|
|
@@ -3643,31 +3740,20 @@ async function queryFile(file, queryString, astCache) {
|
|
|
3643
3740
|
} catch {
|
|
3644
3741
|
return { matches: [], degraded: true };
|
|
3645
3742
|
}
|
|
3646
|
-
if (parsed.source.length === 0) {
|
|
3647
|
-
return { matches: [], degraded: false };
|
|
3648
|
-
}
|
|
3649
|
-
source = parsed.source;
|
|
3650
3743
|
tree = parsed.tree;
|
|
3651
3744
|
} else {
|
|
3745
|
+
ownTree = true;
|
|
3746
|
+
const freshParser = new Parser();
|
|
3747
|
+
freshParser.setLanguage(lang);
|
|
3652
3748
|
try {
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
return { matches: [], degraded: true };
|
|
3656
|
-
}
|
|
3657
|
-
if (source.length === 0) {
|
|
3658
|
-
return { matches: [], degraded: false };
|
|
3659
|
-
}
|
|
3660
|
-
if (source.length > MAX_PARSE_BYTES) {
|
|
3661
|
-
return { matches: [], degraded: true };
|
|
3662
|
-
}
|
|
3663
|
-
try {
|
|
3664
|
-
const parser = getOrCreateParser(lang, file.language);
|
|
3665
|
-
const parsed = parser.parse(source);
|
|
3749
|
+
const parsed = freshParser.parse(source);
|
|
3750
|
+
freshParser.delete();
|
|
3666
3751
|
if (!parsed) {
|
|
3667
3752
|
return { matches: [], degraded: true };
|
|
3668
3753
|
}
|
|
3669
3754
|
tree = parsed;
|
|
3670
3755
|
} catch {
|
|
3756
|
+
freshParser.delete();
|
|
3671
3757
|
return { matches: [], degraded: true };
|
|
3672
3758
|
}
|
|
3673
3759
|
}
|
|
@@ -3679,7 +3765,7 @@ async function queryFile(file, queryString, astCache) {
|
|
|
3679
3765
|
query = new TSQuery(lang, queryString);
|
|
3680
3766
|
compiledQueryCache.set(queryCacheKey, query);
|
|
3681
3767
|
} catch (err) {
|
|
3682
|
-
tree.delete();
|
|
3768
|
+
if (ownTree) tree.delete();
|
|
3683
3769
|
console.error(
|
|
3684
3770
|
`[seaworthy] Invalid query for ${file.language}:`,
|
|
3685
3771
|
err.message
|
|
@@ -3729,9 +3815,7 @@ async function queryFile(file, queryString, astCache) {
|
|
|
3729
3815
|
}
|
|
3730
3816
|
return true;
|
|
3731
3817
|
});
|
|
3732
|
-
if (
|
|
3733
|
-
tree.delete();
|
|
3734
|
-
}
|
|
3818
|
+
if (ownTree) tree.delete();
|
|
3735
3819
|
return { matches, degraded: false };
|
|
3736
3820
|
}
|
|
3737
3821
|
function setAnalysisType(finding, analysisType = "pattern") {
|
|
@@ -3785,7 +3869,7 @@ function createTreeSitterCheck(options) {
|
|
|
3785
3869
|
async (file) => {
|
|
3786
3870
|
const query = queries[file.language];
|
|
3787
3871
|
try {
|
|
3788
|
-
const result = await queryFile(file, query, ctx.astCache);
|
|
3872
|
+
const result = await queryFile(file, query, ctx.astCache, ctx.bashAstCache);
|
|
3789
3873
|
if (result.degraded) {
|
|
3790
3874
|
if (!degradedLanguages.has(file.language)) {
|
|
3791
3875
|
degradedLanguages.add(file.language);
|
|
@@ -11870,36 +11954,21 @@ function runAnalyse(file, table, fileContent, astCache) {
|
|
|
11870
11954
|
}
|
|
11871
11955
|
if (content.length === 0) return { flows: [], degraded: false };
|
|
11872
11956
|
let tree;
|
|
11873
|
-
|
|
11957
|
+
const ownTree = true;
|
|
11874
11958
|
const cacheKey = `${file.path}::${file.language}`;
|
|
11875
|
-
if (astCache) {
|
|
11876
|
-
|
|
11877
|
-
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
|
|
11884
|
-
|
|
11885
|
-
|
|
11886
|
-
|
|
11887
|
-
|
|
11888
|
-
const r = await cached;
|
|
11889
|
-
tree = r.tree;
|
|
11890
|
-
} catch {
|
|
11891
|
-
return { flows: [], degraded: true };
|
|
11892
|
-
}
|
|
11893
|
-
} else {
|
|
11894
|
-
try {
|
|
11895
|
-
const parser = getOrCreateParser(lang, file.language);
|
|
11896
|
-
const parsed = parser.parse(content);
|
|
11897
|
-
if (!parsed) return { flows: [], degraded: true };
|
|
11898
|
-
tree = parsed;
|
|
11899
|
-
ownTree = true;
|
|
11900
|
-
} catch {
|
|
11901
|
-
return { flows: [], degraded: true };
|
|
11902
|
-
}
|
|
11959
|
+
if (astCache && !astCache.has(cacheKey)) {
|
|
11960
|
+
astCache.set(cacheKey, Promise.resolve(content));
|
|
11961
|
+
}
|
|
11962
|
+
const freshParser = new TSParser();
|
|
11963
|
+
freshParser.setLanguage(lang);
|
|
11964
|
+
try {
|
|
11965
|
+
const parsed = freshParser.parse(content);
|
|
11966
|
+
freshParser.delete();
|
|
11967
|
+
if (!parsed) return { flows: [], degraded: true };
|
|
11968
|
+
tree = parsed;
|
|
11969
|
+
} catch {
|
|
11970
|
+
freshParser.delete();
|
|
11971
|
+
return { flows: [], degraded: true };
|
|
11903
11972
|
}
|
|
11904
11973
|
const entries = collectEntries(table);
|
|
11905
11974
|
const taggedByNode = /* @__PURE__ */ new Map();
|
|
@@ -12833,7 +12902,7 @@ var check = {
|
|
|
12833
12902
|
const _exampleLine = 1;
|
|
12834
12903
|
for (const file of eligible) {
|
|
12835
12904
|
const query = QUERIES9[file.language];
|
|
12836
|
-
const result = await queryFile(file, query, ctx.astCache);
|
|
12905
|
+
const result = await queryFile(file, query, ctx.astCache, ctx.bashAstCache);
|
|
12837
12906
|
if (result.degraded) {
|
|
12838
12907
|
if (!degradedLanguages.has(file.language)) {
|
|
12839
12908
|
degradedLanguages.add(file.language);
|
|
@@ -13836,7 +13905,7 @@ var sqlRlsStaticCheck = {
|
|
|
13836
13905
|
const dialect = classifySqlDialect(content);
|
|
13837
13906
|
if (dialect !== "postgres" && dialect !== "supabase") continue;
|
|
13838
13907
|
const tableQuery = "(create_table (object_reference name: (identifier) @table_name))";
|
|
13839
|
-
const tableResult = await queryFile(file, tableQuery, ctx.astCache);
|
|
13908
|
+
const tableResult = await queryFile(file, tableQuery, ctx.astCache, ctx.bashAstCache);
|
|
13840
13909
|
if (tableResult.degraded) continue;
|
|
13841
13910
|
const tables = [];
|
|
13842
13911
|
for (const match of tableResult.matches) {
|
|
@@ -13846,7 +13915,7 @@ var sqlRlsStaticCheck = {
|
|
|
13846
13915
|
const columnQuery = `(create_table
|
|
13847
13916
|
(object_reference name: (identifier) @t_name (#eq? @t_name "${tableNameRaw}"))
|
|
13848
13917
|
(column_definitions (column_definition name: (identifier) @col_name)))`;
|
|
13849
|
-
const columnResult = await queryFile(file, columnQuery, ctx.astCache);
|
|
13918
|
+
const columnResult = await queryFile(file, columnQuery, ctx.astCache, ctx.bashAstCache);
|
|
13850
13919
|
const columns = columnResult.matches.map((m) => normalizeIdentifier(m.captures.col_name ?? ""));
|
|
13851
13920
|
const tenantColumns = ["tenant_id", "org_id", "workspace_id", "account_id", "user_id"];
|
|
13852
13921
|
const isTenantTable = columns.some((c) => tenantColumns.includes(c));
|
|
@@ -14295,7 +14364,7 @@ var unprotectedSensitiveSqlColumnsCheck = {
|
|
|
14295
14364
|
(identifier) @col_type
|
|
14296
14365
|
(keyword_text) @col_type
|
|
14297
14366
|
])`;
|
|
14298
|
-
const result = await queryFile(file, query, ctx.astCache);
|
|
14367
|
+
const result = await queryFile(file, query, ctx.astCache, ctx.bashAstCache);
|
|
14299
14368
|
if (result.degraded) continue;
|
|
14300
14369
|
for (const match of result.matches) {
|
|
14301
14370
|
const colNameRaw = match.captures.col_name;
|
|
@@ -14394,7 +14463,7 @@ registry.register({
|
|
|
14394
14463
|
continue;
|
|
14395
14464
|
}
|
|
14396
14465
|
const lines = code.split("\n");
|
|
14397
|
-
const queryResult = await queryFile(file, "(unsafe_block) @unsafe", ctx.astCache);
|
|
14466
|
+
const queryResult = await queryFile(file, "(unsafe_block) @unsafe", ctx.astCache, ctx.bashAstCache);
|
|
14398
14467
|
if (queryResult.degraded) continue;
|
|
14399
14468
|
for (let i = 0; i < queryResult.matches.length; i++) {
|
|
14400
14469
|
const match = queryResult.matches[i];
|
|
@@ -14546,50 +14615,51 @@ registry.register({
|
|
|
14546
14615
|
}
|
|
14547
14616
|
const isWebFile = /axum|actix/i.test(code);
|
|
14548
14617
|
if (!isWebFile) continue;
|
|
14549
|
-
const queryResult = await queryFile(file, RUST_QUERY, ctx.astCache);
|
|
14618
|
+
const queryResult = await queryFile(file, RUST_QUERY, ctx.astCache, ctx.bashAstCache);
|
|
14550
14619
|
if (queryResult.degraded) continue;
|
|
14551
|
-
const
|
|
14552
|
-
|
|
14553
|
-
if (!parsePromise) continue;
|
|
14554
|
-
const parsed = await parsePromise;
|
|
14555
|
-
if (!parsed || !parsed.tree) continue;
|
|
14620
|
+
const parsed = await parseFile(file, ctx.astCache);
|
|
14621
|
+
if (!parsed) continue;
|
|
14556
14622
|
const parentMap = /* @__PURE__ */ new Map();
|
|
14557
14623
|
walk2(parsed.tree.rootNode);
|
|
14558
|
-
|
|
14559
|
-
|
|
14560
|
-
|
|
14561
|
-
|
|
14562
|
-
|
|
14563
|
-
|
|
14564
|
-
|
|
14565
|
-
|
|
14566
|
-
|
|
14567
|
-
|
|
14568
|
-
|
|
14624
|
+
try {
|
|
14625
|
+
for (let i = 0; i < queryResult.matches.length; i++) {
|
|
14626
|
+
const match = queryResult.matches[i];
|
|
14627
|
+
if (!match) continue;
|
|
14628
|
+
const node = findNodeAt2(parsed.tree.rootNode, match.startLine, match.startColumn ?? 0);
|
|
14629
|
+
if (!node) continue;
|
|
14630
|
+
let curr = node;
|
|
14631
|
+
let enclosingFunction = null;
|
|
14632
|
+
while (curr) {
|
|
14633
|
+
if (curr.type === "function_item") {
|
|
14634
|
+
enclosingFunction = curr;
|
|
14635
|
+
break;
|
|
14636
|
+
}
|
|
14637
|
+
const next = parentMap.get(curr.id);
|
|
14638
|
+
if (!next) break;
|
|
14639
|
+
curr = next;
|
|
14569
14640
|
}
|
|
14570
|
-
|
|
14571
|
-
|
|
14572
|
-
|
|
14573
|
-
|
|
14574
|
-
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
14579
|
-
|
|
14580
|
-
|
|
14581
|
-
|
|
14582
|
-
|
|
14583
|
-
|
|
14584
|
-
|
|
14585
|
-
|
|
14586
|
-
|
|
14587
|
-
|
|
14588
|
-
};
|
|
14589
|
-
setAnalysisType(finding, "pattern");
|
|
14590
|
-
findings.push(finding);
|
|
14641
|
+
if (enclosingFunction) {
|
|
14642
|
+
const isAsync = hasAsyncModifier(enclosingFunction);
|
|
14643
|
+
if (isAsync) {
|
|
14644
|
+
const finding = {
|
|
14645
|
+
id: `${CHECK_ID56}:${file.relativePath}:${match.startLine}:${match.startColumn ?? 0}:${i}`,
|
|
14646
|
+
checkId: CHECK_ID56,
|
|
14647
|
+
category: "resilience",
|
|
14648
|
+
severity: "high",
|
|
14649
|
+
message: getCopy("checks.rust-handler-panics.message", match.nodeText),
|
|
14650
|
+
file: file.relativePath,
|
|
14651
|
+
line: match.startLine,
|
|
14652
|
+
column: match.startColumn,
|
|
14653
|
+
confidence: "high",
|
|
14654
|
+
remediation
|
|
14655
|
+
};
|
|
14656
|
+
setAnalysisType(finding, "pattern");
|
|
14657
|
+
findings.push(finding);
|
|
14658
|
+
}
|
|
14591
14659
|
}
|
|
14592
14660
|
}
|
|
14661
|
+
} finally {
|
|
14662
|
+
parsed.tree.delete();
|
|
14593
14663
|
}
|
|
14594
14664
|
}
|
|
14595
14665
|
return findings;
|
|
@@ -15844,7 +15914,7 @@ import { dirname as dirname5, resolve as resolve2, relative as relative7, isAbso
|
|
|
15844
15914
|
// src/copy/index.ts
|
|
15845
15915
|
var copy3 = {
|
|
15846
15916
|
"cli.help.description": "Seaworthy \u2014 static analysis for vibe coders",
|
|
15847
|
-
"cli.help.usage": "Usage:
|
|
15917
|
+
"cli.help.usage": "Usage: seaworthycode [path] [options]",
|
|
15848
15918
|
"cli.help.options": "Options",
|
|
15849
15919
|
"cli.help.learnMore": `Documentation: ${SITE_URL}/docs`,
|
|
15850
15920
|
"cli.setup.intro": "Welcome to Seaworthy! Let's get you set up.",
|
|
@@ -15873,14 +15943,14 @@ var copy3 = {
|
|
|
15873
15943
|
"cli.help.flag.sarifStdout": "Write SARIF report to stdout",
|
|
15874
15944
|
"cli.sarif.mutually_exclusive": "Flags --sarif and --sarif-stdout cannot be used together.",
|
|
15875
15945
|
"cli.sarif.stdout_conflict": "Flags --sarif-stdout and --json cannot be used together. Use --output to send JSON to a file.",
|
|
15876
|
-
"cli.error.llmRequired": "LLM API key required for Pro checks. Set it with
|
|
15877
|
-
"cli.warn.llmSkipped": "Pro checks skipped: no LLM API key configured. Set it with
|
|
15946
|
+
"cli.error.llmRequired": "LLM API key required for Pro checks. Set it with seaworthycode config or the SEAWORTHY_LLM_API_KEY environment variable.",
|
|
15947
|
+
"cli.warn.llmSkipped": "Pro checks skipped: no LLM API key configured. Set it with seaworthycode config or the SEAWORTHY_LLM_API_KEY environment variable to enable semantic analysis.",
|
|
15878
15948
|
"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",
|
|
15879
|
-
"cli.error.config.missingKey": "Missing config key. Usage:
|
|
15880
|
-
"cli.error.config.missingValue": "Missing config value. Usage:
|
|
15949
|
+
"cli.error.config.missingKey": "Missing config key. Usage: seaworthycode config <set|get> <key>",
|
|
15950
|
+
"cli.error.config.missingValue": "Missing config value. Usage: seaworthycode config set <key> <value>",
|
|
15881
15951
|
"cli.error.config.unknownKey": "Unknown config key. Valid keys: licenseKey, llmProvider, llmApiKey, llmModel, llmApiUrl",
|
|
15882
15952
|
"cli.error.config.invalidProvider": "Invalid provider. Valid providers: anthropic, openai, kimi, moonshot, deepseek, gemini, ollama, openrouter",
|
|
15883
|
-
"cli.error.config.invalidAction": "Invalid config action. Usage:
|
|
15953
|
+
"cli.error.config.invalidAction": "Invalid config action. Usage: seaworthycode config <set|get|list>",
|
|
15884
15954
|
"cli.config.setSuccess": (key) => `${key} updated.`,
|
|
15885
15955
|
"cli.licenseNotice": (licensedTo) => `Licensed to ${licensedTo}`,
|
|
15886
15956
|
"cli.error.debugHint": (logPath) => `Re-run with --debug to write a diagnostic log: ${logPath}
|
|
@@ -16013,7 +16083,8 @@ async function executeScan(args, ctx) {
|
|
|
16013
16083
|
licensedTo: ctx.licensedTo ?? "free tier",
|
|
16014
16084
|
scannedFileCount: result.scannedFileCount,
|
|
16015
16085
|
skippedChecks: result.skippedChecks,
|
|
16016
|
-
showIds: args.showIds ?? args.debug
|
|
16086
|
+
showIds: args.showIds ?? args.debug,
|
|
16087
|
+
all: args.all
|
|
16017
16088
|
};
|
|
16018
16089
|
let primaryOutput;
|
|
16019
16090
|
switch (args.format) {
|
|
@@ -16196,7 +16267,7 @@ var FsCredentialProvider = class {
|
|
|
16196
16267
|
import { readFile } from "fs/promises";
|
|
16197
16268
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
16198
16269
|
import { dirname as dirname6, join as join14 } from "path";
|
|
16199
|
-
var NPM_REGISTRY_URL = "https://registry.npmjs.org/
|
|
16270
|
+
var NPM_REGISTRY_URL = "https://registry.npmjs.org/seaworthycode";
|
|
16200
16271
|
var UPDATE_TIMEOUT_MS = 3e3;
|
|
16201
16272
|
function isLowerVersion(current, latest) {
|
|
16202
16273
|
const parse2 = (v) => v.split(".").map((n) => Number(n));
|
|
@@ -16229,7 +16300,7 @@ async function checkUpdate() {
|
|
|
16229
16300
|
}
|
|
16230
16301
|
const installed = await getInstalledVersion();
|
|
16231
16302
|
if (isLowerVersion(installed, latest)) {
|
|
16232
|
-
return `Update available: ${installed} \u2192 ${latest}. Run npm i -g
|
|
16303
|
+
return `Update available: ${installed} \u2192 ${latest}. Run npm i -g seaworthycode to update.`;
|
|
16233
16304
|
}
|
|
16234
16305
|
return void 0;
|
|
16235
16306
|
} catch {
|
|
@@ -16358,7 +16429,8 @@ async function runCli(options) {
|
|
|
16358
16429
|
debug: options.debug,
|
|
16359
16430
|
baseline: options.baseline,
|
|
16360
16431
|
minConfidence: options.minConfidence,
|
|
16361
|
-
showIds: options.showIds
|
|
16432
|
+
showIds: options.showIds,
|
|
16433
|
+
all: options.all
|
|
16362
16434
|
};
|
|
16363
16435
|
const updateStart = Date.now();
|
|
16364
16436
|
const updatePromise = checkUpdate().catch(() => void 0);
|
|
@@ -16533,14 +16605,14 @@ if (existsSync6(repoRootEnv)) {
|
|
|
16533
16605
|
}
|
|
16534
16606
|
var require5 = createRequire4(import.meta.url);
|
|
16535
16607
|
var { version } = require5("../package.json");
|
|
16536
|
-
var cli = cac("
|
|
16608
|
+
var cli = cac("seaworthycode");
|
|
16537
16609
|
cli.version(version);
|
|
16538
|
-
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) => {
|
|
16610
|
+
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)").option("--all", "List every finding; do not collapse low-severity findings").action(async (targetDir, options) => {
|
|
16539
16611
|
const maxFileSize = options.maxFileSize ? Number(options.maxFileSize) : void 0;
|
|
16540
16612
|
const tier = options.tier;
|
|
16541
16613
|
const baseline = options.baseline === "save" ? "save" : options.baseline;
|
|
16542
16614
|
const minConfidence = options.minConfidence;
|
|
16543
|
-
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 });
|
|
16615
|
+
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, all: options.all });
|
|
16544
16616
|
if (result.updateMessage) {
|
|
16545
16617
|
console.error(result.updateMessage);
|
|
16546
16618
|
}
|
|
@@ -16609,6 +16681,7 @@ cli.help(() => {
|
|
|
16609
16681
|
--baseline [ref] Compare against baseline file or save (--baseline save)
|
|
16610
16682
|
--min-confidence <level> Hide findings below confidence (high, medium, low)
|
|
16611
16683
|
--show-ids Show check IDs alongside names
|
|
16684
|
+
--all List every finding; do not collapse low-severity findings
|
|
16612
16685
|
--debug Enable debug logging`
|
|
16613
16686
|
},
|
|
16614
16687
|
{
|