ship-safe 6.0.0 → 6.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/README.md +157 -23
- package/cli/agents/abom-generator.js +225 -0
- package/cli/agents/agent-config-scanner.js +547 -0
- package/cli/agents/html-reporter.js +568 -511
- package/cli/agents/index.js +5 -1
- package/cli/agents/scoring-engine.js +11 -0
- package/cli/bin/ship-safe.js +57 -4
- package/cli/commands/abom.js +73 -0
- package/cli/commands/audit.js +2 -0
- package/cli/commands/ci.js +1 -1
- package/cli/commands/guard.js +99 -0
- package/cli/commands/init.js +58 -0
- package/cli/commands/openclaw.js +378 -0
- package/cli/commands/scan-skill.js +329 -0
- package/cli/commands/scan.js +2 -0
- package/cli/commands/score.js +1 -0
- package/cli/commands/update-intel.js +55 -0
- package/cli/commands/watch.js +120 -0
- package/cli/data/threat-intel.json +85 -0
- package/cli/index.js +2 -0
- package/cli/utils/compliance-map.js +125 -0
- package/cli/utils/output.js +230 -229
- package/cli/utils/threat-intel.js +167 -0
- package/package.json +3 -2
- package/cli/__tests__/agents.test.js +0 -1301
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compliance Mapping Utility
|
|
3
|
+
* ===========================
|
|
4
|
+
*
|
|
5
|
+
* Maps CWE/OWASP findings to compliance frameworks:
|
|
6
|
+
* - SOC 2 Type II (Trust Service Criteria)
|
|
7
|
+
* - ISO 27001:2022 (Annex A controls)
|
|
8
|
+
* - NIST AI Risk Management Framework (AI RMF 1.0)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// CWE → COMPLIANCE CONTROL MAPPING
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
const CWE_MAP = {
|
|
16
|
+
'CWE-74': { soc2: ['CC6.1', 'CC7.1'], iso27001: ['A.8.28'], nistAiRmf: ['MAP 1.5', 'MEASURE 2.6'] },
|
|
17
|
+
'CWE-78': { soc2: ['CC6.1', 'CC7.1'], iso27001: ['A.8.28'], nistAiRmf: ['MAP 1.5'] },
|
|
18
|
+
'CWE-79': { soc2: ['CC6.1'], iso27001: ['A.8.28'], nistAiRmf: [] },
|
|
19
|
+
'CWE-89': { soc2: ['CC6.1'], iso27001: ['A.8.28'], nistAiRmf: [] },
|
|
20
|
+
'CWE-94': { soc2: ['CC6.1', 'CC7.1'], iso27001: ['A.8.28', 'A.8.9'], nistAiRmf: ['MAP 1.5'] },
|
|
21
|
+
'CWE-116': { soc2: ['CC6.1'], iso27001: ['A.8.28'], nistAiRmf: ['MEASURE 2.6'] },
|
|
22
|
+
'CWE-200': { soc2: ['CC6.5', 'CC6.1'], iso27001: ['A.8.11', 'A.5.33'], nistAiRmf: ['GOVERN 1.7', 'MAP 5.1'] },
|
|
23
|
+
'CWE-250': { soc2: ['CC6.3'], iso27001: ['A.8.2'], nistAiRmf: ['GOVERN 1.4'] },
|
|
24
|
+
'CWE-269': { soc2: ['CC6.3', 'CC6.1'], iso27001: ['A.8.2', 'A.5.15'], nistAiRmf: ['GOVERN 1.4'] },
|
|
25
|
+
'CWE-287': { soc2: ['CC6.1', 'CC6.2'], iso27001: ['A.8.5'], nistAiRmf: [] },
|
|
26
|
+
'CWE-306': { soc2: ['CC6.1', 'CC6.2'], iso27001: ['A.8.5'], nistAiRmf: ['GOVERN 1.4'] },
|
|
27
|
+
'CWE-311': { soc2: ['CC6.7'], iso27001: ['A.8.24'], nistAiRmf: [] },
|
|
28
|
+
'CWE-312': { soc2: ['CC6.7', 'CC6.1'], iso27001: ['A.8.24', 'A.5.33'], nistAiRmf: ['MAP 5.1'] },
|
|
29
|
+
'CWE-326': { soc2: ['CC6.7'], iso27001: ['A.8.24'], nistAiRmf: [] },
|
|
30
|
+
'CWE-327': { soc2: ['CC6.7'], iso27001: ['A.8.24'], nistAiRmf: [] },
|
|
31
|
+
'CWE-502': { soc2: ['CC6.1'], iso27001: ['A.8.28'], nistAiRmf: [] },
|
|
32
|
+
'CWE-522': { soc2: ['CC6.1', 'CC6.7'], iso27001: ['A.8.5', 'A.8.24'], nistAiRmf: [] },
|
|
33
|
+
'CWE-611': { soc2: ['CC6.1'], iso27001: ['A.8.28'], nistAiRmf: [] },
|
|
34
|
+
'CWE-668': { soc2: ['CC6.1', 'CC6.6'], iso27001: ['A.8.9', 'A.8.20'], nistAiRmf: ['GOVERN 1.4'] },
|
|
35
|
+
'CWE-798': { soc2: ['CC6.1', 'CC6.7'], iso27001: ['A.5.33', 'A.8.24'], nistAiRmf: [] },
|
|
36
|
+
'CWE-918': { soc2: ['CC6.1', 'CC6.6'], iso27001: ['A.8.20', 'A.8.28'], nistAiRmf: [] },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// OWASP Agentic → NIST AI RMF mapping (agent-specific)
|
|
40
|
+
const AGENTIC_MAP = {
|
|
41
|
+
'ASI01': { soc2: ['CC6.1', 'CC7.2'], iso27001: ['A.8.28', 'A.8.9'], nistAiRmf: ['MAP 1.5', 'MEASURE 2.6', 'MANAGE 2.2'] },
|
|
42
|
+
'ASI02': { soc2: ['CC6.1', 'CC6.3'], iso27001: ['A.8.2', 'A.8.9'], nistAiRmf: ['MAP 1.5', 'GOVERN 1.4', 'MANAGE 2.2'] },
|
|
43
|
+
'ASI03': { soc2: ['CC6.3'], iso27001: ['A.8.2', 'A.5.15'], nistAiRmf: ['GOVERN 1.4', 'MAP 3.4'] },
|
|
44
|
+
'ASI04': { soc2: ['CC6.6', 'CC7.1'], iso27001: ['A.5.19', 'A.5.21'], nistAiRmf: ['MAP 1.5', 'GOVERN 6.1'] },
|
|
45
|
+
'ASI05': { soc2: ['CC6.1', 'CC7.1'], iso27001: ['A.8.28', 'A.8.9'], nistAiRmf: ['MAP 1.5'] },
|
|
46
|
+
'ASI06': { soc2: ['CC6.1'], iso27001: ['A.8.11'], nistAiRmf: ['MEASURE 2.6', 'MANAGE 2.2'] },
|
|
47
|
+
'ASI07': { soc2: ['CC6.1', 'CC6.7'], iso27001: ['A.8.20', 'A.8.24'], nistAiRmf: ['MAP 1.5'] },
|
|
48
|
+
'ASI08': { soc2: ['CC7.4', 'CC7.5'], iso27001: ['A.5.30'], nistAiRmf: ['MANAGE 4.1'] },
|
|
49
|
+
'ASI09': { soc2: ['CC6.2'], iso27001: ['A.8.5', 'A.5.15'], nistAiRmf: ['GOVERN 1.7'] },
|
|
50
|
+
'ASI10': { soc2: ['CC7.2', 'CC7.4'], iso27001: ['A.8.9', 'A.5.30'], nistAiRmf: ['MANAGE 2.2', 'MANAGE 4.1'] },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// PUBLIC API
|
|
55
|
+
// =============================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Map a single finding to compliance controls.
|
|
59
|
+
* @param {object} finding - A finding with `cwe` and `owasp` fields.
|
|
60
|
+
* @returns {{ soc2: string[], iso27001: string[], nistAiRmf: string[] }}
|
|
61
|
+
*/
|
|
62
|
+
export function mapFindingToCompliance(finding) {
|
|
63
|
+
const result = { soc2: new Set(), iso27001: new Set(), nistAiRmf: new Set() };
|
|
64
|
+
|
|
65
|
+
// Map from CWE
|
|
66
|
+
const cwe = finding.cwe || finding.CWE;
|
|
67
|
+
if (cwe && CWE_MAP[cwe]) {
|
|
68
|
+
const m = CWE_MAP[cwe];
|
|
69
|
+
m.soc2.forEach(c => result.soc2.add(c));
|
|
70
|
+
m.iso27001.forEach(c => result.iso27001.add(c));
|
|
71
|
+
m.nistAiRmf.forEach(c => result.nistAiRmf.add(c));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Map from OWASP Agentic
|
|
75
|
+
const owasp = finding.owasp || finding.OWASP;
|
|
76
|
+
if (owasp && AGENTIC_MAP[owasp]) {
|
|
77
|
+
const m = AGENTIC_MAP[owasp];
|
|
78
|
+
m.soc2.forEach(c => result.soc2.add(c));
|
|
79
|
+
m.iso27001.forEach(c => result.iso27001.add(c));
|
|
80
|
+
m.nistAiRmf.forEach(c => result.nistAiRmf.add(c));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
soc2: [...result.soc2].sort(),
|
|
85
|
+
iso27001: [...result.iso27001].sort(),
|
|
86
|
+
nistAiRmf: [...result.nistAiRmf].sort(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Aggregate compliance mappings across all findings.
|
|
92
|
+
* @param {object[]} findings - Array of findings.
|
|
93
|
+
* @returns {{ soc2: object, iso27001: object, nistAiRmf: object, summary: object }}
|
|
94
|
+
*/
|
|
95
|
+
export function getComplianceSummary(findings) {
|
|
96
|
+
const soc2 = {};
|
|
97
|
+
const iso27001 = {};
|
|
98
|
+
const nistAiRmf = {};
|
|
99
|
+
|
|
100
|
+
for (const f of findings) {
|
|
101
|
+
const mapped = mapFindingToCompliance(f);
|
|
102
|
+
|
|
103
|
+
for (const ctrl of mapped.soc2) {
|
|
104
|
+
soc2[ctrl] = (soc2[ctrl] || 0) + 1;
|
|
105
|
+
}
|
|
106
|
+
for (const ctrl of mapped.iso27001) {
|
|
107
|
+
iso27001[ctrl] = (iso27001[ctrl] || 0) + 1;
|
|
108
|
+
}
|
|
109
|
+
for (const ctrl of mapped.nistAiRmf) {
|
|
110
|
+
nistAiRmf[ctrl] = (nistAiRmf[ctrl] || 0) + 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
soc2,
|
|
116
|
+
iso27001,
|
|
117
|
+
nistAiRmf,
|
|
118
|
+
summary: {
|
|
119
|
+
soc2Controls: Object.keys(soc2).length,
|
|
120
|
+
iso27001Controls: Object.keys(iso27001).length,
|
|
121
|
+
nistAiRmfControls: Object.keys(nistAiRmf).length,
|
|
122
|
+
totalFindings: findings.length,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
package/cli/utils/output.js
CHANGED
|
@@ -1,229 +1,230 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Output Utilities
|
|
3
|
-
* ================
|
|
4
|
-
*
|
|
5
|
-
* Consistent, pretty terminal output for the CLI.
|
|
6
|
-
* Uses chalk for colors and provides helper functions
|
|
7
|
-
* for common output patterns.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import chalk from 'chalk';
|
|
11
|
-
|
|
12
|
-
// =============================================================================
|
|
13
|
-
// SEVERITY COLORS
|
|
14
|
-
// =============================================================================
|
|
15
|
-
|
|
16
|
-
export const severityColors = {
|
|
17
|
-
critical: chalk.bgRed.white.bold,
|
|
18
|
-
high: chalk.red.bold,
|
|
19
|
-
medium: chalk.yellow,
|
|
20
|
-
low: chalk.blue
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export const severityIcons = {
|
|
24
|
-
critical: '\u2620\ufe0f ', // skull
|
|
25
|
-
high: '\u26a0\ufe0f ', // warning
|
|
26
|
-
medium: '\u26a1', // lightning
|
|
27
|
-
low: '\u2139\ufe0f ' // info
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// =============================================================================
|
|
31
|
-
// OUTPUT HELPERS
|
|
32
|
-
// =============================================================================
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Print a section header
|
|
36
|
-
*/
|
|
37
|
-
export function header(text) {
|
|
38
|
-
console.log();
|
|
39
|
-
console.log(chalk.cyan.bold('='.repeat(60)));
|
|
40
|
-
console.log(chalk.cyan.bold(` ${text}`));
|
|
41
|
-
console.log(chalk.cyan.bold('='.repeat(60)));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Print a subheader
|
|
46
|
-
*/
|
|
47
|
-
export function subheader(text) {
|
|
48
|
-
console.log();
|
|
49
|
-
console.log(chalk.white.bold(text));
|
|
50
|
-
console.log(chalk.gray('-'.repeat(text.length)));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Print a success message
|
|
55
|
-
*/
|
|
56
|
-
export function success(text) {
|
|
57
|
-
console.log(chalk.green('\u2714 ') + text);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Print a warning message
|
|
62
|
-
*/
|
|
63
|
-
export function warning(text) {
|
|
64
|
-
console.log(chalk.yellow('\u26a0 ') + text);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Print an error message
|
|
69
|
-
*/
|
|
70
|
-
export function error(text) {
|
|
71
|
-
console.log(chalk.red('\u2718 ') + text);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Print an info message
|
|
76
|
-
*/
|
|
77
|
-
export function info(text) {
|
|
78
|
-
console.log(chalk.blue('\u2139 ') + text);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const confidenceColors = {
|
|
82
|
-
high: chalk.red,
|
|
83
|
-
medium: chalk.yellow,
|
|
84
|
-
low: chalk.gray,
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Print a finding (secret detected)
|
|
89
|
-
*/
|
|
90
|
-
export function finding(file, line, patternName, severity, matched, description, confidence) {
|
|
91
|
-
const color = severityColors[severity] || chalk.white;
|
|
92
|
-
const icon = severityIcons[severity] || '';
|
|
93
|
-
const confColor = confidenceColors[confidence] || chalk.gray;
|
|
94
|
-
const confLabel = confidence ? ` ${chalk.gray('Confidence:')} ${confColor(confidence)}` : '';
|
|
95
|
-
|
|
96
|
-
console.log();
|
|
97
|
-
console.log(chalk.white.bold(`${file}:${line}`));
|
|
98
|
-
console.log(` ${icon}${color(`[${severity.toUpperCase()}]`)} ${chalk.white(patternName)}`);
|
|
99
|
-
console.log(` ${chalk.gray('Found:')} ${chalk.yellow(maskSecret(matched))}`);
|
|
100
|
-
if (confLabel) console.log(confLabel);
|
|
101
|
-
console.log(` ${chalk.gray('Why:')} ${description}`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Print a vulnerability finding (code issue — show matched code, not masked)
|
|
106
|
-
*/
|
|
107
|
-
export function vulnerabilityFinding(file, line, patternName, severity, matched, description) {
|
|
108
|
-
const color = severityColors[severity] || chalk.white;
|
|
109
|
-
const icon = severityIcons[severity] || '';
|
|
110
|
-
const snippet = matched.length > 80 ? matched.slice(0, 80) + '…' : matched;
|
|
111
|
-
|
|
112
|
-
console.log();
|
|
113
|
-
console.log(chalk.white.bold(`${file}:${line}`));
|
|
114
|
-
console.log(` ${icon}${color(`[${severity.toUpperCase()}]`)} ${chalk.white(patternName)}`);
|
|
115
|
-
console.log(` ${chalk.gray('Code:')} ${chalk.cyan(snippet)}`);
|
|
116
|
-
console.log(` ${chalk.gray('Why:')} ${description}`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Mask the middle of a secret for safe display
|
|
121
|
-
*/
|
|
122
|
-
export function maskSecret(secret) {
|
|
123
|
-
if (secret.length <= 6) {
|
|
124
|
-
return '***';
|
|
125
|
-
}
|
|
126
|
-
if (secret.length <= 12) {
|
|
127
|
-
return secret.substring(0, 3) + '***';
|
|
128
|
-
}
|
|
129
|
-
return secret.substring(0, 4) + '***' + secret.substring(secret.length - 4);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Print a summary box
|
|
134
|
-
*
|
|
135
|
-
* stats can include:
|
|
136
|
-
* total, critical, high, medium, filesScanned
|
|
137
|
-
* secretsTotal (optional), vulnsTotal (optional)
|
|
138
|
-
*/
|
|
139
|
-
export function summary(stats) {
|
|
140
|
-
console.log();
|
|
141
|
-
console.log(chalk.cyan('='.repeat(60)));
|
|
142
|
-
|
|
143
|
-
if (stats.total === 0) {
|
|
144
|
-
console.log(chalk.green.bold(' \u2714 No issues detected!'));
|
|
145
|
-
} else {
|
|
146
|
-
const secretsTotal = stats.secretsTotal ?? stats.total;
|
|
147
|
-
const vulnsTotal = stats.vulnsTotal ?? 0;
|
|
148
|
-
|
|
149
|
-
if (secretsTotal > 0) {
|
|
150
|
-
console.log(chalk.red.bold(` \u26a0 Found ${secretsTotal} secret(s)`));
|
|
151
|
-
}
|
|
152
|
-
if (vulnsTotal > 0) {
|
|
153
|
-
console.log(chalk.yellow.bold(` \u26a0 Found ${vulnsTotal} code vulnerability/vulnerabilities`));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (stats.critical > 0) {
|
|
157
|
-
console.log(chalk.red(` \u2022 Critical: ${stats.critical}`));
|
|
158
|
-
}
|
|
159
|
-
if (stats.high > 0) {
|
|
160
|
-
console.log(chalk.red(` \u2022 High: ${stats.high}`));
|
|
161
|
-
}
|
|
162
|
-
if (stats.medium > 0) {
|
|
163
|
-
console.log(chalk.yellow(` \u2022 Medium: ${stats.medium}`));
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
console.log(chalk.gray(` Files scanned: ${stats.filesScanned}`));
|
|
168
|
-
console.log(chalk.cyan('='.repeat(60)));
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
console.log(
|
|
177
|
-
console.log();
|
|
178
|
-
console.log(
|
|
179
|
-
console.log(chalk.white('
|
|
180
|
-
console.log(chalk.white('
|
|
181
|
-
console.log();
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
console.log(
|
|
190
|
-
console.log();
|
|
191
|
-
console.log(
|
|
192
|
-
console.log(chalk.white('
|
|
193
|
-
console.log(chalk.white('
|
|
194
|
-
console.log(chalk.
|
|
195
|
-
console.log(chalk.gray(' \u2022
|
|
196
|
-
console.log(chalk.gray(' \u2022
|
|
197
|
-
console.log();
|
|
198
|
-
console.log(
|
|
199
|
-
console.log(chalk.
|
|
200
|
-
console.log(chalk.gray('
|
|
201
|
-
console.log();
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Output Utilities
|
|
3
|
+
* ================
|
|
4
|
+
*
|
|
5
|
+
* Consistent, pretty terminal output for the CLI.
|
|
6
|
+
* Uses chalk for colors and provides helper functions
|
|
7
|
+
* for common output patterns.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// SEVERITY COLORS
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
export const severityColors = {
|
|
17
|
+
critical: chalk.bgRed.white.bold,
|
|
18
|
+
high: chalk.red.bold,
|
|
19
|
+
medium: chalk.yellow,
|
|
20
|
+
low: chalk.blue
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const severityIcons = {
|
|
24
|
+
critical: '\u2620\ufe0f ', // skull
|
|
25
|
+
high: '\u26a0\ufe0f ', // warning
|
|
26
|
+
medium: '\u26a1', // lightning
|
|
27
|
+
low: '\u2139\ufe0f ' // info
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// OUTPUT HELPERS
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Print a section header
|
|
36
|
+
*/
|
|
37
|
+
export function header(text) {
|
|
38
|
+
console.log();
|
|
39
|
+
console.log(chalk.cyan.bold('='.repeat(60)));
|
|
40
|
+
console.log(chalk.cyan.bold(` ${text}`));
|
|
41
|
+
console.log(chalk.cyan.bold('='.repeat(60)));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Print a subheader
|
|
46
|
+
*/
|
|
47
|
+
export function subheader(text) {
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(chalk.white.bold(text));
|
|
50
|
+
console.log(chalk.gray('-'.repeat(text.length)));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Print a success message
|
|
55
|
+
*/
|
|
56
|
+
export function success(text) {
|
|
57
|
+
console.log(chalk.green('\u2714 ') + text);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Print a warning message
|
|
62
|
+
*/
|
|
63
|
+
export function warning(text) {
|
|
64
|
+
console.log(chalk.yellow('\u26a0 ') + text);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Print an error message
|
|
69
|
+
*/
|
|
70
|
+
export function error(text) {
|
|
71
|
+
console.log(chalk.red('\u2718 ') + text);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Print an info message
|
|
76
|
+
*/
|
|
77
|
+
export function info(text) {
|
|
78
|
+
console.log(chalk.blue('\u2139 ') + text);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const confidenceColors = {
|
|
82
|
+
high: chalk.red,
|
|
83
|
+
medium: chalk.yellow,
|
|
84
|
+
low: chalk.gray,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Print a finding (secret detected)
|
|
89
|
+
*/
|
|
90
|
+
export function finding(file, line, patternName, severity, matched, description, confidence) {
|
|
91
|
+
const color = severityColors[severity] || chalk.white;
|
|
92
|
+
const icon = severityIcons[severity] || '';
|
|
93
|
+
const confColor = confidenceColors[confidence] || chalk.gray;
|
|
94
|
+
const confLabel = confidence ? ` ${chalk.gray('Confidence:')} ${confColor(confidence)}` : '';
|
|
95
|
+
|
|
96
|
+
console.log();
|
|
97
|
+
console.log(chalk.white.bold(`${file}:${line}`));
|
|
98
|
+
console.log(` ${icon}${color(`[${severity.toUpperCase()}]`)} ${chalk.white(patternName)}`);
|
|
99
|
+
console.log(` ${chalk.gray('Found:')} ${chalk.yellow(maskSecret(matched))}`);
|
|
100
|
+
if (confLabel) console.log(confLabel);
|
|
101
|
+
console.log(` ${chalk.gray('Why:')} ${description}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Print a vulnerability finding (code issue — show matched code, not masked)
|
|
106
|
+
*/
|
|
107
|
+
export function vulnerabilityFinding(file, line, patternName, severity, matched, description) {
|
|
108
|
+
const color = severityColors[severity] || chalk.white;
|
|
109
|
+
const icon = severityIcons[severity] || '';
|
|
110
|
+
const snippet = matched.length > 80 ? matched.slice(0, 80) + '…' : matched;
|
|
111
|
+
|
|
112
|
+
console.log();
|
|
113
|
+
console.log(chalk.white.bold(`${file}:${line}`));
|
|
114
|
+
console.log(` ${icon}${color(`[${severity.toUpperCase()}]`)} ${chalk.white(patternName)}`);
|
|
115
|
+
console.log(` ${chalk.gray('Code:')} ${chalk.cyan(snippet)}`);
|
|
116
|
+
console.log(` ${chalk.gray('Why:')} ${description}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Mask the middle of a secret for safe display
|
|
121
|
+
*/
|
|
122
|
+
export function maskSecret(secret) {
|
|
123
|
+
if (secret.length <= 6) {
|
|
124
|
+
return '***';
|
|
125
|
+
}
|
|
126
|
+
if (secret.length <= 12) {
|
|
127
|
+
return secret.substring(0, 3) + '***';
|
|
128
|
+
}
|
|
129
|
+
return secret.substring(0, 4) + '***' + secret.substring(secret.length - 4);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Print a summary box
|
|
134
|
+
*
|
|
135
|
+
* stats can include:
|
|
136
|
+
* total, critical, high, medium, filesScanned
|
|
137
|
+
* secretsTotal (optional), vulnsTotal (optional)
|
|
138
|
+
*/
|
|
139
|
+
export function summary(stats) {
|
|
140
|
+
console.log();
|
|
141
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
142
|
+
|
|
143
|
+
if (stats.total === 0) {
|
|
144
|
+
console.log(chalk.green.bold(' \u2714 No issues detected!'));
|
|
145
|
+
} else {
|
|
146
|
+
const secretsTotal = stats.secretsTotal ?? stats.total;
|
|
147
|
+
const vulnsTotal = stats.vulnsTotal ?? 0;
|
|
148
|
+
|
|
149
|
+
if (secretsTotal > 0) {
|
|
150
|
+
console.log(chalk.red.bold(` \u26a0 Found ${secretsTotal} secret(s)`));
|
|
151
|
+
}
|
|
152
|
+
if (vulnsTotal > 0) {
|
|
153
|
+
console.log(chalk.yellow.bold(` \u26a0 Found ${vulnsTotal} code vulnerability/vulnerabilities`));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (stats.critical > 0) {
|
|
157
|
+
console.log(chalk.red(` \u2022 Critical: ${stats.critical}`));
|
|
158
|
+
}
|
|
159
|
+
if (stats.high > 0) {
|
|
160
|
+
console.log(chalk.red(` \u2022 High: ${stats.high}`));
|
|
161
|
+
}
|
|
162
|
+
if (stats.medium > 0) {
|
|
163
|
+
console.log(chalk.yellow(` \u2022 Medium: ${stats.medium}`));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(chalk.gray(` Files scanned: ${stats.filesScanned}`));
|
|
168
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
169
|
+
console.log(chalk.gray(` Cloud dashboard & team scanning: `) + chalk.cyan('https://shipsafecli.com'));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Print recommended actions after finding code vulnerabilities
|
|
174
|
+
*/
|
|
175
|
+
export function vulnRecommendations() {
|
|
176
|
+
console.log();
|
|
177
|
+
console.log(chalk.yellow.bold('Code Vulnerability Actions:'));
|
|
178
|
+
console.log();
|
|
179
|
+
console.log(chalk.white('1.') + ' Fix the flagged code patterns (see "Why" descriptions above)');
|
|
180
|
+
console.log(chalk.white('2.') + ' Use # ship-safe-ignore on lines that are safe (e.g. internal tools, controlled input)');
|
|
181
|
+
console.log(chalk.white('3.') + ' Run npx ship-safe checklist for a full launch-day security review');
|
|
182
|
+
console.log();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Print recommended actions after finding secrets
|
|
187
|
+
*/
|
|
188
|
+
export function recommendations() {
|
|
189
|
+
console.log();
|
|
190
|
+
console.log(chalk.cyan.bold('Recommended Actions:'));
|
|
191
|
+
console.log();
|
|
192
|
+
console.log(chalk.white('1.') + ' Move secrets to environment variables (.env file)');
|
|
193
|
+
console.log(chalk.white('2.') + ' Add .env to your .gitignore');
|
|
194
|
+
console.log(chalk.white('3.') + chalk.yellow(' If already committed:'));
|
|
195
|
+
console.log(chalk.gray(' \u2022 Rotate the compromised credentials immediately'));
|
|
196
|
+
console.log(chalk.gray(' \u2022 Use git-filter-repo or BFG Repo-Cleaner to remove from history'));
|
|
197
|
+
console.log(chalk.gray(' \u2022 Remember: deleting doesn\'t remove git history!'));
|
|
198
|
+
console.log();
|
|
199
|
+
console.log(chalk.white('4.') + ' Set up pre-commit hooks to catch this automatically:');
|
|
200
|
+
console.log(chalk.gray(' npm install --save-dev husky'));
|
|
201
|
+
console.log(chalk.gray(' npx husky add .husky/pre-commit "npx ship-safe scan ."'));
|
|
202
|
+
console.log();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Print a checklist item
|
|
207
|
+
*/
|
|
208
|
+
export function checklistItem(number, title, checked = null) {
|
|
209
|
+
const checkbox = checked === null
|
|
210
|
+
? chalk.gray('[ ]')
|
|
211
|
+
: checked
|
|
212
|
+
? chalk.green('[\u2714]')
|
|
213
|
+
: chalk.red('[\u2718]');
|
|
214
|
+
|
|
215
|
+
console.log(`${checkbox} ${chalk.white.bold(`${number}.`)} ${title}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Print progress (for verbose mode)
|
|
220
|
+
*/
|
|
221
|
+
export function progress(text) {
|
|
222
|
+
process.stdout.write(chalk.gray(`\r${text}`));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Clear the current line
|
|
227
|
+
*/
|
|
228
|
+
export function clearLine() {
|
|
229
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
230
|
+
}
|