sapper-ai 0.2.2 → 0.3.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/cli.d.ts.map +1 -1
- package/dist/cli.js +86 -4
- package/dist/scan.d.ts +3 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +169 -38
- package/package.json +1 -1
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAWA,wBAAsB,MAAM,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAiCpF"}
|
package/dist/cli.js
CHANGED
|
@@ -37,6 +37,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
37
37
|
exports.runCli = runCli;
|
|
38
38
|
const node_fs_1 = require("node:fs");
|
|
39
39
|
const node_child_process_1 = require("node:child_process");
|
|
40
|
+
const node_os_1 = require("node:os");
|
|
40
41
|
const node_path_1 = require("node:path");
|
|
41
42
|
const readline = __importStar(require("node:readline"));
|
|
42
43
|
const presets_1 = require("./presets");
|
|
@@ -52,7 +53,12 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
52
53
|
printUsage();
|
|
53
54
|
return 1;
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
+
const scanOptions = await resolveScanOptions(parsed);
|
|
57
|
+
if (!scanOptions) {
|
|
58
|
+
printUsage();
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
return (0, scan_1.runScan)(scanOptions);
|
|
56
62
|
}
|
|
57
63
|
if (argv[0] === 'dashboard') {
|
|
58
64
|
return runDashboard();
|
|
@@ -69,8 +75,12 @@ function printUsage() {
|
|
|
69
75
|
sapper-ai - AI security guardrails
|
|
70
76
|
|
|
71
77
|
Usage:
|
|
72
|
-
sapper-ai scan
|
|
73
|
-
sapper-ai scan
|
|
78
|
+
sapper-ai scan Interactive scan scope (TTY only)
|
|
79
|
+
sapper-ai scan . Current directory only (no subdirectories)
|
|
80
|
+
sapper-ai scan --deep Current directory + subdirectories
|
|
81
|
+
sapper-ai scan --system AI system paths (~/.claude, ~/.cursor, ...)
|
|
82
|
+
sapper-ai scan ./path Scan a specific file/directory
|
|
83
|
+
sapper-ai scan --fix Quarantine blocked files
|
|
74
84
|
sapper-ai init Interactive setup wizard
|
|
75
85
|
sapper-ai dashboard Launch web dashboard
|
|
76
86
|
sapper-ai --help Show this help
|
|
@@ -81,17 +91,89 @@ Learn more: https://github.com/sapper-ai/sapperai
|
|
|
81
91
|
function parseScanArgs(argv) {
|
|
82
92
|
const targets = [];
|
|
83
93
|
let fix = false;
|
|
94
|
+
let deep = false;
|
|
95
|
+
let system = false;
|
|
84
96
|
for (const arg of argv) {
|
|
85
97
|
if (arg === '--fix') {
|
|
86
98
|
fix = true;
|
|
87
99
|
continue;
|
|
88
100
|
}
|
|
101
|
+
if (arg === '--deep') {
|
|
102
|
+
deep = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (arg === '--system') {
|
|
106
|
+
system = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
89
109
|
if (arg.startsWith('-')) {
|
|
90
110
|
return null;
|
|
91
111
|
}
|
|
92
112
|
targets.push(arg);
|
|
93
113
|
}
|
|
94
|
-
return { targets, fix };
|
|
114
|
+
return { targets, fix, deep, system };
|
|
115
|
+
}
|
|
116
|
+
function displayPath(path) {
|
|
117
|
+
const home = (0, node_os_1.homedir)();
|
|
118
|
+
if (path === home)
|
|
119
|
+
return '~';
|
|
120
|
+
return path.startsWith(home + '/') ? `~/${path.slice(home.length + 1)}` : path;
|
|
121
|
+
}
|
|
122
|
+
async function promptScanScope(cwd) {
|
|
123
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
124
|
+
const ask = (q) => new Promise((res) => rl.question(q, res));
|
|
125
|
+
try {
|
|
126
|
+
console.log('\n SapperAI Security Scanner\n');
|
|
127
|
+
console.log(' ? Scan scope:');
|
|
128
|
+
console.log(` ❯ 1) Current directory only ${displayPath(cwd)}`);
|
|
129
|
+
console.log(` 2) Current + subdirectories ${displayPath((0, node_path_1.join)(cwd, '**'))}`);
|
|
130
|
+
console.log(' 3) AI system scan ~/.claude, ~/.cursor, ~/.vscode ...');
|
|
131
|
+
console.log();
|
|
132
|
+
const answer = await ask(' Choose [1-3] (default: 2): ');
|
|
133
|
+
const picked = Number.parseInt(answer.trim(), 10);
|
|
134
|
+
if (picked === 1)
|
|
135
|
+
return 'shallow';
|
|
136
|
+
if (picked === 3)
|
|
137
|
+
return 'system';
|
|
138
|
+
return 'deep';
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
rl.close();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function resolveScanOptions(args) {
|
|
145
|
+
const cwd = process.cwd();
|
|
146
|
+
if (args.system) {
|
|
147
|
+
if (args.targets.length > 0) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
return { system: true, fix: args.fix, scopeLabel: 'AI system scan' };
|
|
151
|
+
}
|
|
152
|
+
if (args.targets.length > 0) {
|
|
153
|
+
if (args.targets.length === 1 && args.targets[0] === '.' && !args.deep) {
|
|
154
|
+
return { targets: [cwd], deep: false, fix: args.fix, scopeLabel: 'Current directory only' };
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
targets: args.targets,
|
|
158
|
+
deep: true,
|
|
159
|
+
fix: args.fix,
|
|
160
|
+
scopeLabel: args.targets.length === 1 && args.targets[0] === '.' ? 'Current + subdirectories' : 'Custom path',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (args.deep) {
|
|
164
|
+
return { targets: [cwd], deep: true, fix: args.fix, scopeLabel: 'Current + subdirectories' };
|
|
165
|
+
}
|
|
166
|
+
if (process.stdout.isTTY !== true) {
|
|
167
|
+
return { targets: [cwd], deep: true, fix: args.fix, scopeLabel: 'Current + subdirectories' };
|
|
168
|
+
}
|
|
169
|
+
const scope = await promptScanScope(cwd);
|
|
170
|
+
if (scope === 'system') {
|
|
171
|
+
return { system: true, fix: args.fix, scopeLabel: 'AI system scan' };
|
|
172
|
+
}
|
|
173
|
+
if (scope === 'shallow') {
|
|
174
|
+
return { targets: [cwd], deep: false, fix: args.fix, scopeLabel: 'Current directory only' };
|
|
175
|
+
}
|
|
176
|
+
return { targets: [cwd], deep: true, fix: args.fix, scopeLabel: 'Current + subdirectories' };
|
|
95
177
|
}
|
|
96
178
|
async function runDashboard() {
|
|
97
179
|
const configuredPort = process.env.PORT;
|
package/dist/scan.d.ts
CHANGED
package/dist/scan.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,GAAG,CAAC,EAAE,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AA6TD,wBAAsB,OAAO,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAsGxE"}
|
package/dist/scan.js
CHANGED
|
@@ -8,6 +8,20 @@ const node_path_1 = require("node:path");
|
|
|
8
8
|
const core_1 = require("@sapper-ai/core");
|
|
9
9
|
const presets_1 = require("./presets");
|
|
10
10
|
const CONFIG_FILE_NAMES = ['sapperai.config.yaml', 'sapperai.config.yml'];
|
|
11
|
+
const GREEN = '\x1b[32m';
|
|
12
|
+
const YELLOW = '\x1b[33m';
|
|
13
|
+
const RED = '\x1b[31m';
|
|
14
|
+
const RESET = '\x1b[0m';
|
|
15
|
+
const SYSTEM_SCAN_PATHS = (() => {
|
|
16
|
+
const home = (0, node_os_1.homedir)();
|
|
17
|
+
return [
|
|
18
|
+
(0, node_path_1.join)(home, '.claude'),
|
|
19
|
+
(0, node_path_1.join)(home, '.config', 'claude-code'),
|
|
20
|
+
(0, node_path_1.join)(home, '.cursor'),
|
|
21
|
+
(0, node_path_1.join)(home, '.vscode', 'extensions'),
|
|
22
|
+
(0, node_path_1.join)(home, 'Library', 'Application Support', 'Claude'),
|
|
23
|
+
];
|
|
24
|
+
})();
|
|
11
25
|
function findConfigFile(cwd) {
|
|
12
26
|
for (const name of CONFIG_FILE_NAMES) {
|
|
13
27
|
const fullPath = (0, node_path_1.resolve)(cwd, name);
|
|
@@ -30,15 +44,11 @@ function getThresholds(policy) {
|
|
|
30
44
|
const blockMinConfidence = typeof extended.thresholds?.blockMinConfidence === 'number' ? extended.thresholds.blockMinConfidence : 0.5;
|
|
31
45
|
return { riskThreshold, blockMinConfidence };
|
|
32
46
|
}
|
|
33
|
-
function toDefaultTargets(cwd) {
|
|
34
|
-
const home = (0, node_os_1.homedir)();
|
|
35
|
-
return [(0, node_path_1.join)(home, '.claude', 'plugins'), (0, node_path_1.join)(home, '.config', 'claude-code'), cwd];
|
|
36
|
-
}
|
|
37
47
|
function shouldSkipDir(dirName) {
|
|
38
48
|
const base = (0, node_path_1.basename)(dirName);
|
|
39
49
|
return base === 'node_modules' || base === '.git' || base === 'dist';
|
|
40
50
|
}
|
|
41
|
-
async function collectFiles(targetPath) {
|
|
51
|
+
async function collectFiles(targetPath, deep) {
|
|
42
52
|
try {
|
|
43
53
|
const info = await (0, promises_1.stat)(targetPath);
|
|
44
54
|
if (info.isFile()) {
|
|
@@ -51,6 +61,21 @@ async function collectFiles(targetPath) {
|
|
|
51
61
|
catch {
|
|
52
62
|
return [];
|
|
53
63
|
}
|
|
64
|
+
if (!deep) {
|
|
65
|
+
try {
|
|
66
|
+
const entries = await (0, promises_1.readdir)(targetPath, { withFileTypes: true });
|
|
67
|
+
const results = [];
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (!entry.isFile())
|
|
70
|
+
continue;
|
|
71
|
+
results.push((0, node_path_1.join)(targetPath, entry.name));
|
|
72
|
+
}
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
54
79
|
const results = [];
|
|
55
80
|
const stack = [targetPath];
|
|
56
81
|
while (stack.length > 0) {
|
|
@@ -80,6 +105,85 @@ async function collectFiles(targetPath) {
|
|
|
80
105
|
}
|
|
81
106
|
return results;
|
|
82
107
|
}
|
|
108
|
+
function riskColor(risk) {
|
|
109
|
+
if (risk >= 0.8)
|
|
110
|
+
return RED;
|
|
111
|
+
if (risk >= 0.5)
|
|
112
|
+
return YELLOW;
|
|
113
|
+
return GREEN;
|
|
114
|
+
}
|
|
115
|
+
function stripAnsi(text) {
|
|
116
|
+
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
117
|
+
}
|
|
118
|
+
function truncateToWidth(text, maxWidth) {
|
|
119
|
+
if (maxWidth <= 0) {
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
122
|
+
if (text.length <= maxWidth) {
|
|
123
|
+
return text;
|
|
124
|
+
}
|
|
125
|
+
if (maxWidth <= 3) {
|
|
126
|
+
return '.'.repeat(maxWidth);
|
|
127
|
+
}
|
|
128
|
+
return `...${text.slice(text.length - (maxWidth - 3))}`;
|
|
129
|
+
}
|
|
130
|
+
function renderProgressBar(current, total, width) {
|
|
131
|
+
const safeTotal = Math.max(1, total);
|
|
132
|
+
const pct = Math.floor((current / safeTotal) * 100);
|
|
133
|
+
const filled = Math.floor((current / safeTotal) * width);
|
|
134
|
+
const bar = '█'.repeat(filled) + '░'.repeat(Math.max(0, width - filled));
|
|
135
|
+
return ` ${bar} ${pct}% │ ${current}/${total} files`;
|
|
136
|
+
}
|
|
137
|
+
function extractPatternLabel(decision) {
|
|
138
|
+
const reason = decision.reasons[0];
|
|
139
|
+
if (!reason)
|
|
140
|
+
return 'threat';
|
|
141
|
+
return reason.startsWith('Detected pattern: ') ? reason.slice('Detected pattern: '.length) : reason;
|
|
142
|
+
}
|
|
143
|
+
function padRight(text, width) {
|
|
144
|
+
if (text.length >= width)
|
|
145
|
+
return text;
|
|
146
|
+
return text + ' '.repeat(width - text.length);
|
|
147
|
+
}
|
|
148
|
+
function padRightVisual(text, width) {
|
|
149
|
+
const visLen = stripAnsi(text).length;
|
|
150
|
+
if (visLen >= width)
|
|
151
|
+
return text;
|
|
152
|
+
return text + ' '.repeat(width - visLen);
|
|
153
|
+
}
|
|
154
|
+
function padLeft(text, width) {
|
|
155
|
+
if (text.length >= width)
|
|
156
|
+
return text;
|
|
157
|
+
return ' '.repeat(width - text.length) + text;
|
|
158
|
+
}
|
|
159
|
+
function renderFindingsTable(findings, opts) {
|
|
160
|
+
const rows = findings.map((f, i) => {
|
|
161
|
+
const file = f.filePath.startsWith(opts.cwd + '/') ? f.filePath.slice(opts.cwd.length + 1) : f.filePath;
|
|
162
|
+
const pattern = extractPatternLabel(f.decision);
|
|
163
|
+
const riskValue = f.decision.risk.toFixed(2);
|
|
164
|
+
const riskPlain = padLeft(riskValue, 4);
|
|
165
|
+
const risk = opts.color ? `${riskColor(f.decision.risk)}${riskPlain}${RESET}` : riskPlain;
|
|
166
|
+
return { idx: String(i + 1), file, risk, pattern };
|
|
167
|
+
});
|
|
168
|
+
const idxWidth = Math.max(1, ...rows.map((r) => r.idx.length));
|
|
169
|
+
const riskWidth = 4;
|
|
170
|
+
const patternWidth = Math.min(20, Math.max('Pattern'.length, ...rows.map((r) => r.pattern.length)));
|
|
171
|
+
const baseWidth = 2 + idxWidth + 2 + 2 + riskWidth + 2 + 2 + patternWidth + 2;
|
|
172
|
+
const maxTableWidth = Math.max(60, Math.min(opts.columns || 80, 120));
|
|
173
|
+
const fileWidth = Math.max(20, Math.min(50, maxTableWidth - baseWidth));
|
|
174
|
+
const top = ` ┌${'─'.repeat(idxWidth + 2)}┬${'─'.repeat(fileWidth + 2)}┬${'─'.repeat(riskWidth + 2)}┬${'─'.repeat(patternWidth + 2)}┐`;
|
|
175
|
+
const header = ` │ ${padRight('#', idxWidth)} │ ${padRight('File', fileWidth)} │ ${padRight('Risk', riskWidth)} │ ${padRight('Pattern', patternWidth)} │`;
|
|
176
|
+
const sep = ` ├${'─'.repeat(idxWidth + 2)}┼${'─'.repeat(fileWidth + 2)}┼${'─'.repeat(riskWidth + 2)}┼${'─'.repeat(patternWidth + 2)}┤`;
|
|
177
|
+
const lines = [top, header, sep];
|
|
178
|
+
for (const r of rows) {
|
|
179
|
+
const file = truncateToWidth(r.file, fileWidth);
|
|
180
|
+
const pattern = truncateToWidth(r.pattern, patternWidth);
|
|
181
|
+
lines.push(` │ ${padRight(r.idx, idxWidth)} │ ${padRight(file, fileWidth)} │ ${padRightVisual(r.risk, riskWidth)} │ ${padRight(pattern, patternWidth)} │`);
|
|
182
|
+
}
|
|
183
|
+
const bottom = ` └${'─'.repeat(idxWidth + 2)}┴${'─'.repeat(fileWidth + 2)}┴${'─'.repeat(riskWidth + 2)}┴${'─'.repeat(patternWidth + 2)}┘`;
|
|
184
|
+
lines.push(bottom);
|
|
185
|
+
return lines;
|
|
186
|
+
}
|
|
83
187
|
function isThreat(decision, policy) {
|
|
84
188
|
const { riskThreshold, blockMinConfidence } = getThresholds(policy);
|
|
85
189
|
return decision.risk >= riskThreshold && decision.confidence >= blockMinConfidence;
|
|
@@ -142,60 +246,87 @@ async function scanFile(filePath, policy, scanner, detectors, fix, quarantineMan
|
|
|
142
246
|
}
|
|
143
247
|
return { scanned: true, finding: { filePath, decision: bestThreat } };
|
|
144
248
|
}
|
|
145
|
-
function formatFindingLine(index, finding) {
|
|
146
|
-
const reason = finding.decision.reasons[0];
|
|
147
|
-
const label = reason?.startsWith('Detected pattern: ')
|
|
148
|
-
? reason.slice('Detected pattern: '.length)
|
|
149
|
-
: reason ?? 'threat';
|
|
150
|
-
const risk = finding.decision.risk.toFixed(2);
|
|
151
|
-
const quarantineSuffix = finding.quarantinedId ? ` (quarantined: ${finding.quarantinedId})` : '';
|
|
152
|
-
return ` ${index}. ${finding.filePath}\n Risk: ${risk} | ${label}${quarantineSuffix}`;
|
|
153
|
-
}
|
|
154
249
|
async function runScan(options = {}) {
|
|
155
250
|
const cwd = process.cwd();
|
|
156
251
|
const policy = resolvePolicy(cwd);
|
|
157
252
|
const fix = options.fix === true;
|
|
158
|
-
const
|
|
253
|
+
const deep = options.system ? true : options.deep !== false;
|
|
254
|
+
const targets = options.system === true
|
|
255
|
+
? SYSTEM_SCAN_PATHS
|
|
256
|
+
: options.targets && options.targets.length > 0
|
|
257
|
+
? options.targets
|
|
258
|
+
: [cwd];
|
|
159
259
|
const scanner = new core_1.Scanner();
|
|
160
260
|
const detectors = (0, core_1.createDetectors)({ policy });
|
|
161
261
|
const quarantineDir = process.env.SAPPERAI_QUARANTINE_DIR;
|
|
162
262
|
const quarantineManager = quarantineDir ? new core_1.QuarantineManager({ quarantineDir }) : new core_1.QuarantineManager();
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
263
|
+
const isTTY = process.stdout.isTTY === true;
|
|
264
|
+
const color = isTTY;
|
|
265
|
+
const scopeLabel = options.scopeLabel ??
|
|
266
|
+
(options.system
|
|
267
|
+
? 'AI system scan'
|
|
268
|
+
: deep
|
|
269
|
+
? 'Current + subdirectories'
|
|
270
|
+
: 'Current directory only');
|
|
271
|
+
console.log('\n SapperAI Security Scanner\n');
|
|
272
|
+
console.log(` Scope: ${scopeLabel}`);
|
|
168
273
|
console.log();
|
|
169
274
|
const fileSet = new Set();
|
|
170
275
|
for (const target of targets) {
|
|
171
|
-
const files = await collectFiles(target);
|
|
276
|
+
const files = await collectFiles(target, deep);
|
|
172
277
|
for (const f of files) {
|
|
173
278
|
fileSet.add(f);
|
|
174
279
|
}
|
|
175
280
|
}
|
|
176
|
-
|
|
281
|
+
const files = Array.from(fileSet).sort();
|
|
282
|
+
console.log(` Collecting files... ${files.length} files found`);
|
|
283
|
+
console.log();
|
|
177
284
|
const findings = [];
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
285
|
+
const total = files.length;
|
|
286
|
+
const progressWidth = Math.max(10, Math.min(30, (process.stdout.columns ?? 80) - 30));
|
|
287
|
+
for (let i = 0; i < files.length; i += 1) {
|
|
288
|
+
const filePath = files[i];
|
|
289
|
+
if (isTTY && total > 0) {
|
|
290
|
+
const bar = renderProgressBar(i + 1, total, progressWidth);
|
|
291
|
+
const label = ' Scanning: ';
|
|
292
|
+
const maxPath = Math.max(10, (process.stdout.columns ?? 80) - stripAnsi(bar).length - label.length);
|
|
293
|
+
const scanning = `${label}${truncateToWidth(filePath, maxPath)}`;
|
|
294
|
+
if (i === 0) {
|
|
295
|
+
process.stdout.write(`${bar}\n${scanning}\n`);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
process.stdout.write(`\x1b[2A\x1b[2K\r${bar}\n\x1b[2K\r${scanning}\n`);
|
|
299
|
+
}
|
|
182
300
|
}
|
|
301
|
+
const result = await scanFile(filePath, policy, scanner, detectors, fix, quarantineManager);
|
|
183
302
|
if (result.finding) {
|
|
184
303
|
findings.push(result.finding);
|
|
185
304
|
}
|
|
186
305
|
}
|
|
187
|
-
|
|
188
|
-
|
|
306
|
+
if (isTTY && total > 0) {
|
|
307
|
+
process.stdout.write('\x1b[2A\x1b[2K\r\x1b[1B\x1b[2K\r');
|
|
308
|
+
}
|
|
309
|
+
if (findings.length === 0) {
|
|
310
|
+
const msg = ` ✓ All clear — ${total} files scanned, 0 threats detected`;
|
|
311
|
+
console.log(color ? `${GREEN}${msg}${RESET}` : msg);
|
|
312
|
+
console.log();
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
const warn = ` ⚠ ${total} files scanned, ${findings.length} threats detected`;
|
|
316
|
+
console.log(color ? `${RED}${warn}${RESET}` : warn);
|
|
189
317
|
console.log();
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
318
|
+
const tableLines = renderFindingsTable(findings, {
|
|
319
|
+
cwd,
|
|
320
|
+
columns: process.stdout.columns ?? 80,
|
|
321
|
+
color,
|
|
322
|
+
});
|
|
323
|
+
for (const line of tableLines) {
|
|
324
|
+
console.log(line);
|
|
325
|
+
}
|
|
326
|
+
console.log();
|
|
327
|
+
if (!fix) {
|
|
328
|
+
console.log(" Run 'npx sapper-ai scan --fix' to quarantine blocked files.");
|
|
329
|
+
console.log();
|
|
199
330
|
}
|
|
200
|
-
return
|
|
331
|
+
return 1;
|
|
201
332
|
}
|