ship-safe 6.2.0 → 6.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/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  <a href="https://nodejs.org"><img src="https://img.shields.io/node/v/ship-safe" alt="Node.js version" /></a>
12
12
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
13
13
  <a href="https://github.com/asamassekou10/ship-safe/stargazers"><img src="https://img.shields.io/github/stars/asamassekou10/ship-safe?style=social" alt="GitHub stars" /></a>
14
+ <a href="https://github.com/sponsors/asamassekou10"><img src="https://img.shields.io/badge/Sponsor-%E2%9D%A4-ea4aaa?logo=github" alt="Sponsor" /></a>
14
15
  </p>
15
16
 
16
17
  ---
@@ -720,6 +721,18 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
720
721
 
721
722
  ---
722
723
 
724
+ ## Sponsors
725
+
726
+ Ship Safe is MIT-licensed and free forever. If it saves you time or helps you ship more securely, consider sponsoring — it helps keep the project maintained and growing.
727
+
728
+ <p align="center">
729
+ <a href="https://github.com/sponsors/asamassekou10">
730
+ <img src="https://img.shields.io/badge/Sponsor%20Ship%20Safe-%E2%9D%A4-ea4aaa?style=for-the-badge&logo=github" alt="Sponsor Ship Safe" />
731
+ </a>
732
+ </p>
733
+
734
+ ---
735
+
723
736
  ## License
724
737
 
725
738
  MIT - Use it, share it, secure your stuff.
@@ -26,6 +26,7 @@ export { PIIComplianceAgent } from './pii-compliance-agent.js';
26
26
  export { VibeCodingAgent } from './vibe-coding-agent.js';
27
27
  export { ExceptionHandlerAgent } from './exception-handler-agent.js';
28
28
  export { AgentConfigScanner } from './agent-config-scanner.js';
29
+ export { LegalRiskAgent, LEGALLY_RISKY_PACKAGES } from './legal-risk-agent.js';
29
30
  export { ABOMGenerator } from './abom-generator.js';
30
31
  export { VerifierAgent } from './verifier-agent.js';
31
32
  export { DeepAnalyzer } from './deep-analyzer.js';
@@ -0,0 +1,302 @@
1
+ /**
2
+ * LegalRiskAgent
3
+ * ==============
4
+ *
5
+ * Scans project dependency manifests for packages that carry legal risk:
6
+ * active DMCA takedowns, known leaked-source derivatives, IP disputes,
7
+ * or license violations.
8
+ *
9
+ * This is a separate threat category from security IOCs — the danger is
10
+ * not malware, but legal liability for shipping the dependency.
11
+ *
12
+ * Supported manifests:
13
+ * npm/yarn/pnpm → package.json
14
+ * Python → requirements.txt, pyproject.toml
15
+ * Rust → Cargo.toml
16
+ * Go → go.mod
17
+ *
18
+ * USAGE:
19
+ * ship-safe legal .
20
+ * ship-safe audit . --include-legal
21
+ */
22
+
23
+ import fs from 'fs';
24
+ import path from 'path';
25
+ import { BaseAgent, createFinding } from './base-agent.js';
26
+
27
+ // =============================================================================
28
+ // LEGALLY RISKY PACKAGES
29
+ // Format: { name, versions, ecosystem, risk, severity, detail, references }
30
+ //
31
+ // versions: array of specific bad versions, or '*' for all versions
32
+ // ecosystem: 'npm' | 'pypi' | 'cargo' | 'go' | '*'
33
+ // risk: 'dmca' | 'ip-dispute' | 'leaked-source' | 'license-violation'
34
+ // =============================================================================
35
+ export const LEGALLY_RISKY_PACKAGES = [
36
+ // ---------------------------------------------------------------------------
37
+ // Claude Code source leak (March 31 2026)
38
+ // Anthropic's Claude Code source was accidentally leaked. Several repos
39
+ // appeared immediately; Anthropic filed DMCA takedowns but derivatives
40
+ // remain online. Shipping any of these exposes you to IP liability.
41
+ // ---------------------------------------------------------------------------
42
+ {
43
+ name: 'claw-code',
44
+ versions: '*',
45
+ ecosystem: 'npm',
46
+ risk: 'dmca',
47
+ severity: 'high',
48
+ detail:
49
+ 'Derived from leaked Anthropic Claude Code source (March 2026). ' +
50
+ 'Anthropic has filed DMCA takedown notices. Shipping this package ' +
51
+ 'may expose your project to IP infringement liability.',
52
+ references: [
53
+ 'https://cybernews.com/security/anthropic-claude-code-source-leak/',
54
+ 'https://venturebeat.com/technology/claude-codes-source-code-appears-to-have-leaked-heres-what-we-know',
55
+ ],
56
+ },
57
+ {
58
+ name: 'claw-code-js',
59
+ versions: '*',
60
+ ecosystem: 'npm',
61
+ risk: 'leaked-source',
62
+ severity: 'high',
63
+ detail:
64
+ 'JavaScript port derived from the leaked Anthropic Claude Code source (March 2026). ' +
65
+ 'Under active DMCA enforcement. Contains Anthropic proprietary IP.',
66
+ references: [
67
+ 'https://cybernews.com/tech/claude-code-leak-spawns-fastest-github-repo/',
68
+ ],
69
+ },
70
+ {
71
+ name: 'claude-code-oss',
72
+ versions: '*',
73
+ ecosystem: 'npm',
74
+ risk: 'leaked-source',
75
+ severity: 'high',
76
+ detail:
77
+ 'Open-source mirror of the leaked Claude Code source (March 2026). ' +
78
+ 'Despite "open-source" branding, the underlying code is Anthropic proprietary IP ' +
79
+ 'and DMCA takedowns are in progress.',
80
+ references: [
81
+ 'https://cybernews.com/security/anthropic-claude-code-source-leak/',
82
+ ],
83
+ },
84
+ // ---------------------------------------------------------------------------
85
+ // License violations — well-known cases
86
+ // ---------------------------------------------------------------------------
87
+ {
88
+ name: 'faker',
89
+ versions: ['6.6.6'],
90
+ ecosystem: 'npm',
91
+ risk: 'license-violation',
92
+ severity: 'medium',
93
+ detail:
94
+ 'faker@6.6.6 was deliberately sabotaged by its maintainer (January 2022). ' +
95
+ 'The package prints an infinite loop of gibberish. Replaced by @faker-js/faker ' +
96
+ 'which is community-maintained under MIT.',
97
+ references: [
98
+ 'https://www.bleepingcomputer.com/news/security/dev-corrupts-npm-libs-colors-and-faker-breaking-thousands-of-apps/',
99
+ ],
100
+ },
101
+ {
102
+ name: 'colors',
103
+ versions: ['1.4.44-liberty-2'],
104
+ ecosystem: 'npm',
105
+ risk: 'license-violation',
106
+ severity: 'medium',
107
+ detail:
108
+ 'colors@1.4.44-liberty-2 was a malicious release by the maintainer that ' +
109
+ 'deliberately printed an infinite loop. Use colors@1.4.0 or the maintained fork.',
110
+ references: [
111
+ 'https://www.bleepingcomputer.com/news/security/dev-corrupts-npm-libs-colors-and-faker-breaking-thousands-of-apps/',
112
+ ],
113
+ },
114
+ ];
115
+
116
+ // =============================================================================
117
+ // AGENT
118
+ // =============================================================================
119
+
120
+ export class LegalRiskAgent extends BaseAgent {
121
+ constructor() {
122
+ super('LegalRiskAgent', 'Legal risk audit: DMCA, IP disputes, leaked source in dependencies', 'legal');
123
+ }
124
+
125
+ async analyze(context) {
126
+ const { rootPath } = context;
127
+ const findings = [];
128
+
129
+ findings.push(...this._scanNpm(rootPath));
130
+ findings.push(...this._scanPython(rootPath));
131
+ findings.push(...this._scanCargo(rootPath));
132
+ findings.push(...this._scanGoMod(rootPath));
133
+
134
+ return findings;
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // npm / yarn / pnpm — package.json
139
+ // ---------------------------------------------------------------------------
140
+ _scanNpm(rootPath) {
141
+ const findings = [];
142
+ const pkgPath = path.join(rootPath, 'package.json');
143
+ if (!fs.existsSync(pkgPath)) return findings;
144
+
145
+ let pkg;
146
+ try {
147
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
148
+ } catch {
149
+ return findings;
150
+ }
151
+
152
+ const allDeps = {
153
+ ...(pkg.dependencies || {}),
154
+ ...(pkg.devDependencies || {}),
155
+ ...(pkg.optionalDependencies || {}),
156
+ ...(pkg.peerDependencies || {}),
157
+ };
158
+
159
+ for (const [name, version] of Object.entries(allDeps)) {
160
+ const entry = LEGALLY_RISKY_PACKAGES.find(
161
+ e => e.name === name && (e.ecosystem === 'npm' || e.ecosystem === '*')
162
+ );
163
+ if (!entry) continue;
164
+
165
+ const bare = String(version).replace(/^[\^~>=<]+/, '').trim();
166
+ const versionMatches =
167
+ entry.versions === '*' || entry.versions.includes(bare);
168
+
169
+ if (versionMatches) {
170
+ findings.push(this._makeFinding(pkgPath, name, bare, entry));
171
+ }
172
+ }
173
+
174
+ return findings;
175
+ }
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // Python — requirements.txt
179
+ // ---------------------------------------------------------------------------
180
+ _scanPython(rootPath) {
181
+ const findings = [];
182
+ const reqPath = path.join(rootPath, 'requirements.txt');
183
+ if (!fs.existsSync(reqPath)) return findings;
184
+
185
+ const lines = (this.readFile(reqPath) || '').split('\n');
186
+ for (let i = 0; i < lines.length; i++) {
187
+ const line = lines[i].trim();
188
+ if (!line || line.startsWith('#')) continue;
189
+
190
+ // Match: package==version or package>=version etc., or bare package name
191
+ const m = line.match(/^([\w.-]+)\s*(?:[=<>!~]=?\s*([\d.\w]+))?/);
192
+ if (!m) continue;
193
+ const [, name, version = '*'] = m;
194
+
195
+ const entry = LEGALLY_RISKY_PACKAGES.find(
196
+ e => e.name.toLowerCase() === name.toLowerCase() &&
197
+ (e.ecosystem === 'pypi' || e.ecosystem === '*')
198
+ );
199
+ if (!entry) continue;
200
+
201
+ const versionMatches =
202
+ entry.versions === '*' || entry.versions.includes(version);
203
+ if (versionMatches) {
204
+ findings.push(this._makeFinding(reqPath, name, version, entry));
205
+ }
206
+ }
207
+
208
+ return findings;
209
+ }
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // Rust — Cargo.toml
213
+ // ---------------------------------------------------------------------------
214
+ _scanCargo(rootPath) {
215
+ const findings = [];
216
+ const cargoPath = path.join(rootPath, 'Cargo.toml');
217
+ if (!fs.existsSync(cargoPath)) return findings;
218
+
219
+ const content = this.readFile(cargoPath) || '';
220
+ // Match lines like: package-name = "1.2.3" or package-name = { version = "1.2.3" }
221
+ const depPattern = /^\s*([\w-]+)\s*=\s*(?:"([\d.\w^~>=<*]+)"|{[^}]*version\s*=\s*"([\d.\w^~>=<*]+)")/gm;
222
+ let match;
223
+ while ((match = depPattern.exec(content)) !== null) {
224
+ const name = match[1];
225
+ const version = (match[2] || match[3] || '*').replace(/^[\^~>=<]+/, '').trim();
226
+
227
+ const entry = LEGALLY_RISKY_PACKAGES.find(
228
+ e => e.name === name && (e.ecosystem === 'cargo' || e.ecosystem === '*')
229
+ );
230
+ if (!entry) continue;
231
+
232
+ const versionMatches = entry.versions === '*' || entry.versions.includes(version);
233
+ if (versionMatches) {
234
+ findings.push(this._makeFinding(cargoPath, name, version, entry));
235
+ }
236
+ }
237
+
238
+ return findings;
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // Go — go.mod
243
+ // ---------------------------------------------------------------------------
244
+ _scanGoMod(rootPath) {
245
+ const findings = [];
246
+ const goModPath = path.join(rootPath, 'go.mod');
247
+ if (!fs.existsSync(goModPath)) return findings;
248
+
249
+ const lines = (this.readFile(goModPath) || '').split('\n');
250
+ for (const line of lines) {
251
+ const m = line.trim().match(/^([\w./\-]+)\s+(v[\d.]+)/);
252
+ if (!m) continue;
253
+ const [, name, version] = m;
254
+
255
+ const entry = LEGALLY_RISKY_PACKAGES.find(
256
+ e => e.name === name && (e.ecosystem === 'go' || e.ecosystem === '*')
257
+ );
258
+ if (!entry) continue;
259
+
260
+ const bare = version.replace(/^v/, '');
261
+ const versionMatches = entry.versions === '*' || entry.versions.includes(bare);
262
+ if (versionMatches) {
263
+ findings.push(this._makeFinding(goModPath, name, bare, entry));
264
+ }
265
+ }
266
+
267
+ return findings;
268
+ }
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // Finding factory
272
+ // ---------------------------------------------------------------------------
273
+ _makeFinding(file, name, version, entry) {
274
+ const riskLabel = {
275
+ dmca: 'DMCA Takedown',
276
+ 'ip-dispute': 'IP Dispute',
277
+ 'leaked-source': 'Leaked Source',
278
+ 'license-violation': 'License Violation',
279
+ }[entry.risk] || entry.risk;
280
+
281
+ const versionStr = version === '*' ? '(any version)' : `@${version}`;
282
+
283
+ return createFinding({
284
+ file,
285
+ line: 0,
286
+ severity: entry.severity,
287
+ category: 'legal',
288
+ rule: `LEGAL_RISK_${entry.risk.toUpperCase().replace(/-/g, '_')}`,
289
+ title: `[${riskLabel}] ${name}${versionStr}`,
290
+ description: entry.detail,
291
+ matched: version === '*' ? name : `${name}@${version}`,
292
+ confidence: 'high',
293
+ fix:
294
+ `Remove ${name} from your dependencies. ` +
295
+ (entry.references.length > 0
296
+ ? `See: ${entry.references[0]}`
297
+ : ''),
298
+ });
299
+ }
300
+ }
301
+
302
+ export default LegalRiskAgent;
@@ -45,6 +45,7 @@ import { scanSkillCommand } from '../commands/scan-skill.js';
45
45
  import { abomCommand } from '../commands/abom.js';
46
46
  import { updateIntelCommand } from '../commands/update-intel.js';
47
47
  import { hooksCommand } from '../commands/hooks.js';
48
+ import { legalCommand } from '../commands/legal.js';
48
49
  import { ABOMGenerator } from '../agents/abom-generator.js';
49
50
  import { PolicyEngine } from '../agents/policy-engine.js';
50
51
  import { SBOMGenerator } from '../agents/sbom-generator.js';
@@ -220,6 +221,7 @@ program
220
221
  .option('--base-url <url>', 'Custom OpenAI-compatible endpoint (e.g. http://localhost:1234/v1/chat/completions)')
221
222
  .option('--budget <cents>', 'Max spend in cents for deep analysis (default: 50)', parseInt)
222
223
  .option('--verify', 'Check if leaked secrets are still active (probes provider APIs)')
224
+ .option('--include-legal', 'Also run the legal risk scan (DMCA, leaked source, IP disputes)')
223
225
  .option('-v, --verbose', 'Verbose output')
224
226
  .action(auditCommand);
225
227
 
@@ -392,6 +394,15 @@ How it works:
392
394
  `)
393
395
  .action(hooksCommand);
394
396
 
397
+ // -----------------------------------------------------------------------------
398
+ // LEGAL COMMAND
399
+ // -----------------------------------------------------------------------------
400
+ program
401
+ .command('legal [path]')
402
+ .description('Legal risk audit: DMCA notices, leaked-source derivatives, IP disputes in dependencies')
403
+ .option('--json', 'Output results as JSON')
404
+ .action(legalCommand);
405
+
395
406
  // -----------------------------------------------------------------------------
396
407
  // UPDATE-INTEL COMMAND
397
408
  // -----------------------------------------------------------------------------
@@ -430,6 +441,7 @@ if (process.argv.length === 2) {
430
441
  console.log(chalk.white(' npx ship-safe scan-skill <u>') + chalk.gray('# Vet a skill before installing'));
431
442
  console.log(chalk.white(' npx ship-safe abom . ') + chalk.gray('# Agent Bill of Materials (CycloneDX)'));
432
443
  console.log(chalk.white(' npx ship-safe sbom . ') + chalk.gray('# Generate CycloneDX SBOM (CRA-ready)'));
444
+ console.log(chalk.white(' npx ship-safe legal . ') + chalk.gray('# Legal risk audit: DMCA, leaked source, IP disputes'));
433
445
  console.log(chalk.white(' npx ship-safe update-intel ') + chalk.gray('# Update threat intelligence feed'));
434
446
  console.log(chalk.white(' npx ship-safe policy init ') + chalk.gray('# Create security policy template'));
435
447
  console.log(chalk.white(' npx ship-safe doctor ') + chalk.gray('# Check environment and configuration'));
@@ -18,6 +18,7 @@ import chalk from 'chalk';
18
18
  import ora from 'ora';
19
19
  import fg from 'fast-glob';
20
20
  import { buildOrchestrator } from '../agents/index.js';
21
+ import { LegalRiskAgent } from '../agents/legal-risk-agent.js';
21
22
  import { ScoringEngine } from '../agents/scoring-engine.js';
22
23
  import { PolicyEngine } from '../agents/policy-engine.js';
23
24
  import { HTMLReporter } from '../agents/html-reporter.js';
@@ -56,6 +57,7 @@ const CATEGORY_LABELS = {
56
57
  'supply-chain': 'Supply Chain',
57
58
  api: 'API Security',
58
59
  llm: 'AI/LLM Security',
60
+ legal: 'Legal Risk',
59
61
  };
60
62
 
61
63
  const EFFORT_MAP = {
@@ -67,6 +69,7 @@ const EFFORT_MAP = {
67
69
  'supply-chain': 'medium',
68
70
  api: 'medium',
69
71
  llm: 'high',
72
+ legal: 'low',
70
73
  };
71
74
 
72
75
  // =============================================================================
@@ -239,11 +242,28 @@ export async function auditCommand(targetPath = '.', options = {}) {
239
242
  console.log(chalk.gray(' [Phase 3/4] Dependencies: skipped (--no-deps)'));
240
243
  }
241
244
 
245
+ // ── Phase 3b: Legal Risk Scan (opt-in) ───────────────────────────────────
246
+ let legalFindings = [];
247
+ if (options.includeLegal) {
248
+ const legalSpinner = machineOutput ? null : ora({ text: chalk.white('[Phase 3b] Legal risk scan…'), color: 'cyan' }).start();
249
+ try {
250
+ const legalAgent = new LegalRiskAgent();
251
+ legalFindings = await legalAgent.analyze({ rootPath: absolutePath, files: allFiles });
252
+ if (legalSpinner) legalSpinner.succeed(
253
+ legalFindings.length === 0
254
+ ? chalk.green('[Phase 3b] Legal: clean')
255
+ : chalk.yellow(`[Phase 3b] Legal: ${legalFindings.length} finding(s)`)
256
+ );
257
+ } catch {
258
+ if (legalSpinner) legalSpinner.succeed(chalk.gray('[Phase 3b] Legal: skipped'));
259
+ }
260
+ }
261
+
242
262
  // ── Phase 4: Merge, Score, and Build Plan ─────────────────────────────────
243
263
  const scoreSpinner = machineOutput ? null : ora({ text: chalk.white('[Phase 4/4] Computing security score...'), color: 'cyan' }).start();
244
264
 
245
- // Merge secret findings + agent findings, deduplicate
246
- const allFindings = deduplicateFindings([...secretFindings, ...agentFindings]);
265
+ // Merge secret findings + agent findings + legal findings, deduplicate
266
+ const allFindings = deduplicateFindings([...secretFindings, ...agentFindings, ...legalFindings]);
247
267
 
248
268
  // Apply policy
249
269
  const policy = PolicyEngine.load(absolutePath);
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Legal Command
3
+ * =============
4
+ *
5
+ * Scans dependency manifests for packages that carry legal risk:
6
+ * active DMCA takedowns, leaked-source derivatives, IP disputes,
7
+ * and license violations.
8
+ *
9
+ * USAGE:
10
+ * ship-safe legal [path] Scan for legally risky dependencies
11
+ * ship-safe legal . --json JSON output
12
+ *
13
+ * EXIT CODES:
14
+ * 0 Clean — no legally risky packages found
15
+ * 1 Findings — one or more legally risky packages detected
16
+ */
17
+
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+ import chalk from 'chalk';
21
+ import ora from 'ora';
22
+ import { LegalRiskAgent } from '../agents/legal-risk-agent.js';
23
+ import * as output from '../utils/output.js';
24
+
25
+ // =============================================================================
26
+ // RISK LABELS & COLORS
27
+ // =============================================================================
28
+
29
+ const RISK_COLORS = {
30
+ dmca: chalk.red.bold,
31
+ 'ip-dispute': chalk.red,
32
+ 'leaked-source': chalk.yellow.bold,
33
+ 'license-violation': chalk.yellow,
34
+ };
35
+
36
+ const RISK_LABELS = {
37
+ dmca: 'DMCA Takedown',
38
+ 'ip-dispute': 'IP Dispute',
39
+ 'leaked-source': 'Leaked Source',
40
+ 'license-violation': 'License Violation',
41
+ };
42
+
43
+ const SEVERITY_COLORS = {
44
+ critical: chalk.bgRed.white.bold,
45
+ high: chalk.red.bold,
46
+ medium: chalk.yellow,
47
+ low: chalk.blue,
48
+ };
49
+
50
+ // =============================================================================
51
+ // MAIN COMMAND
52
+ // =============================================================================
53
+
54
+ export async function legalCommand(targetPath = '.', options = {}) {
55
+ const absolutePath = path.resolve(targetPath);
56
+
57
+ if (!fs.existsSync(absolutePath)) {
58
+ output.error(`Path does not exist: ${absolutePath}`);
59
+ process.exit(1);
60
+ }
61
+
62
+ if (!options.json) {
63
+ console.log();
64
+ output.header('Legal Risk Audit');
65
+ console.log(chalk.gray(' Scanning for DMCA notices, leaked-source derivatives, and IP disputes'));
66
+ console.log();
67
+ }
68
+
69
+ // ── Run the agent ──────────────────────────────────────────────────────────
70
+ const spinner = options.json
71
+ ? null
72
+ : ora({ text: 'Scanning dependency manifests…', color: 'cyan' }).start();
73
+
74
+ const agent = new LegalRiskAgent();
75
+ let findings = [];
76
+
77
+ try {
78
+ findings = await agent.analyze({ rootPath: absolutePath, files: [] });
79
+ if (spinner) spinner.stop();
80
+ } catch (err) {
81
+ if (spinner) spinner.stop();
82
+ output.error(`Legal scan failed: ${err.message}`);
83
+ process.exit(1);
84
+ }
85
+
86
+ // ── JSON output ────────────────────────────────────────────────────────────
87
+ if (options.json) {
88
+ console.log(JSON.stringify({ findings, total: findings.length }, null, 2));
89
+ process.exit(findings.length > 0 ? 1 : 0);
90
+ }
91
+
92
+ // ── Human-readable output ──────────────────────────────────────────────────
93
+ if (findings.length === 0) {
94
+ output.success('No legally risky packages found.');
95
+ console.log();
96
+ console.log(chalk.gray(' Scanned: package.json, requirements.txt, Cargo.toml, go.mod'));
97
+ console.log();
98
+ return;
99
+ }
100
+
101
+ // Group by severity
102
+ const bySeverity = { critical: [], high: [], medium: [], low: [] };
103
+ for (const f of findings) {
104
+ (bySeverity[f.severity] || bySeverity.medium).push(f);
105
+ }
106
+
107
+ const total = findings.length;
108
+ const critCount = bySeverity.critical.length;
109
+ const highCount = bySeverity.high.length;
110
+
111
+ // Summary line
112
+ console.log(
113
+ chalk.red.bold(` ${total} legal risk finding${total === 1 ? '' : 's'}`),
114
+ chalk.gray('—'),
115
+ critCount > 0 ? chalk.red.bold(`${critCount} critical`) + chalk.gray(', ') : '',
116
+ highCount > 0 ? chalk.red(`${highCount} high`) : '',
117
+ );
118
+ console.log();
119
+
120
+ // Print findings
121
+ for (const severity of ['critical', 'high', 'medium', 'low']) {
122
+ const group = bySeverity[severity];
123
+ if (group.length === 0) continue;
124
+
125
+ for (const f of group) {
126
+ const sevBadge = SEVERITY_COLORS[severity]
127
+ ? SEVERITY_COLORS[severity](` ${severity.toUpperCase()} `)
128
+ : chalk.gray(` ${severity.toUpperCase()} `);
129
+
130
+ // Extract risk type from rule: LEGAL_RISK_DMCA → dmca
131
+ const riskKey = f.rule
132
+ .replace('LEGAL_RISK_', '')
133
+ .toLowerCase()
134
+ .replace(/_/g, '-');
135
+ const riskColor = RISK_COLORS[riskKey] || chalk.white;
136
+ const riskLabel = RISK_LABELS[riskKey] || riskKey;
137
+
138
+ console.log(` ${sevBadge} ${chalk.white.bold(f.title)}`);
139
+ console.log(` ${riskColor(`[${riskLabel}]`)} ${chalk.gray(path.relative(absolutePath, f.file) || f.file)}`);
140
+ console.log();
141
+ console.log(` ${chalk.gray(f.description)}`);
142
+ console.log();
143
+ if (f.fix) {
144
+ console.log(` ${chalk.cyan('Fix:')} ${chalk.gray(f.fix)}`);
145
+ }
146
+ console.log();
147
+ console.log(chalk.gray(' ' + '─'.repeat(56)));
148
+ console.log();
149
+ }
150
+ }
151
+
152
+ // Footer
153
+ console.log(chalk.yellow.bold(' ⚠ Shipping legally risky packages can expose your project to IP liability.'));
154
+ console.log(chalk.gray(' Review each finding and remove the affected dependency before releasing.'));
155
+ console.log();
156
+
157
+ process.exit(1);
158
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ship-safe",
3
- "version": "6.2.0",
3
+ "version": "6.3.0",
4
4
  "description": "AI-powered multi-agent security platform. 18 agents scan 80+ attack classes with LLM-powered deep analysis. Red team your code before attackers do.",
5
5
  "main": "cli/index.js",
6
6
  "bin": {