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