@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.
Files changed (2) hide show
  1. package/dist/index.js +60 -10
  2. 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 docPaths = /(?:\/docs\/|\/examples\/|\/fixtures\/|__tests__\/fixtures)/i;
5936
- if (docPaths.test(file.path)) return [];
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("Could not reach AI scanning service. Showing rule-based results only.")
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.13",
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
+ }