@ship-safe/cli 1.0.1 → 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 +277 -149
- 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
|
|
@@ -6873,7 +6874,12 @@ function collectFiles(rootDir, options) {
|
|
|
6873
6874
|
ig.add(options.exclude);
|
|
6874
6875
|
}
|
|
6875
6876
|
function walk(dir) {
|
|
6876
|
-
|
|
6877
|
+
let entries;
|
|
6878
|
+
try {
|
|
6879
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
6880
|
+
} catch {
|
|
6881
|
+
return;
|
|
6882
|
+
}
|
|
6877
6883
|
for (const entry of entries) {
|
|
6878
6884
|
if (entry.isSymbolicLink()) continue;
|
|
6879
6885
|
const fullPath = path2.join(dir, entry.name);
|
|
@@ -6898,144 +6904,215 @@ function collectFiles(rootDir, options) {
|
|
|
6898
6904
|
}
|
|
6899
6905
|
|
|
6900
6906
|
// src/lib/output.ts
|
|
6907
|
+
import chalk2 from "chalk";
|
|
6908
|
+
|
|
6909
|
+
// src/lib/banner.ts
|
|
6901
6910
|
import chalk from "chalk";
|
|
6902
|
-
var
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
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 ")
|
|
6908
7000
|
};
|
|
6909
7001
|
var SEVERITY_COLOR = {
|
|
6910
|
-
critical:
|
|
6911
|
-
high:
|
|
6912
|
-
medium:
|
|
6913
|
-
low:
|
|
6914
|
-
info:
|
|
7002
|
+
critical: chalk2.red,
|
|
7003
|
+
high: chalk2.yellow,
|
|
7004
|
+
medium: chalk2.hex("#ca8a04"),
|
|
7005
|
+
low: chalk2.blue,
|
|
7006
|
+
info: chalk2.gray
|
|
6915
7007
|
};
|
|
6916
|
-
function formatFinding(finding
|
|
7008
|
+
function formatFinding(finding) {
|
|
6917
7009
|
const lines = [];
|
|
6918
7010
|
const color = SEVERITY_COLOR[finding.severity];
|
|
6919
7011
|
lines.push(
|
|
6920
|
-
`
|
|
6921
|
-
${SEVERITY_LABEL[finding.severity]} ${chalk.bold(finding.title)}`
|
|
7012
|
+
`${SEVERITY_BADGE[finding.severity]} ${chalk2.bold.white(finding.title)}`
|
|
6922
7013
|
);
|
|
6923
7014
|
lines.push(
|
|
6924
|
-
|
|
7015
|
+
chalk2.dim(" \u{1F4C4} ") + chalk2.cyan(finding.file) + chalk2.dim(`:${finding.line}`) + (finding.cwe ? chalk2.dim(` \xB7 ${finding.cwe}`) : "")
|
|
6925
7016
|
);
|
|
6926
|
-
lines.push(` ${finding.description}`);
|
|
7017
|
+
lines.push(` ${chalk2.dim(finding.description)}`);
|
|
6927
7018
|
if (finding.snippet) {
|
|
6928
7019
|
lines.push("");
|
|
6929
|
-
|
|
7020
|
+
const snippetLines = finding.snippet.split("\n");
|
|
7021
|
+
for (const snippetLine of snippetLines) {
|
|
6930
7022
|
if (snippetLine.startsWith(">")) {
|
|
6931
|
-
lines.push(` ${color(snippetLine)}`);
|
|
7023
|
+
lines.push(` ${color.bold(snippetLine)}`);
|
|
6932
7024
|
} else {
|
|
6933
|
-
lines.push(` ${
|
|
7025
|
+
lines.push(` ${chalk2.gray(snippetLine)}`);
|
|
6934
7026
|
}
|
|
6935
7027
|
}
|
|
6936
7028
|
}
|
|
6937
7029
|
lines.push("");
|
|
6938
|
-
lines.push(
|
|
7030
|
+
lines.push(chalk2.green(` \u{1F4A1} Fix: ${finding.fix.description}`));
|
|
6939
7031
|
if (finding.fix.suggestion) {
|
|
6940
|
-
lines.push(
|
|
7032
|
+
lines.push(chalk2.green.dim(` ${finding.fix.suggestion}`));
|
|
6941
7033
|
}
|
|
6942
7034
|
return lines.join("\n");
|
|
6943
7035
|
}
|
|
6944
7036
|
function formatTableOutput(result, options = {}) {
|
|
6945
7037
|
const lines = [];
|
|
6946
|
-
lines.push("");
|
|
6947
|
-
lines.push(chalk.bold.cyan(" ShipSafe Security Scan Results"));
|
|
6948
|
-
lines.push(chalk.gray(" " + "\u2500".repeat(50)));
|
|
6949
|
-
lines.push("");
|
|
6950
7038
|
const { summary } = result;
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
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
|
+
});
|
|
6955
7049
|
if (summary.total === 0) {
|
|
6956
|
-
lines.push(chalk.green.bold(" No security issues found!"));
|
|
6957
7050
|
if (!options.isLoggedIn) {
|
|
6958
|
-
lines.push("");
|
|
6959
7051
|
lines.push(
|
|
6960
|
-
|
|
6961
|
-
`
|
|
7052
|
+
chalk2.dim(
|
|
7053
|
+
` Run ${chalk2.bold.white("shipsafe login")} to unlock deep AI analysis.`
|
|
6962
7054
|
)
|
|
6963
7055
|
);
|
|
7056
|
+
lines.push("");
|
|
6964
7057
|
}
|
|
6965
|
-
lines.push("");
|
|
6966
7058
|
return lines.join("\n");
|
|
6967
7059
|
}
|
|
6968
|
-
const severityLine = [
|
|
6969
|
-
summary.bySeverity.critical > 0 ? chalk.red.bold(`${summary.bySeverity.critical} critical`) : null,
|
|
6970
|
-
summary.bySeverity.high > 0 ? chalk.yellow.bold(`${summary.bySeverity.high} high`) : null,
|
|
6971
|
-
summary.bySeverity.medium > 0 ? chalk.hex("#ca8a04").bold(`${summary.bySeverity.medium} medium`) : null,
|
|
6972
|
-
summary.bySeverity.low > 0 ? chalk.blue(`${summary.bySeverity.low} low`) : null,
|
|
6973
|
-
summary.bySeverity.info > 0 ? chalk.gray(`${summary.bySeverity.info} info`) : null
|
|
6974
|
-
].filter(Boolean).join(" | ");
|
|
6975
|
-
lines.push(` ${chalk.bold("Findings:")} ${summary.total} (${severityLine})`);
|
|
6976
7060
|
const sorted = [...result.findings].sort(
|
|
6977
7061
|
(a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]
|
|
6978
7062
|
);
|
|
6979
|
-
|
|
6980
|
-
|
|
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("");
|
|
6981
7083
|
}
|
|
6982
7084
|
if (options.diff) {
|
|
6983
7085
|
const { newFindings, resolvedFindings } = options.diff;
|
|
6984
7086
|
if (newFindings.length > 0 || resolvedFindings.length > 0) {
|
|
6985
|
-
|
|
6986
|
-
lines.push(
|
|
7087
|
+
printDivider();
|
|
7088
|
+
lines.push(chalk2.bold.white(" \u{1F4CA} Changes since last scan:"));
|
|
6987
7089
|
if (newFindings.length > 0) {
|
|
6988
7090
|
lines.push(
|
|
6989
|
-
|
|
7091
|
+
chalk2.red(
|
|
7092
|
+
` \u2191 ${newFindings.length} new issue${newFindings.length === 1 ? "" : "s"} introduced`
|
|
7093
|
+
)
|
|
6990
7094
|
);
|
|
6991
7095
|
}
|
|
6992
7096
|
if (resolvedFindings.length > 0) {
|
|
6993
7097
|
lines.push(
|
|
6994
|
-
|
|
7098
|
+
chalk2.green(
|
|
7099
|
+
` \u2193 ${resolvedFindings.length} issue${resolvedFindings.length === 1 ? "" : "s"} resolved`
|
|
7100
|
+
)
|
|
6995
7101
|
);
|
|
6996
7102
|
}
|
|
6997
7103
|
}
|
|
6998
7104
|
}
|
|
6999
7105
|
if (options.suppressedCount && options.suppressedCount > 0) {
|
|
7000
7106
|
lines.push(
|
|
7001
|
-
|
|
7002
|
-
|
|
7107
|
+
chalk2.dim(
|
|
7108
|
+
`
|
|
7109
|
+
${options.suppressedCount} finding${options.suppressedCount === 1 ? "" : "s"} suppressed via .shipsafeignore`
|
|
7110
|
+
)
|
|
7003
7111
|
);
|
|
7004
7112
|
}
|
|
7005
|
-
lines.push("");
|
|
7006
|
-
lines.push(chalk.gray(" " + "\u2500".repeat(50)));
|
|
7007
7113
|
if (!options.isLoggedIn && summary.total > 0) {
|
|
7008
7114
|
const criticalHigh = summary.bySeverity.critical + summary.bySeverity.high;
|
|
7009
|
-
|
|
7010
|
-
if (criticalHigh > 0) {
|
|
7011
|
-
lines.push(
|
|
7012
|
-
chalk.yellow(
|
|
7013
|
-
` Found ${criticalHigh} critical/high issue${criticalHigh === 1 ? "" : "s"}. Deep AI analysis can find auth logic flaws,`
|
|
7014
|
-
)
|
|
7015
|
-
);
|
|
7016
|
-
lines.push(
|
|
7017
|
-
chalk.yellow(
|
|
7018
|
-
` business logic bugs, and more \u2014 run ${chalk.bold("shipsafe login")} to unlock.`
|
|
7019
|
-
)
|
|
7020
|
-
);
|
|
7021
|
-
} else {
|
|
7022
|
-
lines.push(
|
|
7023
|
-
chalk.gray(
|
|
7024
|
-
` Surface scan clean. Run ${chalk.bold.white("shipsafe login")} for deep AI analysis`
|
|
7025
|
-
)
|
|
7026
|
-
);
|
|
7027
|
-
lines.push(
|
|
7028
|
-
chalk.gray(
|
|
7029
|
-
` covering auth logic, RLS policies, and business logic review.`
|
|
7030
|
-
)
|
|
7031
|
-
);
|
|
7032
|
-
}
|
|
7033
|
-
} else if (!options.isLoggedIn && summary.total === 0) {
|
|
7034
|
-
lines.push(
|
|
7035
|
-
chalk.gray(
|
|
7036
|
-
` No issues in surface scan. Run ${chalk.bold.white("shipsafe login")} to unlock deep AI analysis.`
|
|
7037
|
-
)
|
|
7038
|
-
);
|
|
7115
|
+
printUpsellBox(criticalHigh);
|
|
7039
7116
|
}
|
|
7040
7117
|
lines.push("");
|
|
7041
7118
|
return lines.join("\n");
|
|
@@ -7084,7 +7161,7 @@ function formatSarifOutput(result) {
|
|
|
7084
7161
|
}
|
|
7085
7162
|
|
|
7086
7163
|
// src/commands/login.ts
|
|
7087
|
-
import
|
|
7164
|
+
import chalk3 from "chalk";
|
|
7088
7165
|
import ora from "ora";
|
|
7089
7166
|
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync as existsSync2, unlinkSync } from "fs";
|
|
7090
7167
|
import { homedir } from "os";
|
|
@@ -7096,7 +7173,7 @@ function getStoredToken() {
|
|
|
7096
7173
|
if (!existsSync2(TOKEN_FILE)) return null;
|
|
7097
7174
|
const data = JSON.parse(readFileSync2(TOKEN_FILE, "utf-8"));
|
|
7098
7175
|
if (data.expiresAt < Date.now()) {
|
|
7099
|
-
console.log(
|
|
7176
|
+
console.log(chalk3.yellow("Session expired. Please login again."));
|
|
7100
7177
|
return null;
|
|
7101
7178
|
}
|
|
7102
7179
|
return data;
|
|
@@ -7123,7 +7200,7 @@ async function fetchProfile(apiUrl, rawToken) {
|
|
|
7123
7200
|
}
|
|
7124
7201
|
async function loginCommand(options) {
|
|
7125
7202
|
const apiUrl = options.apiUrl || "https://ship-safe.co";
|
|
7126
|
-
console.log(
|
|
7203
|
+
console.log(chalk3.bold("\nShipSafe CLI Login\n"));
|
|
7127
7204
|
const spinner = ora("Requesting device code...").start();
|
|
7128
7205
|
try {
|
|
7129
7206
|
const res = await fetch(`${apiUrl}/api/cli/device-code`, {
|
|
@@ -7133,7 +7210,7 @@ async function loginCommand(options) {
|
|
|
7133
7210
|
if (!res.ok) {
|
|
7134
7211
|
spinner.fail("Failed to request device code");
|
|
7135
7212
|
console.log(
|
|
7136
|
-
|
|
7213
|
+
chalk3.red(
|
|
7137
7214
|
"Could not connect to ShipSafe. Make sure the server is running."
|
|
7138
7215
|
)
|
|
7139
7216
|
);
|
|
@@ -7141,10 +7218,10 @@ async function loginCommand(options) {
|
|
|
7141
7218
|
}
|
|
7142
7219
|
const { deviceCode, userCode, verificationUrl, expiresIn } = await res.json();
|
|
7143
7220
|
spinner.stop();
|
|
7144
|
-
console.log(
|
|
7145
|
-
console.log(
|
|
7221
|
+
console.log(chalk3.bold("To login, open this URL in your browser:\n"));
|
|
7222
|
+
console.log(chalk3.cyan(` ${verificationUrl}
|
|
7146
7223
|
`));
|
|
7147
|
-
console.log(`Enter this code: ${
|
|
7224
|
+
console.log(`Enter this code: ${chalk3.bold.green(userCode)}
|
|
7148
7225
|
`);
|
|
7149
7226
|
const pollSpinner = ora("Waiting for authorization...").start();
|
|
7150
7227
|
const startTime = Date.now();
|
|
@@ -7175,17 +7252,17 @@ async function loginCommand(options) {
|
|
|
7175
7252
|
}
|
|
7176
7253
|
storeToken(tokenData);
|
|
7177
7254
|
pollSpinner.succeed(
|
|
7178
|
-
|
|
7255
|
+
chalk3.green(`Logged in as ${chalk3.bold(pollData.email)}`)
|
|
7179
7256
|
);
|
|
7180
7257
|
if (profile?.cliTier) {
|
|
7181
7258
|
console.log(
|
|
7182
|
-
|
|
7259
|
+
chalk3.cyan(
|
|
7183
7260
|
` CLI plan: ${profile.cliTier} (${profile.aiQuota.remaining} AI scans remaining)`
|
|
7184
7261
|
)
|
|
7185
7262
|
);
|
|
7186
7263
|
} else {
|
|
7187
7264
|
console.log(
|
|
7188
|
-
|
|
7265
|
+
chalk3.dim(
|
|
7189
7266
|
" No CLI plan. Upgrade at ship-safe.co/pricing for AI-powered scanning."
|
|
7190
7267
|
)
|
|
7191
7268
|
);
|
|
@@ -7194,7 +7271,7 @@ async function loginCommand(options) {
|
|
|
7194
7271
|
}
|
|
7195
7272
|
if (pollData.status === "expired") {
|
|
7196
7273
|
pollSpinner.fail(
|
|
7197
|
-
|
|
7274
|
+
chalk3.yellow(
|
|
7198
7275
|
"Your login link expired. Run `shipsafe login` again."
|
|
7199
7276
|
)
|
|
7200
7277
|
);
|
|
@@ -7204,7 +7281,7 @@ async function loginCommand(options) {
|
|
|
7204
7281
|
networkErrors++;
|
|
7205
7282
|
if (networkErrors >= 3) {
|
|
7206
7283
|
pollSpinner.fail(
|
|
7207
|
-
|
|
7284
|
+
chalk3.red(
|
|
7208
7285
|
"Lost connection to ShipSafe. Check your internet and run `shipsafe login` again."
|
|
7209
7286
|
)
|
|
7210
7287
|
);
|
|
@@ -7214,7 +7291,7 @@ async function loginCommand(options) {
|
|
|
7214
7291
|
}
|
|
7215
7292
|
}
|
|
7216
7293
|
pollSpinner.fail(
|
|
7217
|
-
|
|
7294
|
+
chalk3.yellow(
|
|
7218
7295
|
"Your login link expired. Run `shipsafe login` again."
|
|
7219
7296
|
)
|
|
7220
7297
|
);
|
|
@@ -7222,13 +7299,13 @@ async function loginCommand(options) {
|
|
|
7222
7299
|
spinner.fail("Could not connect to ShipSafe");
|
|
7223
7300
|
if (error instanceof Error && (error.message.includes("fetch") || error.message.includes("ECONNREFUSED"))) {
|
|
7224
7301
|
console.log(
|
|
7225
|
-
|
|
7302
|
+
chalk3.red(
|
|
7226
7303
|
"Make sure you have an internet connection and try again."
|
|
7227
7304
|
)
|
|
7228
7305
|
);
|
|
7229
7306
|
} else {
|
|
7230
7307
|
console.log(
|
|
7231
|
-
|
|
7308
|
+
chalk3.red(
|
|
7232
7309
|
error instanceof Error ? error.message : "Unknown error \u2014 try again."
|
|
7233
7310
|
)
|
|
7234
7311
|
);
|
|
@@ -7239,18 +7316,18 @@ async function logoutCommand() {
|
|
|
7239
7316
|
try {
|
|
7240
7317
|
if (existsSync2(TOKEN_FILE)) {
|
|
7241
7318
|
unlinkSync(TOKEN_FILE);
|
|
7242
|
-
console.log(
|
|
7319
|
+
console.log(chalk3.green("Logged out successfully."));
|
|
7243
7320
|
} else {
|
|
7244
|
-
console.log(
|
|
7321
|
+
console.log(chalk3.yellow("Not currently logged in."));
|
|
7245
7322
|
}
|
|
7246
7323
|
} catch {
|
|
7247
|
-
console.log(
|
|
7324
|
+
console.log(chalk3.red("Failed to logout."));
|
|
7248
7325
|
}
|
|
7249
7326
|
}
|
|
7250
7327
|
async function whoamiCommand(options) {
|
|
7251
7328
|
const token = getStoredToken();
|
|
7252
7329
|
if (!token) {
|
|
7253
|
-
console.log(
|
|
7330
|
+
console.log(chalk3.yellow("Not logged in. Run `shipsafe login` to login."));
|
|
7254
7331
|
return;
|
|
7255
7332
|
}
|
|
7256
7333
|
const apiUrl = options.apiUrl || "https://ship-safe.co";
|
|
@@ -7264,26 +7341,26 @@ async function whoamiCommand(options) {
|
|
|
7264
7341
|
aiQuota: profile.aiQuota
|
|
7265
7342
|
});
|
|
7266
7343
|
spinner.stop();
|
|
7267
|
-
console.log(
|
|
7268
|
-
console.log(` Email: ${
|
|
7269
|
-
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)}`);
|
|
7270
7347
|
if (profile.cliTier) {
|
|
7271
|
-
console.log(` CLI plan: ${
|
|
7348
|
+
console.log(` CLI plan: ${chalk3.green(profile.cliTier)}`);
|
|
7272
7349
|
console.log(
|
|
7273
|
-
` AI scans: ${
|
|
7350
|
+
` AI scans: ${chalk3.cyan(`${profile.aiQuota.used}/${profile.aiQuota.limit}`)} used this month (${chalk3.green(`${profile.aiQuota.remaining}`)} remaining)`
|
|
7274
7351
|
);
|
|
7275
7352
|
} else {
|
|
7276
|
-
console.log(` CLI plan: ${
|
|
7353
|
+
console.log(` CLI plan: ${chalk3.dim("none")}`);
|
|
7277
7354
|
console.log(
|
|
7278
|
-
|
|
7355
|
+
chalk3.dim(" Upgrade at ship-safe.co/pricing for AI-powered scanning.")
|
|
7279
7356
|
);
|
|
7280
7357
|
}
|
|
7281
7358
|
console.log();
|
|
7282
7359
|
} else {
|
|
7283
7360
|
spinner.stop();
|
|
7284
|
-
console.log(
|
|
7361
|
+
console.log(chalk3.green(`Logged in as ${chalk3.bold(token.email)}`));
|
|
7285
7362
|
if (token.cliTier) {
|
|
7286
|
-
console.log(
|
|
7363
|
+
console.log(chalk3.cyan(` CLI plan: ${token.cliTier}`));
|
|
7287
7364
|
}
|
|
7288
7365
|
}
|
|
7289
7366
|
}
|
|
@@ -7291,7 +7368,7 @@ async function whoamiCommand(options) {
|
|
|
7291
7368
|
// src/commands/ignore.ts
|
|
7292
7369
|
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, appendFileSync } from "fs";
|
|
7293
7370
|
import { resolve, join as join2 } from "path";
|
|
7294
|
-
import
|
|
7371
|
+
import chalk4 from "chalk";
|
|
7295
7372
|
var IGNORE_FILE = ".shipsafeignore";
|
|
7296
7373
|
function getIgnorePath(dir) {
|
|
7297
7374
|
return resolve(dir || ".", IGNORE_FILE);
|
|
@@ -7315,7 +7392,7 @@ function ignoreCommand(ruleId, options) {
|
|
|
7315
7392
|
if (existsSync3(ignorePath)) {
|
|
7316
7393
|
const existing = loadIgnoredRules(".");
|
|
7317
7394
|
if (existing.has(ruleId)) {
|
|
7318
|
-
console.log(
|
|
7395
|
+
console.log(chalk4.yellow(`Rule "${ruleId}" is already in ${IGNORE_FILE}`));
|
|
7319
7396
|
return;
|
|
7320
7397
|
}
|
|
7321
7398
|
}
|
|
@@ -7336,18 +7413,18 @@ ${line}
|
|
|
7336
7413
|
`
|
|
7337
7414
|
);
|
|
7338
7415
|
}
|
|
7339
|
-
console.log(
|
|
7416
|
+
console.log(chalk4.green(`Added "${ruleId}" to ${IGNORE_FILE}`));
|
|
7340
7417
|
if (options.reason) {
|
|
7341
|
-
console.log(
|
|
7418
|
+
console.log(chalk4.gray(` Reason: ${options.reason}`));
|
|
7342
7419
|
}
|
|
7343
7420
|
console.log(
|
|
7344
|
-
|
|
7421
|
+
chalk4.gray(` This finding will be suppressed in future scans.`)
|
|
7345
7422
|
);
|
|
7346
7423
|
}
|
|
7347
7424
|
function unignoreCommand(ruleId) {
|
|
7348
7425
|
const ignorePath = getIgnorePath();
|
|
7349
7426
|
if (!existsSync3(ignorePath)) {
|
|
7350
|
-
console.log(
|
|
7427
|
+
console.log(chalk4.yellow(`No ${IGNORE_FILE} found.`));
|
|
7351
7428
|
return;
|
|
7352
7429
|
}
|
|
7353
7430
|
const content = readFileSync3(ignorePath, "utf-8");
|
|
@@ -7359,11 +7436,11 @@ function unignoreCommand(ruleId) {
|
|
|
7359
7436
|
return lineRuleId !== ruleId;
|
|
7360
7437
|
});
|
|
7361
7438
|
if (filtered.length === lines.length) {
|
|
7362
|
-
console.log(
|
|
7439
|
+
console.log(chalk4.yellow(`Rule "${ruleId}" is not in ${IGNORE_FILE}`));
|
|
7363
7440
|
return;
|
|
7364
7441
|
}
|
|
7365
7442
|
writeFileSync2(ignorePath, filtered.join("\n"));
|
|
7366
|
-
console.log(
|
|
7443
|
+
console.log(chalk4.green(`Removed "${ruleId}" from ${IGNORE_FILE}`));
|
|
7367
7444
|
}
|
|
7368
7445
|
|
|
7369
7446
|
// src/lib/scan-history.ts
|
|
@@ -7448,28 +7525,81 @@ function loadConfig(dir) {
|
|
|
7448
7525
|
async function scanCommand(targetPath, options) {
|
|
7449
7526
|
const resolvedPath = resolve2(targetPath);
|
|
7450
7527
|
if (!existsSync5(resolvedPath)) {
|
|
7451
|
-
|
|
7528
|
+
printError(`Path "${targetPath}" does not exist.`);
|
|
7452
7529
|
process.exit(1);
|
|
7453
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
|
+
}
|
|
7454
7567
|
const config = loadConfig(resolvedPath);
|
|
7455
7568
|
const severity = options.severity || config.severity || "low";
|
|
7456
|
-
const spinner = ora2(
|
|
7569
|
+
const spinner = ora2({
|
|
7570
|
+
text: chalk5.dim("Collecting files..."),
|
|
7571
|
+
color: "yellow",
|
|
7572
|
+
spinner: "dots12"
|
|
7573
|
+
}).start();
|
|
7457
7574
|
const files = collectFiles(resolvedPath, { exclude: config.exclude });
|
|
7458
7575
|
if (files.length === 0) {
|
|
7459
|
-
spinner.fail("No supported files found to scan.");
|
|
7576
|
+
spinner.fail(chalk5.red("No supported files found to scan."));
|
|
7460
7577
|
process.exit(1);
|
|
7461
7578
|
}
|
|
7462
|
-
|
|
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
|
+
);
|
|
7463
7586
|
const result = await scan(
|
|
7464
7587
|
{ files, tier: "free" },
|
|
7465
7588
|
(progress) => {
|
|
7466
|
-
spinner.text =
|
|
7589
|
+
spinner.text = chalk5.dim(
|
|
7590
|
+
`${progress.stage} ${chalk5.white(`(${progress.findingsCount} findings)`)}`
|
|
7591
|
+
);
|
|
7467
7592
|
}
|
|
7468
7593
|
);
|
|
7469
|
-
spinner.
|
|
7470
|
-
|
|
7594
|
+
spinner.succeed(
|
|
7595
|
+
chalk5.green(`Scan complete \u2014 ${result.findings.length} findings in ${result.durationMs}ms`)
|
|
7596
|
+
);
|
|
7471
7597
|
if (tokenData?.cliTier) {
|
|
7472
|
-
const aiSpinner = ora2(
|
|
7598
|
+
const aiSpinner = ora2({
|
|
7599
|
+
text: chalk5.dim("Running AI-powered deep analysis..."),
|
|
7600
|
+
color: "yellow",
|
|
7601
|
+
spinner: "dots12"
|
|
7602
|
+
}).start();
|
|
7473
7603
|
try {
|
|
7474
7604
|
const aiRes = await fetch(`${options.apiUrl}/api/cli/ai-scan`, {
|
|
7475
7605
|
method: "POST",
|
|
@@ -7510,31 +7640,29 @@ async function scanCommand(targetPath, options) {
|
|
|
7510
7640
|
info: result.findings.filter((f) => f.severity === "info").length
|
|
7511
7641
|
};
|
|
7512
7642
|
aiSpinner.succeed(
|
|
7513
|
-
|
|
7514
|
-
`AI analysis
|
|
7643
|
+
chalk5.green(
|
|
7644
|
+
`AI analysis: ${aiFindings.length} additional findings`
|
|
7515
7645
|
)
|
|
7516
7646
|
);
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
` AI scans: ${aiData.quota.used}/${aiData.quota.limit} used this month (${aiData.quota.remaining} remaining)`
|
|
7520
|
-
)
|
|
7647
|
+
printInfo(
|
|
7648
|
+
`AI scans: ${aiData.quota.used}/${aiData.quota.limit} used this month (${aiData.quota.remaining} remaining)`
|
|
7521
7649
|
);
|
|
7522
7650
|
} else if (aiRes.status === 429) {
|
|
7523
7651
|
aiSpinner.warn(
|
|
7524
|
-
|
|
7652
|
+
chalk5.yellow("AI scan quota reached. Showing rule-based results only.")
|
|
7525
7653
|
);
|
|
7526
7654
|
} else if (aiRes.status === 401) {
|
|
7527
7655
|
aiSpinner.warn(
|
|
7528
|
-
|
|
7656
|
+
chalk5.yellow("Session expired. Run `shipsafe login` to re-authenticate.")
|
|
7529
7657
|
);
|
|
7530
7658
|
} else {
|
|
7531
7659
|
aiSpinner.warn(
|
|
7532
|
-
|
|
7660
|
+
chalk5.yellow("AI analysis unavailable. Showing rule-based results only.")
|
|
7533
7661
|
);
|
|
7534
7662
|
}
|
|
7535
7663
|
} catch {
|
|
7536
7664
|
aiSpinner.warn(
|
|
7537
|
-
|
|
7665
|
+
chalk5.yellow("Could not reach AI scanning service. Showing rule-based results only.")
|
|
7538
7666
|
);
|
|
7539
7667
|
}
|
|
7540
7668
|
}
|
|
@@ -7582,7 +7710,11 @@ async function scanCommand(targetPath, options) {
|
|
|
7582
7710
|
}
|
|
7583
7711
|
const token = getStoredToken();
|
|
7584
7712
|
if (token && options.upload) {
|
|
7585
|
-
const uploadSpinner = ora2(
|
|
7713
|
+
const uploadSpinner = ora2({
|
|
7714
|
+
text: chalk5.dim("Syncing results to dashboard..."),
|
|
7715
|
+
color: "yellow",
|
|
7716
|
+
spinner: "dots12"
|
|
7717
|
+
}).start();
|
|
7586
7718
|
try {
|
|
7587
7719
|
const apiFindings = result.findings.map((f) => ({
|
|
7588
7720
|
ruleId: f.ruleId,
|
|
@@ -7615,14 +7747,10 @@ async function scanCommand(targetPath, options) {
|
|
|
7615
7747
|
if (res.ok) {
|
|
7616
7748
|
const data = await res.json();
|
|
7617
7749
|
uploadSpinner.succeed(
|
|
7618
|
-
|
|
7750
|
+
chalk5.green(`Results synced: ${data.dashboardUrl || "view in dashboard"}`)
|
|
7619
7751
|
);
|
|
7620
7752
|
if (data.deepScanStatus === "running") {
|
|
7621
|
-
|
|
7622
|
-
chalk4.cyan(
|
|
7623
|
-
" Deep AI analysis running \u2014 check dashboard for updates."
|
|
7624
|
-
)
|
|
7625
|
-
);
|
|
7753
|
+
printInfo("Deep AI analysis running \u2014 check dashboard for updates.");
|
|
7626
7754
|
}
|
|
7627
7755
|
} else if (res.status === 401) {
|
|
7628
7756
|
uploadSpinner.warn(
|
|
@@ -7638,7 +7766,7 @@ async function scanCommand(targetPath, options) {
|
|
|
7638
7766
|
}
|
|
7639
7767
|
} else if (options.upload) {
|
|
7640
7768
|
console.log(
|
|
7641
|
-
|
|
7769
|
+
chalk5.yellow(
|
|
7642
7770
|
"\nNot logged in. Run `shipsafe login` first to sync results."
|
|
7643
7771
|
)
|
|
7644
7772
|
);
|
|
@@ -7656,7 +7784,7 @@ async function scanCommand(targetPath, options) {
|
|
|
7656
7784
|
// src/commands/init.ts
|
|
7657
7785
|
import { writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
|
|
7658
7786
|
import { resolve as resolve3 } from "path";
|
|
7659
|
-
import
|
|
7787
|
+
import chalk6 from "chalk";
|
|
7660
7788
|
var DEFAULT_CONFIG = `# ShipSafe Configuration
|
|
7661
7789
|
# https://ship-safe.co/docs/config
|
|
7662
7790
|
|
|
@@ -7687,11 +7815,11 @@ severity: low
|
|
|
7687
7815
|
function initCommand() {
|
|
7688
7816
|
const configPath = resolve3(".shipsafe.yml");
|
|
7689
7817
|
if (existsSync6(configPath)) {
|
|
7690
|
-
console.log(
|
|
7818
|
+
console.log(chalk6.yellow("A .shipsafe.yml already exists in this directory."));
|
|
7691
7819
|
return;
|
|
7692
7820
|
}
|
|
7693
7821
|
writeFileSync4(configPath, DEFAULT_CONFIG, "utf-8");
|
|
7694
|
-
console.log(
|
|
7822
|
+
console.log(chalk6.green("Created .shipsafe.yml configuration file."));
|
|
7695
7823
|
}
|
|
7696
7824
|
|
|
7697
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",
|