@ship-safe/cli 1.0.2 → 1.1.0
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 +271 -148
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -11,8 +11,9 @@ import { Command, InvalidArgumentError, Option } from "commander";
|
|
|
11
11
|
// src/commands/scan.ts
|
|
12
12
|
import { resolve as resolve2, join as join4 } from "path";
|
|
13
13
|
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
14
|
-
import
|
|
14
|
+
import chalk5 from "chalk";
|
|
15
15
|
import ora2 from "ora";
|
|
16
|
+
import { select } from "@inquirer/prompts";
|
|
16
17
|
import { parse as parseYaml } from "yaml";
|
|
17
18
|
|
|
18
19
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
@@ -6903,144 +6904,215 @@ function collectFiles(rootDir, options) {
|
|
|
6903
6904
|
}
|
|
6904
6905
|
|
|
6905
6906
|
// src/lib/output.ts
|
|
6907
|
+
import chalk2 from "chalk";
|
|
6908
|
+
|
|
6909
|
+
// src/lib/banner.ts
|
|
6906
6910
|
import chalk from "chalk";
|
|
6907
|
-
var
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6911
|
+
var SHIELD = chalk.hex("#FF6B2B");
|
|
6912
|
+
var DIM_ORANGE = chalk.hex("#FF8C42");
|
|
6913
|
+
var BRAND2 = chalk.hex("#FF6B2B").bold;
|
|
6914
|
+
function printBanner() {
|
|
6915
|
+
console.log("");
|
|
6916
|
+
console.log(
|
|
6917
|
+
SHIELD(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E")
|
|
6918
|
+
);
|
|
6919
|
+
console.log(
|
|
6920
|
+
SHIELD(" \u2502") + chalk.bold.white(" \u{1F6E1}\uFE0F ShipSafe Security Scanner ") + SHIELD("\u2502")
|
|
6921
|
+
);
|
|
6922
|
+
console.log(
|
|
6923
|
+
SHIELD(" \u2502") + chalk.dim(" Ship fast. Ship safe. ") + SHIELD("\u2502")
|
|
6924
|
+
);
|
|
6925
|
+
console.log(
|
|
6926
|
+
SHIELD(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F")
|
|
6927
|
+
);
|
|
6928
|
+
console.log("");
|
|
6929
|
+
}
|
|
6930
|
+
function printDivider(width = 50) {
|
|
6931
|
+
console.log(chalk.dim(" " + "\u2500".repeat(width)));
|
|
6932
|
+
}
|
|
6933
|
+
function printError(text) {
|
|
6934
|
+
console.log(chalk.red(` \u2717 ${text}`));
|
|
6935
|
+
}
|
|
6936
|
+
function printInfo(text) {
|
|
6937
|
+
console.log(chalk.dim(` ${text}`));
|
|
6938
|
+
}
|
|
6939
|
+
function printScanSummaryBox(stats) {
|
|
6940
|
+
const label = chalk.dim;
|
|
6941
|
+
const value = chalk.white.bold;
|
|
6942
|
+
console.log("");
|
|
6943
|
+
printDivider();
|
|
6944
|
+
console.log("");
|
|
6945
|
+
console.log(
|
|
6946
|
+
` ${label("Files:")} ${value(String(stats.files))} ${chalk.dim("\xB7")} ${label("Lines:")} ${value(stats.lines.toLocaleString())} ${chalk.dim("\xB7")} ${label("Time:")} ${value(`${stats.duration}ms`)}`
|
|
6947
|
+
);
|
|
6948
|
+
console.log("");
|
|
6949
|
+
if (stats.findings === 0) {
|
|
6950
|
+
console.log(chalk.green.bold(" \u2713 No security issues found!"));
|
|
6951
|
+
} else {
|
|
6952
|
+
const parts = [];
|
|
6953
|
+
if (stats.critical > 0) parts.push(chalk.red.bold(`${stats.critical} critical`));
|
|
6954
|
+
if (stats.high > 0) parts.push(chalk.yellow.bold(`${stats.high} high`));
|
|
6955
|
+
if (stats.medium > 0) parts.push(chalk.hex("#ca8a04").bold(`${stats.medium} medium`));
|
|
6956
|
+
if (stats.low > 0) parts.push(chalk.blue(`${stats.low} low`));
|
|
6957
|
+
console.log(
|
|
6958
|
+
` ${chalk.bold.white(`${stats.findings} issues found:`)} ${parts.join(chalk.dim(" \xB7 "))}`
|
|
6959
|
+
);
|
|
6960
|
+
}
|
|
6961
|
+
console.log("");
|
|
6962
|
+
printDivider();
|
|
6963
|
+
}
|
|
6964
|
+
function printUpsellBox(criticalHigh) {
|
|
6965
|
+
console.log("");
|
|
6966
|
+
console.log(DIM_ORANGE(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
|
|
6967
|
+
console.log(
|
|
6968
|
+
DIM_ORANGE(" \u2502") + BRAND2(" \u{1F512} Unlock AI-Powered Deep Analysis ") + DIM_ORANGE("\u2502")
|
|
6969
|
+
);
|
|
6970
|
+
console.log(DIM_ORANGE(" \u2502 \u2502"));
|
|
6971
|
+
if (criticalHigh > 0) {
|
|
6972
|
+
console.log(
|
|
6973
|
+
DIM_ORANGE(" \u2502 ") + chalk.white(`Found ${criticalHigh} critical/high issues. AI finds`) + " ".repeat(Math.max(0, 46 - `Found ${criticalHigh} critical/high issues. AI finds`.length)) + DIM_ORANGE("\u2502")
|
|
6974
|
+
);
|
|
6975
|
+
console.log(
|
|
6976
|
+
DIM_ORANGE(" \u2502 ") + chalk.dim("auth logic flaws, business logic bugs & more.") + " ".repeat(3) + DIM_ORANGE("\u2502")
|
|
6977
|
+
);
|
|
6978
|
+
} else {
|
|
6979
|
+
console.log(
|
|
6980
|
+
DIM_ORANGE(" \u2502 ") + chalk.dim("Surface scan clean. AI finds auth logic,") + " ".repeat(8) + DIM_ORANGE("\u2502")
|
|
6981
|
+
);
|
|
6982
|
+
console.log(
|
|
6983
|
+
DIM_ORANGE(" \u2502 ") + chalk.dim("RLS policies & business logic bugs.") + " ".repeat(13) + DIM_ORANGE("\u2502")
|
|
6984
|
+
);
|
|
6985
|
+
}
|
|
6986
|
+
console.log(DIM_ORANGE(" \u2502 \u2502"));
|
|
6987
|
+
console.log(
|
|
6988
|
+
DIM_ORANGE(" \u2502 ") + chalk.white("\u2192 Run ") + BRAND2("shipsafe login") + chalk.white(" to get started") + " ".repeat(13) + DIM_ORANGE("\u2502")
|
|
6989
|
+
);
|
|
6990
|
+
console.log(DIM_ORANGE(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
6991
|
+
}
|
|
6992
|
+
|
|
6993
|
+
// src/lib/output.ts
|
|
6994
|
+
var SEVERITY_BADGE = {
|
|
6995
|
+
critical: chalk2.bgRed.white.bold(" CRITICAL "),
|
|
6996
|
+
high: chalk2.bgYellow.black.bold(" HIGH "),
|
|
6997
|
+
medium: chalk2.bgHex("#ca8a04").black.bold(" MEDIUM "),
|
|
6998
|
+
low: chalk2.bgBlue.white.bold(" LOW "),
|
|
6999
|
+
info: chalk2.bgGray.white(" INFO ")
|
|
6913
7000
|
};
|
|
6914
7001
|
var SEVERITY_COLOR = {
|
|
6915
|
-
critical:
|
|
6916
|
-
high:
|
|
6917
|
-
medium:
|
|
6918
|
-
low:
|
|
6919
|
-
info:
|
|
7002
|
+
critical: chalk2.red,
|
|
7003
|
+
high: chalk2.yellow,
|
|
7004
|
+
medium: chalk2.hex("#ca8a04"),
|
|
7005
|
+
low: chalk2.blue,
|
|
7006
|
+
info: chalk2.gray
|
|
6920
7007
|
};
|
|
6921
|
-
function formatFinding(finding
|
|
7008
|
+
function formatFinding(finding) {
|
|
6922
7009
|
const lines = [];
|
|
6923
7010
|
const color = SEVERITY_COLOR[finding.severity];
|
|
6924
7011
|
lines.push(
|
|
6925
|
-
`
|
|
6926
|
-
${SEVERITY_LABEL[finding.severity]} ${chalk.bold(finding.title)}`
|
|
7012
|
+
`${SEVERITY_BADGE[finding.severity]} ${chalk2.bold.white(finding.title)}`
|
|
6927
7013
|
);
|
|
6928
7014
|
lines.push(
|
|
6929
|
-
|
|
7015
|
+
chalk2.dim(" \u{1F4C4} ") + chalk2.cyan(finding.file) + chalk2.dim(`:${finding.line}`) + (finding.cwe ? chalk2.dim(` \xB7 ${finding.cwe}`) : "")
|
|
6930
7016
|
);
|
|
6931
|
-
lines.push(` ${finding.description}`);
|
|
7017
|
+
lines.push(` ${chalk2.dim(finding.description)}`);
|
|
6932
7018
|
if (finding.snippet) {
|
|
6933
7019
|
lines.push("");
|
|
6934
|
-
|
|
7020
|
+
const snippetLines = finding.snippet.split("\n");
|
|
7021
|
+
for (const snippetLine of snippetLines) {
|
|
6935
7022
|
if (snippetLine.startsWith(">")) {
|
|
6936
|
-
lines.push(` ${color(snippetLine)}`);
|
|
7023
|
+
lines.push(` ${color.bold(snippetLine)}`);
|
|
6937
7024
|
} else {
|
|
6938
|
-
lines.push(` ${
|
|
7025
|
+
lines.push(` ${chalk2.gray(snippetLine)}`);
|
|
6939
7026
|
}
|
|
6940
7027
|
}
|
|
6941
7028
|
}
|
|
6942
7029
|
lines.push("");
|
|
6943
|
-
lines.push(
|
|
7030
|
+
lines.push(chalk2.green(` \u{1F4A1} Fix: ${finding.fix.description}`));
|
|
6944
7031
|
if (finding.fix.suggestion) {
|
|
6945
|
-
lines.push(
|
|
7032
|
+
lines.push(chalk2.green.dim(` ${finding.fix.suggestion}`));
|
|
6946
7033
|
}
|
|
6947
7034
|
return lines.join("\n");
|
|
6948
7035
|
}
|
|
6949
7036
|
function formatTableOutput(result, options = {}) {
|
|
6950
7037
|
const lines = [];
|
|
6951
|
-
lines.push("");
|
|
6952
|
-
lines.push(chalk.bold.cyan(" ShipSafe Security Scan Results"));
|
|
6953
|
-
lines.push(chalk.gray(" " + "\u2500".repeat(50)));
|
|
6954
|
-
lines.push("");
|
|
6955
7038
|
const { summary } = result;
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
7039
|
+
printScanSummaryBox({
|
|
7040
|
+
files: summary.filesScanned,
|
|
7041
|
+
lines: summary.linesScanned,
|
|
7042
|
+
duration: result.durationMs,
|
|
7043
|
+
findings: summary.total,
|
|
7044
|
+
critical: summary.bySeverity.critical,
|
|
7045
|
+
high: summary.bySeverity.high,
|
|
7046
|
+
medium: summary.bySeverity.medium,
|
|
7047
|
+
low: summary.bySeverity.low
|
|
7048
|
+
});
|
|
6960
7049
|
if (summary.total === 0) {
|
|
6961
|
-
lines.push(chalk.green.bold(" No security issues found!"));
|
|
6962
7050
|
if (!options.isLoggedIn) {
|
|
6963
|
-
lines.push("");
|
|
6964
7051
|
lines.push(
|
|
6965
|
-
|
|
6966
|
-
`
|
|
7052
|
+
chalk2.dim(
|
|
7053
|
+
` Run ${chalk2.bold.white("shipsafe login")} to unlock deep AI analysis.`
|
|
6967
7054
|
)
|
|
6968
7055
|
);
|
|
7056
|
+
lines.push("");
|
|
6969
7057
|
}
|
|
6970
|
-
lines.push("");
|
|
6971
7058
|
return lines.join("\n");
|
|
6972
7059
|
}
|
|
6973
|
-
const severityLine = [
|
|
6974
|
-
summary.bySeverity.critical > 0 ? chalk.red.bold(`${summary.bySeverity.critical} critical`) : null,
|
|
6975
|
-
summary.bySeverity.high > 0 ? chalk.yellow.bold(`${summary.bySeverity.high} high`) : null,
|
|
6976
|
-
summary.bySeverity.medium > 0 ? chalk.hex("#ca8a04").bold(`${summary.bySeverity.medium} medium`) : null,
|
|
6977
|
-
summary.bySeverity.low > 0 ? chalk.blue(`${summary.bySeverity.low} low`) : null,
|
|
6978
|
-
summary.bySeverity.info > 0 ? chalk.gray(`${summary.bySeverity.info} info`) : null
|
|
6979
|
-
].filter(Boolean).join(" | ");
|
|
6980
|
-
lines.push(` ${chalk.bold("Findings:")} ${summary.total} (${severityLine})`);
|
|
6981
7060
|
const sorted = [...result.findings].sort(
|
|
6982
7061
|
(a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]
|
|
6983
7062
|
);
|
|
6984
|
-
|
|
6985
|
-
|
|
7063
|
+
let currentSeverity = null;
|
|
7064
|
+
let countInSeverity = 0;
|
|
7065
|
+
for (const finding of sorted) {
|
|
7066
|
+
if (finding.severity !== currentSeverity) {
|
|
7067
|
+
if (currentSeverity !== null) {
|
|
7068
|
+
lines.push("");
|
|
7069
|
+
}
|
|
7070
|
+
currentSeverity = finding.severity;
|
|
7071
|
+
countInSeverity = sorted.filter(
|
|
7072
|
+
(f) => f.severity === currentSeverity
|
|
7073
|
+
).length;
|
|
7074
|
+
lines.push(
|
|
7075
|
+
chalk2.dim(" \u2500\u2500\u2500\u2500 ") + SEVERITY_COLOR[currentSeverity].bold(
|
|
7076
|
+
`${currentSeverity.toUpperCase()} (${countInSeverity})`
|
|
7077
|
+
) + chalk2.dim(" " + "\u2500".repeat(35))
|
|
7078
|
+
);
|
|
7079
|
+
lines.push("");
|
|
7080
|
+
}
|
|
7081
|
+
lines.push(formatFinding(finding));
|
|
7082
|
+
lines.push("");
|
|
6986
7083
|
}
|
|
6987
7084
|
if (options.diff) {
|
|
6988
7085
|
const { newFindings, resolvedFindings } = options.diff;
|
|
6989
7086
|
if (newFindings.length > 0 || resolvedFindings.length > 0) {
|
|
6990
|
-
|
|
6991
|
-
lines.push(
|
|
7087
|
+
printDivider();
|
|
7088
|
+
lines.push(chalk2.bold.white(" \u{1F4CA} Changes since last scan:"));
|
|
6992
7089
|
if (newFindings.length > 0) {
|
|
6993
7090
|
lines.push(
|
|
6994
|
-
|
|
7091
|
+
chalk2.red(
|
|
7092
|
+
` \u2191 ${newFindings.length} new issue${newFindings.length === 1 ? "" : "s"} introduced`
|
|
7093
|
+
)
|
|
6995
7094
|
);
|
|
6996
7095
|
}
|
|
6997
7096
|
if (resolvedFindings.length > 0) {
|
|
6998
7097
|
lines.push(
|
|
6999
|
-
|
|
7098
|
+
chalk2.green(
|
|
7099
|
+
` \u2193 ${resolvedFindings.length} issue${resolvedFindings.length === 1 ? "" : "s"} resolved`
|
|
7100
|
+
)
|
|
7000
7101
|
);
|
|
7001
7102
|
}
|
|
7002
7103
|
}
|
|
7003
7104
|
}
|
|
7004
7105
|
if (options.suppressedCount && options.suppressedCount > 0) {
|
|
7005
7106
|
lines.push(
|
|
7006
|
-
|
|
7007
|
-
|
|
7107
|
+
chalk2.dim(
|
|
7108
|
+
`
|
|
7109
|
+
${options.suppressedCount} finding${options.suppressedCount === 1 ? "" : "s"} suppressed via .shipsafeignore`
|
|
7110
|
+
)
|
|
7008
7111
|
);
|
|
7009
7112
|
}
|
|
7010
|
-
lines.push("");
|
|
7011
|
-
lines.push(chalk.gray(" " + "\u2500".repeat(50)));
|
|
7012
7113
|
if (!options.isLoggedIn && summary.total > 0) {
|
|
7013
7114
|
const criticalHigh = summary.bySeverity.critical + summary.bySeverity.high;
|
|
7014
|
-
|
|
7015
|
-
if (criticalHigh > 0) {
|
|
7016
|
-
lines.push(
|
|
7017
|
-
chalk.yellow(
|
|
7018
|
-
` Found ${criticalHigh} critical/high issue${criticalHigh === 1 ? "" : "s"}. Deep AI analysis can find auth logic flaws,`
|
|
7019
|
-
)
|
|
7020
|
-
);
|
|
7021
|
-
lines.push(
|
|
7022
|
-
chalk.yellow(
|
|
7023
|
-
` business logic bugs, and more \u2014 run ${chalk.bold("shipsafe login")} to unlock.`
|
|
7024
|
-
)
|
|
7025
|
-
);
|
|
7026
|
-
} else {
|
|
7027
|
-
lines.push(
|
|
7028
|
-
chalk.gray(
|
|
7029
|
-
` Surface scan clean. Run ${chalk.bold.white("shipsafe login")} for deep AI analysis`
|
|
7030
|
-
)
|
|
7031
|
-
);
|
|
7032
|
-
lines.push(
|
|
7033
|
-
chalk.gray(
|
|
7034
|
-
` covering auth logic, RLS policies, and business logic review.`
|
|
7035
|
-
)
|
|
7036
|
-
);
|
|
7037
|
-
}
|
|
7038
|
-
} else if (!options.isLoggedIn && summary.total === 0) {
|
|
7039
|
-
lines.push(
|
|
7040
|
-
chalk.gray(
|
|
7041
|
-
` No issues in surface scan. Run ${chalk.bold.white("shipsafe login")} to unlock deep AI analysis.`
|
|
7042
|
-
)
|
|
7043
|
-
);
|
|
7115
|
+
printUpsellBox(criticalHigh);
|
|
7044
7116
|
}
|
|
7045
7117
|
lines.push("");
|
|
7046
7118
|
return lines.join("\n");
|
|
@@ -7089,7 +7161,7 @@ function formatSarifOutput(result) {
|
|
|
7089
7161
|
}
|
|
7090
7162
|
|
|
7091
7163
|
// src/commands/login.ts
|
|
7092
|
-
import
|
|
7164
|
+
import chalk3 from "chalk";
|
|
7093
7165
|
import ora from "ora";
|
|
7094
7166
|
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync as existsSync2, unlinkSync } from "fs";
|
|
7095
7167
|
import { homedir } from "os";
|
|
@@ -7101,7 +7173,7 @@ function getStoredToken() {
|
|
|
7101
7173
|
if (!existsSync2(TOKEN_FILE)) return null;
|
|
7102
7174
|
const data = JSON.parse(readFileSync2(TOKEN_FILE, "utf-8"));
|
|
7103
7175
|
if (data.expiresAt < Date.now()) {
|
|
7104
|
-
console.log(
|
|
7176
|
+
console.log(chalk3.yellow("Session expired. Please login again."));
|
|
7105
7177
|
return null;
|
|
7106
7178
|
}
|
|
7107
7179
|
return data;
|
|
@@ -7128,7 +7200,7 @@ async function fetchProfile(apiUrl, rawToken) {
|
|
|
7128
7200
|
}
|
|
7129
7201
|
async function loginCommand(options) {
|
|
7130
7202
|
const apiUrl = options.apiUrl || "https://ship-safe.co";
|
|
7131
|
-
console.log(
|
|
7203
|
+
console.log(chalk3.bold("\nShipSafe CLI Login\n"));
|
|
7132
7204
|
const spinner = ora("Requesting device code...").start();
|
|
7133
7205
|
try {
|
|
7134
7206
|
const res = await fetch(`${apiUrl}/api/cli/device-code`, {
|
|
@@ -7138,7 +7210,7 @@ async function loginCommand(options) {
|
|
|
7138
7210
|
if (!res.ok) {
|
|
7139
7211
|
spinner.fail("Failed to request device code");
|
|
7140
7212
|
console.log(
|
|
7141
|
-
|
|
7213
|
+
chalk3.red(
|
|
7142
7214
|
"Could not connect to ShipSafe. Make sure the server is running."
|
|
7143
7215
|
)
|
|
7144
7216
|
);
|
|
@@ -7146,10 +7218,10 @@ async function loginCommand(options) {
|
|
|
7146
7218
|
}
|
|
7147
7219
|
const { deviceCode, userCode, verificationUrl, expiresIn } = await res.json();
|
|
7148
7220
|
spinner.stop();
|
|
7149
|
-
console.log(
|
|
7150
|
-
console.log(
|
|
7221
|
+
console.log(chalk3.bold("To login, open this URL in your browser:\n"));
|
|
7222
|
+
console.log(chalk3.cyan(` ${verificationUrl}
|
|
7151
7223
|
`));
|
|
7152
|
-
console.log(`Enter this code: ${
|
|
7224
|
+
console.log(`Enter this code: ${chalk3.bold.green(userCode)}
|
|
7153
7225
|
`);
|
|
7154
7226
|
const pollSpinner = ora("Waiting for authorization...").start();
|
|
7155
7227
|
const startTime = Date.now();
|
|
@@ -7180,17 +7252,17 @@ async function loginCommand(options) {
|
|
|
7180
7252
|
}
|
|
7181
7253
|
storeToken(tokenData);
|
|
7182
7254
|
pollSpinner.succeed(
|
|
7183
|
-
|
|
7255
|
+
chalk3.green(`Logged in as ${chalk3.bold(pollData.email)}`)
|
|
7184
7256
|
);
|
|
7185
7257
|
if (profile?.cliTier) {
|
|
7186
7258
|
console.log(
|
|
7187
|
-
|
|
7259
|
+
chalk3.cyan(
|
|
7188
7260
|
` CLI plan: ${profile.cliTier} (${profile.aiQuota.remaining} AI scans remaining)`
|
|
7189
7261
|
)
|
|
7190
7262
|
);
|
|
7191
7263
|
} else {
|
|
7192
7264
|
console.log(
|
|
7193
|
-
|
|
7265
|
+
chalk3.dim(
|
|
7194
7266
|
" No CLI plan. Upgrade at ship-safe.co/pricing for AI-powered scanning."
|
|
7195
7267
|
)
|
|
7196
7268
|
);
|
|
@@ -7199,7 +7271,7 @@ async function loginCommand(options) {
|
|
|
7199
7271
|
}
|
|
7200
7272
|
if (pollData.status === "expired") {
|
|
7201
7273
|
pollSpinner.fail(
|
|
7202
|
-
|
|
7274
|
+
chalk3.yellow(
|
|
7203
7275
|
"Your login link expired. Run `shipsafe login` again."
|
|
7204
7276
|
)
|
|
7205
7277
|
);
|
|
@@ -7209,7 +7281,7 @@ async function loginCommand(options) {
|
|
|
7209
7281
|
networkErrors++;
|
|
7210
7282
|
if (networkErrors >= 3) {
|
|
7211
7283
|
pollSpinner.fail(
|
|
7212
|
-
|
|
7284
|
+
chalk3.red(
|
|
7213
7285
|
"Lost connection to ShipSafe. Check your internet and run `shipsafe login` again."
|
|
7214
7286
|
)
|
|
7215
7287
|
);
|
|
@@ -7219,7 +7291,7 @@ async function loginCommand(options) {
|
|
|
7219
7291
|
}
|
|
7220
7292
|
}
|
|
7221
7293
|
pollSpinner.fail(
|
|
7222
|
-
|
|
7294
|
+
chalk3.yellow(
|
|
7223
7295
|
"Your login link expired. Run `shipsafe login` again."
|
|
7224
7296
|
)
|
|
7225
7297
|
);
|
|
@@ -7227,13 +7299,13 @@ async function loginCommand(options) {
|
|
|
7227
7299
|
spinner.fail("Could not connect to ShipSafe");
|
|
7228
7300
|
if (error instanceof Error && (error.message.includes("fetch") || error.message.includes("ECONNREFUSED"))) {
|
|
7229
7301
|
console.log(
|
|
7230
|
-
|
|
7302
|
+
chalk3.red(
|
|
7231
7303
|
"Make sure you have an internet connection and try again."
|
|
7232
7304
|
)
|
|
7233
7305
|
);
|
|
7234
7306
|
} else {
|
|
7235
7307
|
console.log(
|
|
7236
|
-
|
|
7308
|
+
chalk3.red(
|
|
7237
7309
|
error instanceof Error ? error.message : "Unknown error \u2014 try again."
|
|
7238
7310
|
)
|
|
7239
7311
|
);
|
|
@@ -7244,18 +7316,18 @@ async function logoutCommand() {
|
|
|
7244
7316
|
try {
|
|
7245
7317
|
if (existsSync2(TOKEN_FILE)) {
|
|
7246
7318
|
unlinkSync(TOKEN_FILE);
|
|
7247
|
-
console.log(
|
|
7319
|
+
console.log(chalk3.green("Logged out successfully."));
|
|
7248
7320
|
} else {
|
|
7249
|
-
console.log(
|
|
7321
|
+
console.log(chalk3.yellow("Not currently logged in."));
|
|
7250
7322
|
}
|
|
7251
7323
|
} catch {
|
|
7252
|
-
console.log(
|
|
7324
|
+
console.log(chalk3.red("Failed to logout."));
|
|
7253
7325
|
}
|
|
7254
7326
|
}
|
|
7255
7327
|
async function whoamiCommand(options) {
|
|
7256
7328
|
const token = getStoredToken();
|
|
7257
7329
|
if (!token) {
|
|
7258
|
-
console.log(
|
|
7330
|
+
console.log(chalk3.yellow("Not logged in. Run `shipsafe login` to login."));
|
|
7259
7331
|
return;
|
|
7260
7332
|
}
|
|
7261
7333
|
const apiUrl = options.apiUrl || "https://ship-safe.co";
|
|
@@ -7269,26 +7341,26 @@ async function whoamiCommand(options) {
|
|
|
7269
7341
|
aiQuota: profile.aiQuota
|
|
7270
7342
|
});
|
|
7271
7343
|
spinner.stop();
|
|
7272
|
-
console.log(
|
|
7273
|
-
console.log(` Email: ${
|
|
7274
|
-
console.log(` Web tier: ${
|
|
7344
|
+
console.log(chalk3.bold("\nShipSafe Account\n"));
|
|
7345
|
+
console.log(` Email: ${chalk3.cyan(profile.email)}`);
|
|
7346
|
+
console.log(` Web tier: ${chalk3.cyan(profile.tier)}`);
|
|
7275
7347
|
if (profile.cliTier) {
|
|
7276
|
-
console.log(` CLI plan: ${
|
|
7348
|
+
console.log(` CLI plan: ${chalk3.green(profile.cliTier)}`);
|
|
7277
7349
|
console.log(
|
|
7278
|
-
` AI scans: ${
|
|
7350
|
+
` AI scans: ${chalk3.cyan(`${profile.aiQuota.used}/${profile.aiQuota.limit}`)} used this month (${chalk3.green(`${profile.aiQuota.remaining}`)} remaining)`
|
|
7279
7351
|
);
|
|
7280
7352
|
} else {
|
|
7281
|
-
console.log(` CLI plan: ${
|
|
7353
|
+
console.log(` CLI plan: ${chalk3.dim("none")}`);
|
|
7282
7354
|
console.log(
|
|
7283
|
-
|
|
7355
|
+
chalk3.dim(" Upgrade at ship-safe.co/pricing for AI-powered scanning.")
|
|
7284
7356
|
);
|
|
7285
7357
|
}
|
|
7286
7358
|
console.log();
|
|
7287
7359
|
} else {
|
|
7288
7360
|
spinner.stop();
|
|
7289
|
-
console.log(
|
|
7361
|
+
console.log(chalk3.green(`Logged in as ${chalk3.bold(token.email)}`));
|
|
7290
7362
|
if (token.cliTier) {
|
|
7291
|
-
console.log(
|
|
7363
|
+
console.log(chalk3.cyan(` CLI plan: ${token.cliTier}`));
|
|
7292
7364
|
}
|
|
7293
7365
|
}
|
|
7294
7366
|
}
|
|
@@ -7296,7 +7368,7 @@ async function whoamiCommand(options) {
|
|
|
7296
7368
|
// src/commands/ignore.ts
|
|
7297
7369
|
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, appendFileSync } from "fs";
|
|
7298
7370
|
import { resolve, join as join2 } from "path";
|
|
7299
|
-
import
|
|
7371
|
+
import chalk4 from "chalk";
|
|
7300
7372
|
var IGNORE_FILE = ".shipsafeignore";
|
|
7301
7373
|
function getIgnorePath(dir) {
|
|
7302
7374
|
return resolve(dir || ".", IGNORE_FILE);
|
|
@@ -7320,7 +7392,7 @@ function ignoreCommand(ruleId, options) {
|
|
|
7320
7392
|
if (existsSync3(ignorePath)) {
|
|
7321
7393
|
const existing = loadIgnoredRules(".");
|
|
7322
7394
|
if (existing.has(ruleId)) {
|
|
7323
|
-
console.log(
|
|
7395
|
+
console.log(chalk4.yellow(`Rule "${ruleId}" is already in ${IGNORE_FILE}`));
|
|
7324
7396
|
return;
|
|
7325
7397
|
}
|
|
7326
7398
|
}
|
|
@@ -7341,18 +7413,18 @@ ${line}
|
|
|
7341
7413
|
`
|
|
7342
7414
|
);
|
|
7343
7415
|
}
|
|
7344
|
-
console.log(
|
|
7416
|
+
console.log(chalk4.green(`Added "${ruleId}" to ${IGNORE_FILE}`));
|
|
7345
7417
|
if (options.reason) {
|
|
7346
|
-
console.log(
|
|
7418
|
+
console.log(chalk4.gray(` Reason: ${options.reason}`));
|
|
7347
7419
|
}
|
|
7348
7420
|
console.log(
|
|
7349
|
-
|
|
7421
|
+
chalk4.gray(` This finding will be suppressed in future scans.`)
|
|
7350
7422
|
);
|
|
7351
7423
|
}
|
|
7352
7424
|
function unignoreCommand(ruleId) {
|
|
7353
7425
|
const ignorePath = getIgnorePath();
|
|
7354
7426
|
if (!existsSync3(ignorePath)) {
|
|
7355
|
-
console.log(
|
|
7427
|
+
console.log(chalk4.yellow(`No ${IGNORE_FILE} found.`));
|
|
7356
7428
|
return;
|
|
7357
7429
|
}
|
|
7358
7430
|
const content = readFileSync3(ignorePath, "utf-8");
|
|
@@ -7364,11 +7436,11 @@ function unignoreCommand(ruleId) {
|
|
|
7364
7436
|
return lineRuleId !== ruleId;
|
|
7365
7437
|
});
|
|
7366
7438
|
if (filtered.length === lines.length) {
|
|
7367
|
-
console.log(
|
|
7439
|
+
console.log(chalk4.yellow(`Rule "${ruleId}" is not in ${IGNORE_FILE}`));
|
|
7368
7440
|
return;
|
|
7369
7441
|
}
|
|
7370
7442
|
writeFileSync2(ignorePath, filtered.join("\n"));
|
|
7371
|
-
console.log(
|
|
7443
|
+
console.log(chalk4.green(`Removed "${ruleId}" from ${IGNORE_FILE}`));
|
|
7372
7444
|
}
|
|
7373
7445
|
|
|
7374
7446
|
// src/lib/scan-history.ts
|
|
@@ -7453,28 +7525,81 @@ function loadConfig(dir) {
|
|
|
7453
7525
|
async function scanCommand(targetPath, options) {
|
|
7454
7526
|
const resolvedPath = resolve2(targetPath);
|
|
7455
7527
|
if (!existsSync5(resolvedPath)) {
|
|
7456
|
-
|
|
7528
|
+
printError(`Path "${targetPath}" does not exist.`);
|
|
7457
7529
|
process.exit(1);
|
|
7458
7530
|
}
|
|
7531
|
+
if (options.output === "table") {
|
|
7532
|
+
printBanner();
|
|
7533
|
+
}
|
|
7534
|
+
let tokenData = getStoredToken();
|
|
7535
|
+
if (!tokenData && !options.ci && options.output === "table") {
|
|
7536
|
+
const choice = await select({
|
|
7537
|
+
message: chalk5.white("How would you like to scan?"),
|
|
7538
|
+
choices: [
|
|
7539
|
+
{
|
|
7540
|
+
name: `${chalk5.green("\u25B8")} ${chalk5.bold("Free scan")} ${chalk5.dim("\u2014 rule-based analysis, no account needed")}`,
|
|
7541
|
+
value: "free"
|
|
7542
|
+
},
|
|
7543
|
+
{
|
|
7544
|
+
name: `${chalk5.hex("#FF6B2B")("\u25B8")} ${chalk5.bold("Login first")} ${chalk5.dim("\u2014 unlock AI-powered deep analysis")}`,
|
|
7545
|
+
value: "login"
|
|
7546
|
+
}
|
|
7547
|
+
],
|
|
7548
|
+
theme: {
|
|
7549
|
+
prefix: { idle: chalk5.hex("#FF6B2B")(" \u{1F6E1}\uFE0F"), done: chalk5.green(" \u2713") },
|
|
7550
|
+
style: {
|
|
7551
|
+
highlight: (text) => chalk5.hex("#FF6B2B")(text)
|
|
7552
|
+
}
|
|
7553
|
+
}
|
|
7554
|
+
});
|
|
7555
|
+
if (choice === "login") {
|
|
7556
|
+
await loginCommand({ apiUrl: options.apiUrl });
|
|
7557
|
+
tokenData = getStoredToken();
|
|
7558
|
+
if (!tokenData) {
|
|
7559
|
+
printInfo("Login skipped. Running free scan...\n");
|
|
7560
|
+
} else {
|
|
7561
|
+
console.log("");
|
|
7562
|
+
}
|
|
7563
|
+
} else {
|
|
7564
|
+
console.log("");
|
|
7565
|
+
}
|
|
7566
|
+
}
|
|
7459
7567
|
const config = loadConfig(resolvedPath);
|
|
7460
7568
|
const severity = options.severity || config.severity || "low";
|
|
7461
|
-
const spinner = ora2(
|
|
7569
|
+
const spinner = ora2({
|
|
7570
|
+
text: chalk5.dim("Collecting files..."),
|
|
7571
|
+
color: "yellow",
|
|
7572
|
+
spinner: "dots12"
|
|
7573
|
+
}).start();
|
|
7462
7574
|
const files = collectFiles(resolvedPath, { exclude: config.exclude });
|
|
7463
7575
|
if (files.length === 0) {
|
|
7464
|
-
spinner.fail("No supported files found to scan.");
|
|
7576
|
+
spinner.fail(chalk5.red("No supported files found to scan."));
|
|
7465
7577
|
process.exit(1);
|
|
7466
7578
|
}
|
|
7467
|
-
|
|
7579
|
+
const totalLines = files.reduce(
|
|
7580
|
+
(sum, f) => sum + f.content.split("\n").length,
|
|
7581
|
+
0
|
|
7582
|
+
);
|
|
7583
|
+
spinner.text = chalk5.dim(
|
|
7584
|
+
`Scanning ${chalk5.white.bold(String(files.length))} files (${chalk5.white.bold(totalLines.toLocaleString())} lines)...`
|
|
7585
|
+
);
|
|
7468
7586
|
const result = await scan(
|
|
7469
7587
|
{ files, tier: "free" },
|
|
7470
7588
|
(progress) => {
|
|
7471
|
-
spinner.text =
|
|
7589
|
+
spinner.text = chalk5.dim(
|
|
7590
|
+
`${progress.stage} ${chalk5.white(`(${progress.findingsCount} findings)`)}`
|
|
7591
|
+
);
|
|
7472
7592
|
}
|
|
7473
7593
|
);
|
|
7474
|
-
spinner.
|
|
7475
|
-
|
|
7594
|
+
spinner.succeed(
|
|
7595
|
+
chalk5.green(`Scan complete \u2014 ${result.findings.length} findings in ${result.durationMs}ms`)
|
|
7596
|
+
);
|
|
7476
7597
|
if (tokenData?.cliTier) {
|
|
7477
|
-
const aiSpinner = ora2(
|
|
7598
|
+
const aiSpinner = ora2({
|
|
7599
|
+
text: chalk5.dim("Running AI-powered deep analysis..."),
|
|
7600
|
+
color: "yellow",
|
|
7601
|
+
spinner: "dots12"
|
|
7602
|
+
}).start();
|
|
7478
7603
|
try {
|
|
7479
7604
|
const aiRes = await fetch(`${options.apiUrl}/api/cli/ai-scan`, {
|
|
7480
7605
|
method: "POST",
|
|
@@ -7515,31 +7640,29 @@ async function scanCommand(targetPath, options) {
|
|
|
7515
7640
|
info: result.findings.filter((f) => f.severity === "info").length
|
|
7516
7641
|
};
|
|
7517
7642
|
aiSpinner.succeed(
|
|
7518
|
-
|
|
7519
|
-
`AI analysis
|
|
7643
|
+
chalk5.green(
|
|
7644
|
+
`AI analysis: ${aiFindings.length} additional findings`
|
|
7520
7645
|
)
|
|
7521
7646
|
);
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
` AI scans: ${aiData.quota.used}/${aiData.quota.limit} used this month (${aiData.quota.remaining} remaining)`
|
|
7525
|
-
)
|
|
7647
|
+
printInfo(
|
|
7648
|
+
`AI scans: ${aiData.quota.used}/${aiData.quota.limit} used this month (${aiData.quota.remaining} remaining)`
|
|
7526
7649
|
);
|
|
7527
7650
|
} else if (aiRes.status === 429) {
|
|
7528
7651
|
aiSpinner.warn(
|
|
7529
|
-
|
|
7652
|
+
chalk5.yellow("AI scan quota reached. Showing rule-based results only.")
|
|
7530
7653
|
);
|
|
7531
7654
|
} else if (aiRes.status === 401) {
|
|
7532
7655
|
aiSpinner.warn(
|
|
7533
|
-
|
|
7656
|
+
chalk5.yellow("Session expired. Run `shipsafe login` to re-authenticate.")
|
|
7534
7657
|
);
|
|
7535
7658
|
} else {
|
|
7536
7659
|
aiSpinner.warn(
|
|
7537
|
-
|
|
7660
|
+
chalk5.yellow("AI analysis unavailable. Showing rule-based results only.")
|
|
7538
7661
|
);
|
|
7539
7662
|
}
|
|
7540
7663
|
} catch {
|
|
7541
7664
|
aiSpinner.warn(
|
|
7542
|
-
|
|
7665
|
+
chalk5.yellow("Could not reach AI scanning service. Showing rule-based results only.")
|
|
7543
7666
|
);
|
|
7544
7667
|
}
|
|
7545
7668
|
}
|
|
@@ -7587,7 +7710,11 @@ async function scanCommand(targetPath, options) {
|
|
|
7587
7710
|
}
|
|
7588
7711
|
const token = getStoredToken();
|
|
7589
7712
|
if (token && options.upload) {
|
|
7590
|
-
const uploadSpinner = ora2(
|
|
7713
|
+
const uploadSpinner = ora2({
|
|
7714
|
+
text: chalk5.dim("Syncing results to dashboard..."),
|
|
7715
|
+
color: "yellow",
|
|
7716
|
+
spinner: "dots12"
|
|
7717
|
+
}).start();
|
|
7591
7718
|
try {
|
|
7592
7719
|
const apiFindings = result.findings.map((f) => ({
|
|
7593
7720
|
ruleId: f.ruleId,
|
|
@@ -7620,14 +7747,10 @@ async function scanCommand(targetPath, options) {
|
|
|
7620
7747
|
if (res.ok) {
|
|
7621
7748
|
const data = await res.json();
|
|
7622
7749
|
uploadSpinner.succeed(
|
|
7623
|
-
|
|
7750
|
+
chalk5.green(`Results synced: ${data.dashboardUrl || "view in dashboard"}`)
|
|
7624
7751
|
);
|
|
7625
7752
|
if (data.deepScanStatus === "running") {
|
|
7626
|
-
|
|
7627
|
-
chalk4.cyan(
|
|
7628
|
-
" Deep AI analysis running \u2014 check dashboard for updates."
|
|
7629
|
-
)
|
|
7630
|
-
);
|
|
7753
|
+
printInfo("Deep AI analysis running \u2014 check dashboard for updates.");
|
|
7631
7754
|
}
|
|
7632
7755
|
} else if (res.status === 401) {
|
|
7633
7756
|
uploadSpinner.warn(
|
|
@@ -7643,7 +7766,7 @@ async function scanCommand(targetPath, options) {
|
|
|
7643
7766
|
}
|
|
7644
7767
|
} else if (options.upload) {
|
|
7645
7768
|
console.log(
|
|
7646
|
-
|
|
7769
|
+
chalk5.yellow(
|
|
7647
7770
|
"\nNot logged in. Run `shipsafe login` first to sync results."
|
|
7648
7771
|
)
|
|
7649
7772
|
);
|
|
@@ -7661,7 +7784,7 @@ async function scanCommand(targetPath, options) {
|
|
|
7661
7784
|
// src/commands/init.ts
|
|
7662
7785
|
import { writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
|
|
7663
7786
|
import { resolve as resolve3 } from "path";
|
|
7664
|
-
import
|
|
7787
|
+
import chalk6 from "chalk";
|
|
7665
7788
|
var DEFAULT_CONFIG = `# ShipSafe Configuration
|
|
7666
7789
|
# https://ship-safe.co/docs/config
|
|
7667
7790
|
|
|
@@ -7692,11 +7815,11 @@ severity: low
|
|
|
7692
7815
|
function initCommand() {
|
|
7693
7816
|
const configPath = resolve3(".shipsafe.yml");
|
|
7694
7817
|
if (existsSync6(configPath)) {
|
|
7695
|
-
console.log(
|
|
7818
|
+
console.log(chalk6.yellow("A .shipsafe.yml already exists in this directory."));
|
|
7696
7819
|
return;
|
|
7697
7820
|
}
|
|
7698
7821
|
writeFileSync4(configPath, DEFAULT_CONFIG, "utf-8");
|
|
7699
|
-
console.log(
|
|
7822
|
+
console.log(chalk6.green("Created .shipsafe.yml configuration file."));
|
|
7700
7823
|
}
|
|
7701
7824
|
|
|
7702
7825
|
// src/index.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ship-safe/cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Security scanner for AI-generated code — find vulnerabilities before you ship",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@anthropic-ai/sdk": "^0.39",
|
|
43
|
+
"@inquirer/prompts": "^8.3.2",
|
|
43
44
|
"chalk": "^5",
|
|
44
45
|
"commander": "^13",
|
|
45
46
|
"ignore": "^7",
|
|
@@ -50,8 +51,8 @@
|
|
|
50
51
|
"@types/node": "^22",
|
|
51
52
|
"tsup": "^8",
|
|
52
53
|
"typescript": "^5.7",
|
|
53
|
-
"@shipsafe/
|
|
54
|
-
"@shipsafe/
|
|
54
|
+
"@shipsafe/scanner": "0.1.0",
|
|
55
|
+
"@shipsafe/shared": "0.1.0"
|
|
55
56
|
},
|
|
56
57
|
"scripts": {
|
|
57
58
|
"build": "tsup",
|