@ship-safe/cli 1.1.13 → 1.1.14
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 +60 -10
- package/package.json +11 -10
package/dist/index.js
CHANGED
|
@@ -5926,14 +5926,26 @@ function isInsideStringLiteral(line, pattern) {
|
|
|
5926
5926
|
}
|
|
5927
5927
|
return backtickCount % 2 === 1;
|
|
5928
5928
|
}
|
|
5929
|
+
function isJsxTextContent(line) {
|
|
5930
|
+
const trimmed = line.trim();
|
|
5931
|
+
if (/^\w+\s*[:=]\s*["'`]/.test(trimmed) && trimmed.length > 80) {
|
|
5932
|
+
const nonCodeChars = trimmed.replace(/[a-zA-Z\s.,!?;:'"()\-]/g, "").length;
|
|
5933
|
+
if (nonCodeChars / trimmed.length < 0.15) return true;
|
|
5934
|
+
}
|
|
5935
|
+
if (/>[^<]{40,}</.test(trimmed)) return true;
|
|
5936
|
+
if (/["'`][A-Z][^"'`]{60,}["'`]/.test(trimmed)) {
|
|
5937
|
+
return true;
|
|
5938
|
+
}
|
|
5939
|
+
return false;
|
|
5940
|
+
}
|
|
5929
5941
|
function matchRule(rule, file) {
|
|
5930
5942
|
if (!rule.languages.includes("*") && !rule.languages.includes(file.language)) {
|
|
5931
5943
|
return [];
|
|
5932
5944
|
}
|
|
5933
5945
|
const isSecretRule = rule.id.startsWith("secrets/");
|
|
5934
5946
|
if (!isSecretRule) {
|
|
5935
|
-
const
|
|
5936
|
-
if (
|
|
5947
|
+
const contentPaths = /(?:\/docs\/|\/blog\/|\/for\/|\/examples\/|\/fixtures\/|\/tutorials?\/|\/guides?\/|__tests__\/fixtures)/i;
|
|
5948
|
+
if (contentPaths.test(file.path)) return [];
|
|
5937
5949
|
}
|
|
5938
5950
|
const findings = [];
|
|
5939
5951
|
const lines = file.content.split("\n");
|
|
@@ -5946,6 +5958,7 @@ function matchRule(rule, file) {
|
|
|
5946
5958
|
regex.lastIndex = 0;
|
|
5947
5959
|
if (!isSecretRule && isCommentLine(line)) continue;
|
|
5948
5960
|
if (!isSecretRule && isInsideStringLiteral(line, new RegExp(pattern.regex, "gi"))) continue;
|
|
5961
|
+
if (!isSecretRule && isJsxTextContent(line)) continue;
|
|
5949
5962
|
if (rule.excludePatterns?.length) {
|
|
5950
5963
|
const excluded = rule.excludePatterns.some((ep) => {
|
|
5951
5964
|
const exRegex = new RegExp(ep.regex, "i");
|
|
@@ -6839,12 +6852,15 @@ function detectPlatform(files) {
|
|
|
6839
6852
|
if (lower.endsWith("package.json") && content.includes('"v0"')) {
|
|
6840
6853
|
signals.v0.push("v0 dependency in package.json");
|
|
6841
6854
|
}
|
|
6842
|
-
if (content.includes("base44") || content.includes("Base44")) {
|
|
6843
|
-
signals.base44.push("base44 reference in source");
|
|
6844
|
-
}
|
|
6845
6855
|
if (lower.includes("base44.config") || lower.includes(".base44")) {
|
|
6846
6856
|
signals.base44.push("base44 config file");
|
|
6847
6857
|
}
|
|
6858
|
+
if (/from\s+['"]@?base44\b/i.test(content) || /require\s*\(\s*['"]@?base44\b/i.test(content)) {
|
|
6859
|
+
signals.base44.push("base44 SDK import in source");
|
|
6860
|
+
}
|
|
6861
|
+
if (/https?:\/\/[^"'\s]*base44\.app/i.test(content)) {
|
|
6862
|
+
signals.base44.push("base44.app URL in source");
|
|
6863
|
+
}
|
|
6848
6864
|
}
|
|
6849
6865
|
const scores = Object.entries(signals).filter(([key]) => key !== "manual").map(([platform, sigs]) => ({
|
|
6850
6866
|
platform,
|
|
@@ -7757,19 +7773,48 @@ async function scanCommand(targetPath, options) {
|
|
|
7757
7773
|
);
|
|
7758
7774
|
if (tokenData?.cliTier) {
|
|
7759
7775
|
const aiSpinner = ora2({
|
|
7760
|
-
text: chalk5.dim("Running AI-powered deep analysis..."),
|
|
7776
|
+
text: chalk5.dim("Running AI-powered deep analysis... (this may take 1-3 minutes)"),
|
|
7761
7777
|
color: "yellow",
|
|
7762
7778
|
spinner: "dots12"
|
|
7763
7779
|
}).start();
|
|
7780
|
+
const stages = [
|
|
7781
|
+
"Uploading files to AI scanner...",
|
|
7782
|
+
"Analyzing code with Claude AI...",
|
|
7783
|
+
"Detecting auth & logic vulnerabilities...",
|
|
7784
|
+
"Checking business logic flaws...",
|
|
7785
|
+
"Generating plain-English report..."
|
|
7786
|
+
];
|
|
7787
|
+
let stageIdx = 0;
|
|
7788
|
+
const progressInterval = setInterval(() => {
|
|
7789
|
+
stageIdx = Math.min(stageIdx + 1, stages.length - 1);
|
|
7790
|
+
aiSpinner.text = chalk5.dim(`${stages[stageIdx]} (${(stageIdx + 1) * 20}%)`);
|
|
7791
|
+
}, 15e3);
|
|
7764
7792
|
try {
|
|
7793
|
+
const MAX_AI_FILES = 100;
|
|
7794
|
+
const MAX_FILE_BYTES = 1e5;
|
|
7795
|
+
const MAX_TOTAL_BYTES = 4 * 1024 * 1024;
|
|
7796
|
+
let totalBytes = 0;
|
|
7797
|
+
const aiFiles = [];
|
|
7798
|
+
for (const f of files) {
|
|
7799
|
+
const size = new TextEncoder().encode(f.content).byteLength;
|
|
7800
|
+
if (size > MAX_FILE_BYTES) continue;
|
|
7801
|
+
if (totalBytes + size > MAX_TOTAL_BYTES) break;
|
|
7802
|
+
totalBytes += size;
|
|
7803
|
+
aiFiles.push(f);
|
|
7804
|
+
if (aiFiles.length >= MAX_AI_FILES) break;
|
|
7805
|
+
}
|
|
7806
|
+
aiSpinner.text = chalk5.dim(`Uploading ${aiFiles.length} files to AI scanner...`);
|
|
7765
7807
|
const aiRes = await fetch(`${options.apiUrl}/api/cli/ai-scan`, {
|
|
7766
7808
|
method: "POST",
|
|
7767
7809
|
headers: {
|
|
7768
7810
|
"Content-Type": "application/json",
|
|
7769
7811
|
Authorization: `Bearer ${tokenData.token}`
|
|
7770
7812
|
},
|
|
7771
|
-
body: JSON.stringify({ files })
|
|
7813
|
+
body: JSON.stringify({ files: aiFiles }),
|
|
7814
|
+
signal: AbortSignal.timeout(3e5)
|
|
7815
|
+
// 5 minute timeout
|
|
7772
7816
|
});
|
|
7817
|
+
clearInterval(progressInterval);
|
|
7773
7818
|
if (aiRes.ok) {
|
|
7774
7819
|
const aiData = await aiRes.json();
|
|
7775
7820
|
const aiFindings = aiData.findings.map((f) => ({
|
|
@@ -7805,8 +7850,9 @@ async function scanCommand(targetPath, options) {
|
|
|
7805
7850
|
`AI analysis: ${aiFindings.length} additional findings`
|
|
7806
7851
|
)
|
|
7807
7852
|
);
|
|
7853
|
+
const isUnlimited = !aiData.quota.limit || aiData.quota.limit < 0 || aiData.quota.limit >= 999999;
|
|
7808
7854
|
printInfo(
|
|
7809
|
-
`AI scans: ${aiData.quota.used}/${aiData.quota.limit} used this month (${aiData.quota.remaining} remaining)`
|
|
7855
|
+
isUnlimited ? `AI scans: ${aiData.quota.used} used this month (unlimited plan)` : `AI scans: ${aiData.quota.used}/${aiData.quota.limit} used this month (${aiData.quota.remaining} remaining)`
|
|
7810
7856
|
);
|
|
7811
7857
|
} else if (aiRes.status === 429) {
|
|
7812
7858
|
aiSpinner.warn(
|
|
@@ -7826,10 +7872,14 @@ async function scanCommand(targetPath, options) {
|
|
|
7826
7872
|
chalk5.yellow(`AI analysis unavailable (HTTP ${aiRes.status}). ${errBody || "Showing rule-based results only."}`)
|
|
7827
7873
|
);
|
|
7828
7874
|
}
|
|
7829
|
-
} catch {
|
|
7875
|
+
} catch (fetchErr) {
|
|
7876
|
+
clearInterval(progressInterval);
|
|
7877
|
+
const errMsg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
|
|
7830
7878
|
aiSpinner.warn(
|
|
7831
|
-
chalk5.yellow(
|
|
7879
|
+
chalk5.yellow(`Could not reach AI scanning service: ${errMsg}`)
|
|
7832
7880
|
);
|
|
7881
|
+
} finally {
|
|
7882
|
+
clearInterval(progressInterval);
|
|
7833
7883
|
}
|
|
7834
7884
|
}
|
|
7835
7885
|
const ignoredRules = loadIgnoredRules(resolvedPath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ship-safe/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.14",
|
|
4
4
|
"description": "Security scanner for AI-generated code — find vulnerabilities before you ship",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,6 +38,12 @@
|
|
|
38
38
|
"access": "public",
|
|
39
39
|
"registry": "https://registry.npmjs.org/"
|
|
40
40
|
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"dev": "tsup --watch",
|
|
44
|
+
"type-check": "tsc --noEmit",
|
|
45
|
+
"prepublishOnly": "pnpm run build"
|
|
46
|
+
},
|
|
41
47
|
"dependencies": {
|
|
42
48
|
"@anthropic-ai/sdk": "^0.39",
|
|
43
49
|
"@inquirer/prompts": "^8.3.2",
|
|
@@ -48,15 +54,10 @@
|
|
|
48
54
|
"yaml": "^2"
|
|
49
55
|
},
|
|
50
56
|
"devDependencies": {
|
|
57
|
+
"@shipsafe/scanner": "workspace:*",
|
|
58
|
+
"@shipsafe/shared": "workspace:*",
|
|
51
59
|
"@types/node": "^22",
|
|
52
60
|
"tsup": "^8",
|
|
53
|
-
"typescript": "^5.7"
|
|
54
|
-
"@shipsafe/scanner": "0.1.0",
|
|
55
|
-
"@shipsafe/shared": "0.1.0"
|
|
56
|
-
},
|
|
57
|
-
"scripts": {
|
|
58
|
-
"build": "tsup",
|
|
59
|
-
"dev": "tsup --watch",
|
|
60
|
-
"type-check": "tsc --noEmit"
|
|
61
|
+
"typescript": "^5.7"
|
|
61
62
|
}
|
|
62
|
-
}
|
|
63
|
+
}
|