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 +13 -0
- package/cli/agents/index.js +1 -0
- package/cli/agents/legal-risk-agent.js +302 -0
- package/cli/bin/ship-safe.js +12 -0
- package/cli/commands/audit.js +22 -2
- package/cli/commands/legal.js +158 -0
- package/package.json +1 -1
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.
|
package/cli/agents/index.js
CHANGED
|
@@ -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;
|
package/cli/bin/ship-safe.js
CHANGED
|
@@ -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'));
|
package/cli/commands/audit.js
CHANGED
|
@@ -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.
|
|
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": {
|