ship-safe 3.1.0 → 4.0.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 +200 -307
- package/cli/agents/api-fuzzer.js +224 -0
- package/cli/agents/auth-bypass-agent.js +326 -0
- package/cli/agents/base-agent.js +240 -0
- package/cli/agents/cicd-scanner.js +200 -0
- package/cli/agents/config-auditor.js +413 -0
- package/cli/agents/git-history-scanner.js +167 -0
- package/cli/agents/html-reporter.js +363 -0
- package/cli/agents/index.js +56 -0
- package/cli/agents/injection-tester.js +401 -0
- package/cli/agents/llm-redteam.js +251 -0
- package/cli/agents/mobile-scanner.js +225 -0
- package/cli/agents/orchestrator.js +152 -0
- package/cli/agents/policy-engine.js +149 -0
- package/cli/agents/recon-agent.js +196 -0
- package/cli/agents/sbom-generator.js +176 -0
- package/cli/agents/scoring-engine.js +207 -0
- package/cli/agents/ssrf-prober.js +130 -0
- package/cli/agents/supply-chain-agent.js +274 -0
- package/cli/bin/ship-safe.js +119 -2
- package/cli/commands/agent.js +606 -0
- package/cli/commands/audit.js +565 -0
- package/cli/commands/deps.js +447 -0
- package/cli/commands/fix.js +3 -3
- package/cli/commands/init.js +86 -3
- package/cli/commands/mcp.js +2 -2
- package/cli/commands/red-team.js +315 -0
- package/cli/commands/remediate.js +4 -4
- package/cli/commands/rotate.js +6 -6
- package/cli/commands/scan.js +64 -23
- package/cli/commands/score.js +446 -0
- package/cli/commands/watch.js +160 -0
- package/cli/index.js +40 -2
- package/cli/providers/llm-provider.js +288 -0
- package/cli/utils/entropy.js +6 -0
- package/cli/utils/output.js +42 -2
- package/cli/utils/patterns.js +393 -1
- package/package.json +19 -15
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRFProber Agent
|
|
3
|
+
* =================
|
|
4
|
+
*
|
|
5
|
+
* Detects Server-Side Request Forgery vulnerabilities.
|
|
6
|
+
* The fastest-growing attack vector (452% surge in 2025).
|
|
7
|
+
*
|
|
8
|
+
* Checks: user input in URL construction, webhook validation,
|
|
9
|
+
* cloud metadata access, DNS rebinding, protocol smuggling.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { BaseAgent } from './base-agent.js';
|
|
14
|
+
|
|
15
|
+
const PATTERNS = [
|
|
16
|
+
{
|
|
17
|
+
rule: 'SSRF_USER_URL_FETCH',
|
|
18
|
+
title: 'SSRF: User Input in fetch()',
|
|
19
|
+
regex: /fetch\s*\(\s*(?:req\.|request\.|ctx\.|query|params|body|input|url|data)/g,
|
|
20
|
+
severity: 'critical',
|
|
21
|
+
cwe: 'CWE-918',
|
|
22
|
+
owasp: 'A10:2021',
|
|
23
|
+
description: 'User-controlled URL passed to fetch() enables SSRF. Validate against an allowlist.',
|
|
24
|
+
fix: 'Validate URL against allowlist: new URL(input).hostname must match allowed hosts',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
rule: 'SSRF_USER_URL_AXIOS',
|
|
28
|
+
title: 'SSRF: User Input in axios/got/http',
|
|
29
|
+
regex: /(?:axios|got|http|https|request|superagent|node-fetch|undici)(?:\.get|\.post|\.put|\.delete|\.request|\s*\()\s*\(\s*(?:req\.|request\.|ctx\.|query|params|body|input|url|data)/g,
|
|
30
|
+
severity: 'critical',
|
|
31
|
+
cwe: 'CWE-918',
|
|
32
|
+
owasp: 'A10:2021',
|
|
33
|
+
description: 'User-supplied URL in HTTP client enables SSRF. Validate and restrict to public IPs.',
|
|
34
|
+
fix: 'Parse URL, block private IPs (127.0.0.1, 10.x, 172.16-31.x, 169.254.x), block file:// protocol',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
rule: 'SSRF_URL_TEMPLATE',
|
|
38
|
+
title: 'SSRF: Template Literal in URL',
|
|
39
|
+
regex: /(?:fetch|axios|got|http\.get|https\.get)\s*\(\s*`[^`]*\$\{(?:req\.|request\.|ctx\.|query|params|body|input)/g,
|
|
40
|
+
severity: 'critical',
|
|
41
|
+
cwe: 'CWE-918',
|
|
42
|
+
owasp: 'A10:2021',
|
|
43
|
+
description: 'User input interpolated into URL for HTTP request enables SSRF.',
|
|
44
|
+
fix: 'Validate and sanitize the URL before making the request',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
rule: 'SSRF_WEBHOOK_URL',
|
|
48
|
+
title: 'SSRF: Unvalidated Webhook URL',
|
|
49
|
+
regex: /webhook[_-]?url\s*[:=]\s*(?:req\.|request\.|ctx\.|body|query|params|input)/gi,
|
|
50
|
+
severity: 'high',
|
|
51
|
+
cwe: 'CWE-918',
|
|
52
|
+
owasp: 'A10:2021',
|
|
53
|
+
description: 'Accepting user-supplied webhook URLs without validation enables SSRF.',
|
|
54
|
+
fix: 'Validate webhook URL: must be HTTPS, public IP, not cloud metadata endpoint',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
rule: 'SSRF_CLOUD_METADATA',
|
|
58
|
+
title: 'SSRF: Cloud Metadata Endpoint Access',
|
|
59
|
+
regex: /169\.254\.169\.254|metadata\.google\.internal|100\.100\.100\.200/g,
|
|
60
|
+
severity: 'critical',
|
|
61
|
+
cwe: 'CWE-918',
|
|
62
|
+
owasp: 'A10:2021',
|
|
63
|
+
description: 'Cloud metadata endpoint in code. If URL is user-controlled, this enables credential theft.',
|
|
64
|
+
fix: 'Block metadata IPs in URL validation. Use IMDSv2 on AWS (requires token header).',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
rule: 'SSRF_INTERNAL_IP',
|
|
68
|
+
title: 'SSRF: Internal IP Pattern',
|
|
69
|
+
regex: /(?:127\.0\.0\.|0\.0\.0\.0|10\.\d+\.\d+\.\d+|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.)\d+/g,
|
|
70
|
+
severity: 'medium',
|
|
71
|
+
cwe: 'CWE-918',
|
|
72
|
+
owasp: 'A10:2021',
|
|
73
|
+
confidence: 'low',
|
|
74
|
+
description: 'Internal IP address in code. Verify it is not reachable via user-controlled URLs.',
|
|
75
|
+
fix: 'Block private IP ranges in URL validation for user-supplied URLs',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
rule: 'SSRF_REDIRECT_FOLLOW',
|
|
79
|
+
title: 'SSRF: HTTP Client Follows Redirects',
|
|
80
|
+
regex: /(?:follow|maxRedirects|redirect)\s*:\s*(?:true|\d{2,})/g,
|
|
81
|
+
severity: 'medium',
|
|
82
|
+
cwe: 'CWE-918',
|
|
83
|
+
owasp: 'A10:2021',
|
|
84
|
+
confidence: 'low',
|
|
85
|
+
description: 'Following redirects can bypass SSRF protections (redirect to internal IP).',
|
|
86
|
+
fix: 'Disable redirect following or re-validate the redirect target URL',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
rule: 'SSRF_PYTHON_REQUESTS',
|
|
90
|
+
title: 'SSRF: Python requests with User Input',
|
|
91
|
+
regex: /requests\.(?:get|post|put|delete|head|patch)\s*\(\s*(?:request\.|flask\.|data|args|form)/g,
|
|
92
|
+
severity: 'critical',
|
|
93
|
+
cwe: 'CWE-918',
|
|
94
|
+
owasp: 'A10:2021',
|
|
95
|
+
description: 'User-controlled URL in Python requests enables SSRF.',
|
|
96
|
+
fix: 'Validate URL scheme (https only), resolve DNS and check against private IP ranges',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
rule: 'SSRF_IMAGE_PROXY',
|
|
100
|
+
title: 'SSRF: Image/Proxy URL from User Input',
|
|
101
|
+
regex: /(?:imageUrl|image_url|proxyUrl|proxy_url|avatarUrl|avatar_url|iconUrl|icon_url)\s*[:=]\s*(?:req\.|request\.|query|params|body)/gi,
|
|
102
|
+
severity: 'high',
|
|
103
|
+
cwe: 'CWE-918',
|
|
104
|
+
owasp: 'A10:2021',
|
|
105
|
+
description: 'Image/proxy URLs from user input are common SSRF vectors.',
|
|
106
|
+
fix: 'Validate URL, restrict to known CDN domains, or use an image proxy service',
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
export class SSRFProber extends BaseAgent {
|
|
111
|
+
constructor() {
|
|
112
|
+
super('SSRFProber', 'Detect Server-Side Request Forgery vulnerabilities', 'ssrf');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async analyze(context) {
|
|
116
|
+
const { files } = context;
|
|
117
|
+
const codeFiles = files.filter(f => {
|
|
118
|
+
const ext = path.extname(f).toLowerCase();
|
|
119
|
+
return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.rb', '.php', '.go'].includes(ext);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
let findings = [];
|
|
123
|
+
for (const file of codeFiles) {
|
|
124
|
+
findings = findings.concat(this.scanFileWithPatterns(file, PATTERNS));
|
|
125
|
+
}
|
|
126
|
+
return findings;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export default SSRFProber;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SupplyChainAudit Agent
|
|
3
|
+
* =======================
|
|
4
|
+
*
|
|
5
|
+
* Comprehensive supply chain security analysis.
|
|
6
|
+
* Goes beyond npm audit: checks for dependency confusion,
|
|
7
|
+
* typosquatting, malicious install scripts, lockfile integrity,
|
|
8
|
+
* EPSS scoring, and KEV flagging.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { BaseAgent, createFinding } from './base-agent.js';
|
|
14
|
+
|
|
15
|
+
// Common packages that are often typosquatted
|
|
16
|
+
const POPULAR_PACKAGES = [
|
|
17
|
+
'lodash', 'express', 'react', 'axios', 'moment', 'request',
|
|
18
|
+
'chalk', 'commander', 'debug', 'uuid', 'dotenv', 'cors',
|
|
19
|
+
'body-parser', 'jsonwebtoken', 'bcrypt', 'mongoose', 'sequelize',
|
|
20
|
+
'webpack', 'babel', 'eslint', 'prettier', 'typescript',
|
|
21
|
+
'next', 'nuxt', 'svelte', 'vue', 'angular',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Well-known packages that happen to be close to other popular names
|
|
25
|
+
// (not typosquats — verified legitimate packages)
|
|
26
|
+
const KNOWN_SAFE = new Set([
|
|
27
|
+
'ora', 'got', 'ink', 'yup', 'joi', 'ava', 'tap', 'npm', 'nwb',
|
|
28
|
+
'pug', 'koa', 'hap', 'ejs', 'csv', 'ws', 'pg', 'ms',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
// Known malicious package name patterns
|
|
32
|
+
const SUSPICIOUS_NAME_PATTERNS = [
|
|
33
|
+
/^@[^/]+\/[^/]+-[0-9]+$/, // @scope/package-123 (random suffix)
|
|
34
|
+
/^[a-z]+-[a-z]+-[a-z]+-[a-z]+$/, // overly-generic multi-word names
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export class SupplyChainAudit extends BaseAgent {
|
|
38
|
+
constructor() {
|
|
39
|
+
super('SupplyChainAudit', 'Comprehensive supply chain security audit', 'supply-chain');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async analyze(context) {
|
|
43
|
+
const { rootPath } = context;
|
|
44
|
+
const findings = [];
|
|
45
|
+
|
|
46
|
+
// ── 1. Check package.json ─────────────────────────────────────────────────
|
|
47
|
+
const pkgPath = path.join(rootPath, 'package.json');
|
|
48
|
+
if (fs.existsSync(pkgPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
51
|
+
const allDeps = {
|
|
52
|
+
...(pkg.dependencies || {}),
|
|
53
|
+
...(pkg.devDependencies || {}),
|
|
54
|
+
...(pkg.optionalDependencies || {}),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ── Typosquatting detection ───────────────────────────────────────────
|
|
58
|
+
for (const depName of Object.keys(allDeps)) {
|
|
59
|
+
if (KNOWN_SAFE.has(depName)) continue;
|
|
60
|
+
for (const popular of POPULAR_PACKAGES) {
|
|
61
|
+
const distance = this.levenshtein(depName, popular);
|
|
62
|
+
if (distance > 0 && distance <= 2 && depName !== popular) {
|
|
63
|
+
findings.push(createFinding({
|
|
64
|
+
file: pkgPath,
|
|
65
|
+
line: 0,
|
|
66
|
+
severity: 'high',
|
|
67
|
+
category: 'supply-chain',
|
|
68
|
+
rule: 'TYPOSQUAT_SUSPECT',
|
|
69
|
+
title: `Possible Typosquat: "${depName}" (similar to "${popular}")`,
|
|
70
|
+
description: `Package "${depName}" is ${distance} character(s) away from popular package "${popular}". This could be a typosquatting attempt.`,
|
|
71
|
+
matched: depName,
|
|
72
|
+
fix: `Verify this is the intended package. Did you mean "${popular}"?`,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Deprecated/suspicious version pins ───────────────────────────────
|
|
79
|
+
for (const [name, version] of Object.entries(allDeps)) {
|
|
80
|
+
if (typeof version === 'string' && version.startsWith('git+')) {
|
|
81
|
+
findings.push(createFinding({
|
|
82
|
+
file: pkgPath,
|
|
83
|
+
line: 0,
|
|
84
|
+
severity: 'high',
|
|
85
|
+
category: 'supply-chain',
|
|
86
|
+
rule: 'GIT_DEPENDENCY',
|
|
87
|
+
title: `Git Dependency: ${name}`,
|
|
88
|
+
description: `"${name}" is installed from a git URL. Git dependencies bypass registry integrity checks.`,
|
|
89
|
+
matched: `${name}: ${version}`,
|
|
90
|
+
fix: 'Pin to a specific commit hash or use a published npm package version',
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (typeof version === 'string' && version.startsWith('http')) {
|
|
95
|
+
findings.push(createFinding({
|
|
96
|
+
file: pkgPath,
|
|
97
|
+
line: 0,
|
|
98
|
+
severity: 'critical',
|
|
99
|
+
category: 'supply-chain',
|
|
100
|
+
rule: 'URL_DEPENDENCY',
|
|
101
|
+
title: `URL Dependency: ${name}`,
|
|
102
|
+
description: `"${name}" is installed from a URL. This bypasses npm registry and integrity checks.`,
|
|
103
|
+
matched: `${name}: ${version}`,
|
|
104
|
+
fix: 'Publish the package to npm or use a private registry',
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof version === 'string' && version === '*') {
|
|
109
|
+
findings.push(createFinding({
|
|
110
|
+
file: pkgPath,
|
|
111
|
+
line: 0,
|
|
112
|
+
severity: 'high',
|
|
113
|
+
category: 'supply-chain',
|
|
114
|
+
rule: 'WILDCARD_VERSION',
|
|
115
|
+
title: `Wildcard Version: ${name}`,
|
|
116
|
+
description: `"${name}" uses "*" version which accepts any version including malicious updates.`,
|
|
117
|
+
matched: `${name}: *`,
|
|
118
|
+
fix: 'Pin to a specific version or use a caret range: "^x.y.z"',
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Install scripts ──────────────────────────────────────────────────
|
|
124
|
+
if (pkg.scripts) {
|
|
125
|
+
const dangerousScripts = ['preinstall', 'postinstall', 'preuninstall', 'postuninstall'];
|
|
126
|
+
for (const script of dangerousScripts) {
|
|
127
|
+
if (pkg.scripts[script]) {
|
|
128
|
+
const cmd = pkg.scripts[script];
|
|
129
|
+
const suspicious = /curl|wget|bash|sh\s|powershell|eval|base64|nc\s|ncat/i.test(cmd);
|
|
130
|
+
if (suspicious) {
|
|
131
|
+
findings.push(createFinding({
|
|
132
|
+
file: pkgPath,
|
|
133
|
+
line: 0,
|
|
134
|
+
severity: 'critical',
|
|
135
|
+
category: 'supply-chain',
|
|
136
|
+
rule: 'SUSPICIOUS_INSTALL_SCRIPT',
|
|
137
|
+
title: `Suspicious ${script} Script`,
|
|
138
|
+
description: `The ${script} script contains potentially dangerous commands: ${cmd.slice(0, 100)}`,
|
|
139
|
+
matched: cmd,
|
|
140
|
+
fix: 'Review and remove suspicious install scripts',
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
} catch { /* skip parse errors */ }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── 2. Check lockfile integrity ───────────────────────────────────────────
|
|
151
|
+
const lockFiles = [
|
|
152
|
+
{ file: 'package-lock.json', manager: 'npm' },
|
|
153
|
+
{ file: 'yarn.lock', manager: 'yarn' },
|
|
154
|
+
{ file: 'pnpm-lock.yaml', manager: 'pnpm' },
|
|
155
|
+
{ file: 'bun.lockb', manager: 'bun' },
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const hasPackageJson = fs.existsSync(pkgPath);
|
|
159
|
+
let hasLockfile = false;
|
|
160
|
+
|
|
161
|
+
for (const { file, manager } of lockFiles) {
|
|
162
|
+
if (fs.existsSync(path.join(rootPath, file))) {
|
|
163
|
+
hasLockfile = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (hasPackageJson && !hasLockfile) {
|
|
168
|
+
findings.push(createFinding({
|
|
169
|
+
file: pkgPath,
|
|
170
|
+
line: 0,
|
|
171
|
+
severity: 'high',
|
|
172
|
+
category: 'supply-chain',
|
|
173
|
+
rule: 'MISSING_LOCKFILE',
|
|
174
|
+
title: 'No Lock File Found',
|
|
175
|
+
description: 'No package-lock.json, yarn.lock, or pnpm-lock.yaml found. Without a lockfile, installs are non-deterministic and vulnerable to dependency confusion.',
|
|
176
|
+
matched: 'package.json without lockfile',
|
|
177
|
+
fix: 'Run npm install, yarn install, or pnpm install to generate a lockfile, then commit it',
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── 3. Check .npmrc for security settings ─────────────────────────────────
|
|
182
|
+
const npmrcPath = path.join(rootPath, '.npmrc');
|
|
183
|
+
if (fs.existsSync(npmrcPath)) {
|
|
184
|
+
const content = this.readFile(npmrcPath) || '';
|
|
185
|
+
if (content.includes('ignore-scripts=true')) {
|
|
186
|
+
// Good — scripts are disabled
|
|
187
|
+
}
|
|
188
|
+
if (content.includes('registry=') && !content.includes('registry=https://registry.npmjs.org')) {
|
|
189
|
+
findings.push(createFinding({
|
|
190
|
+
file: npmrcPath,
|
|
191
|
+
line: 0,
|
|
192
|
+
severity: 'medium',
|
|
193
|
+
category: 'supply-chain',
|
|
194
|
+
rule: 'CUSTOM_REGISTRY',
|
|
195
|
+
title: 'Custom NPM Registry Configured',
|
|
196
|
+
description: 'A custom npm registry is configured. Verify it is trusted and uses HTTPS.',
|
|
197
|
+
matched: content.match(/registry=.*/)?.[0] || '',
|
|
198
|
+
confidence: 'medium',
|
|
199
|
+
fix: 'Verify the registry URL is trusted and uses HTTPS',
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── 4. Check Python requirements ──────────────────────────────────────────
|
|
205
|
+
const reqPath = path.join(rootPath, 'requirements.txt');
|
|
206
|
+
if (fs.existsSync(reqPath)) {
|
|
207
|
+
const content = this.readFile(reqPath) || '';
|
|
208
|
+
const lines = content.split('\n');
|
|
209
|
+
for (let i = 0; i < lines.length; i++) {
|
|
210
|
+
const line = lines[i].trim();
|
|
211
|
+
if (!line || line.startsWith('#')) continue;
|
|
212
|
+
|
|
213
|
+
// Unpinned versions
|
|
214
|
+
if (!line.includes('==') && !line.includes('>=') && !line.includes('~=') && !line.includes('@')) {
|
|
215
|
+
findings.push(createFinding({
|
|
216
|
+
file: reqPath,
|
|
217
|
+
line: i + 1,
|
|
218
|
+
severity: 'medium',
|
|
219
|
+
category: 'supply-chain',
|
|
220
|
+
rule: 'UNPINNED_PYTHON_DEP',
|
|
221
|
+
title: `Unpinned Python Dependency: ${line}`,
|
|
222
|
+
description: 'Python dependency without version pin. Pin to a specific version for reproducible builds.',
|
|
223
|
+
matched: line,
|
|
224
|
+
fix: `Pin version: ${line}==x.y.z`,
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Git/URL dependencies
|
|
229
|
+
if (line.includes('git+') || line.startsWith('http')) {
|
|
230
|
+
findings.push(createFinding({
|
|
231
|
+
file: reqPath,
|
|
232
|
+
line: i + 1,
|
|
233
|
+
severity: 'high',
|
|
234
|
+
category: 'supply-chain',
|
|
235
|
+
rule: 'GIT_PYTHON_DEP',
|
|
236
|
+
title: `Git/URL Python Dependency: ${line.slice(0, 60)}`,
|
|
237
|
+
description: 'Installing from git/URL bypasses PyPI integrity checks.',
|
|
238
|
+
matched: line,
|
|
239
|
+
fix: 'Publish to PyPI or pin to a specific commit hash',
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return findings;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Simple Levenshtein distance for typosquatting detection.
|
|
250
|
+
*/
|
|
251
|
+
levenshtein(a, b) {
|
|
252
|
+
if (a.length === 0) return b.length;
|
|
253
|
+
if (b.length === 0) return a.length;
|
|
254
|
+
|
|
255
|
+
const matrix = [];
|
|
256
|
+
for (let i = 0; i <= b.length; i++) matrix[i] = [i];
|
|
257
|
+
for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
|
|
258
|
+
|
|
259
|
+
for (let i = 1; i <= b.length; i++) {
|
|
260
|
+
for (let j = 1; j <= a.length; j++) {
|
|
261
|
+
const cost = b[i - 1] === a[j - 1] ? 0 : 1;
|
|
262
|
+
matrix[i][j] = Math.min(
|
|
263
|
+
matrix[i - 1][j] + 1,
|
|
264
|
+
matrix[i][j - 1] + 1,
|
|
265
|
+
matrix[i - 1][j - 1] + cost
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return matrix[b.length][a.length];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export default SupplyChainAudit;
|
package/cli/bin/ship-safe.js
CHANGED
|
@@ -28,11 +28,21 @@ import { guardCommand } from '../commands/guard.js';
|
|
|
28
28
|
import { mcpCommand } from '../commands/mcp.js';
|
|
29
29
|
import { remediateCommand } from '../commands/remediate.js';
|
|
30
30
|
import { rotateCommand } from '../commands/rotate.js';
|
|
31
|
+
import { agentCommand } from '../commands/agent.js';
|
|
32
|
+
import { depsCommand } from '../commands/deps.js';
|
|
33
|
+
import { scoreCommand } from '../commands/score.js';
|
|
34
|
+
import { redTeamCommand } from '../commands/red-team.js';
|
|
35
|
+
import { watchCommand } from '../commands/watch.js';
|
|
36
|
+
import { auditCommand } from '../commands/audit.js';
|
|
37
|
+
import { PolicyEngine } from '../agents/policy-engine.js';
|
|
38
|
+
import { SBOMGenerator } from '../agents/sbom-generator.js';
|
|
31
39
|
|
|
32
40
|
// =============================================================================
|
|
33
41
|
// CLI CONFIGURATION
|
|
34
42
|
// =============================================================================
|
|
35
43
|
|
|
44
|
+
const DEFAULT_MODEL = 'claude-haiku-4-5-20251001';
|
|
45
|
+
|
|
36
46
|
// Read version from package.json
|
|
37
47
|
const __filename = fileURLToPath(import.meta.url);
|
|
38
48
|
const __dirname = dirname(__filename);
|
|
@@ -92,6 +102,7 @@ program
|
|
|
92
102
|
.option('-f, --force', 'Overwrite existing files')
|
|
93
103
|
.option('--gitignore', 'Only copy .gitignore')
|
|
94
104
|
.option('--headers', 'Only copy security headers config')
|
|
105
|
+
.option('--agents', 'Only add security rules to AI agent instruction files (CLAUDE.md, .cursor/rules/, .windsurfrules, copilot-instructions.md)')
|
|
95
106
|
.action(initCommand);
|
|
96
107
|
|
|
97
108
|
// -----------------------------------------------------------------------------
|
|
@@ -140,6 +151,103 @@ program
|
|
|
140
151
|
.option('--provider <name>', 'Only rotate secrets for a specific provider (e.g. github, stripe, openai)')
|
|
141
152
|
.action(rotateCommand);
|
|
142
153
|
|
|
154
|
+
// -----------------------------------------------------------------------------
|
|
155
|
+
// AGENT COMMAND
|
|
156
|
+
// -----------------------------------------------------------------------------
|
|
157
|
+
program
|
|
158
|
+
.command('agent [path]')
|
|
159
|
+
.description('AI-powered security audit: scan, classify with Claude, auto-remediate confirmed secrets')
|
|
160
|
+
.option('--dry-run', 'Show classification and plan without writing any files')
|
|
161
|
+
.option('--model <model>', `Claude model to use (default: ${DEFAULT_MODEL})`)
|
|
162
|
+
.action(agentCommand);
|
|
163
|
+
|
|
164
|
+
// -----------------------------------------------------------------------------
|
|
165
|
+
// DEPS COMMAND
|
|
166
|
+
// -----------------------------------------------------------------------------
|
|
167
|
+
program
|
|
168
|
+
.command('deps [path]')
|
|
169
|
+
.description('Audit dependencies for known CVEs (npm, yarn, pnpm, pip-audit, bundler-audit)')
|
|
170
|
+
.option('--fix', 'Run the package manager fix command after auditing')
|
|
171
|
+
.action(depsCommand);
|
|
172
|
+
|
|
173
|
+
// -----------------------------------------------------------------------------
|
|
174
|
+
// SCORE COMMAND
|
|
175
|
+
// -----------------------------------------------------------------------------
|
|
176
|
+
program
|
|
177
|
+
.command('score [path]')
|
|
178
|
+
.description('Compute a 0-100 security health score for your project')
|
|
179
|
+
.option('--no-deps', 'Skip dependency audit')
|
|
180
|
+
.action(scoreCommand);
|
|
181
|
+
|
|
182
|
+
// -----------------------------------------------------------------------------
|
|
183
|
+
// AUDIT COMMAND (v4.0 — Full Security Audit)
|
|
184
|
+
// -----------------------------------------------------------------------------
|
|
185
|
+
program
|
|
186
|
+
.command('audit [path]')
|
|
187
|
+
.description('Full security audit: secrets + 12 agents + deps + score + remediation plan')
|
|
188
|
+
.option('--json', 'Output results as JSON')
|
|
189
|
+
.option('--sarif', 'Output results in SARIF format')
|
|
190
|
+
.option('--html [file]', 'HTML report path (default: ship-safe-report.html)')
|
|
191
|
+
.option('--no-deps', 'Skip dependency audit')
|
|
192
|
+
.option('--no-ai', 'Skip AI classification')
|
|
193
|
+
.option('-v, --verbose', 'Verbose output')
|
|
194
|
+
.action(auditCommand);
|
|
195
|
+
|
|
196
|
+
// -----------------------------------------------------------------------------
|
|
197
|
+
// RED TEAM COMMAND (v4.0 — Multi-Agent Security Audit)
|
|
198
|
+
// -----------------------------------------------------------------------------
|
|
199
|
+
program
|
|
200
|
+
.command('red-team [path]')
|
|
201
|
+
.description('Multi-agent security audit: 12 agents scan for 50+ attack classes')
|
|
202
|
+
.option('--agents <list>', 'Comma-separated list of agents to run')
|
|
203
|
+
.option('--json', 'Output results as JSON')
|
|
204
|
+
.option('--sarif', 'Output results in SARIF format')
|
|
205
|
+
.option('--html [file]', 'Generate HTML security report')
|
|
206
|
+
.option('--sbom [file]', 'Generate CycloneDX SBOM')
|
|
207
|
+
.option('--no-deps', 'Skip dependency audit')
|
|
208
|
+
.option('--no-ai', 'Skip AI classification')
|
|
209
|
+
.option('-v, --verbose', 'Verbose output')
|
|
210
|
+
.action(redTeamCommand);
|
|
211
|
+
|
|
212
|
+
// -----------------------------------------------------------------------------
|
|
213
|
+
// WATCH COMMAND
|
|
214
|
+
// -----------------------------------------------------------------------------
|
|
215
|
+
program
|
|
216
|
+
.command('watch [path]')
|
|
217
|
+
.description('Continuous monitoring: watch files for security issues in real-time')
|
|
218
|
+
.option('--poll', 'Use polling mode (for network drives)')
|
|
219
|
+
.action(watchCommand);
|
|
220
|
+
|
|
221
|
+
// -----------------------------------------------------------------------------
|
|
222
|
+
// SBOM COMMAND
|
|
223
|
+
// -----------------------------------------------------------------------------
|
|
224
|
+
program
|
|
225
|
+
.command('sbom [path]')
|
|
226
|
+
.description('Generate Software Bill of Materials (CycloneDX SBOM)')
|
|
227
|
+
.option('-o, --output <file>', 'Output file path', 'sbom.json')
|
|
228
|
+
.action((targetPath = '.', options) => {
|
|
229
|
+
const absolutePath = join(process.cwd(), targetPath);
|
|
230
|
+
const sbom = new SBOMGenerator();
|
|
231
|
+
sbom.generateToFile(absolutePath, options.output);
|
|
232
|
+
console.log(chalk.green(`✔ SBOM saved to ${options.output}`));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// -----------------------------------------------------------------------------
|
|
236
|
+
// POLICY COMMAND
|
|
237
|
+
// -----------------------------------------------------------------------------
|
|
238
|
+
program
|
|
239
|
+
.command('policy <action>')
|
|
240
|
+
.description('Manage security policies (init: create policy template)')
|
|
241
|
+
.action((action) => {
|
|
242
|
+
if (action === 'init') {
|
|
243
|
+
const policyPath = PolicyEngine.generateTemplate(process.cwd());
|
|
244
|
+
console.log(chalk.green(`✔ Policy template created: ${policyPath}`));
|
|
245
|
+
console.log(chalk.gray(' Edit .ship-safe.policy.json to configure your security policy.'));
|
|
246
|
+
} else {
|
|
247
|
+
console.log(chalk.yellow(`Unknown policy action: ${action}. Use: policy init`));
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
143
251
|
// -----------------------------------------------------------------------------
|
|
144
252
|
// PARSE AND RUN
|
|
145
253
|
// -----------------------------------------------------------------------------
|
|
@@ -148,12 +256,21 @@ program
|
|
|
148
256
|
if (process.argv.length === 2) {
|
|
149
257
|
console.log(banner);
|
|
150
258
|
console.log(chalk.yellow('\nQuick start:\n'));
|
|
259
|
+
console.log(chalk.cyan.bold(' v4.0 — Full Security Audit'));
|
|
260
|
+
console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets + agents + deps + remediation plan'));
|
|
261
|
+
console.log(chalk.white(' npx ship-safe red-team . ') + chalk.gray('# 12-agent red team scan (50+ attack classes)'));
|
|
262
|
+
console.log(chalk.white(' npx ship-safe watch . ') + chalk.gray('# Continuous monitoring mode'));
|
|
263
|
+
console.log(chalk.white(' npx ship-safe sbom . ') + chalk.gray('# Generate CycloneDX SBOM'));
|
|
264
|
+
console.log(chalk.white(' npx ship-safe policy init ') + chalk.gray('# Create security policy template'));
|
|
265
|
+
console.log();
|
|
266
|
+
console.log(chalk.gray(' Core commands:'));
|
|
267
|
+
console.log(chalk.white(' npx ship-safe agent . ') + chalk.gray('# AI audit: scan + classify + auto-fix'));
|
|
151
268
|
console.log(chalk.white(' npx ship-safe scan . ') + chalk.gray('# Scan for secrets'));
|
|
152
269
|
console.log(chalk.white(' npx ship-safe remediate . ') + chalk.gray('# Auto-fix: rewrite code + write .env'));
|
|
153
270
|
console.log(chalk.white(' npx ship-safe rotate . ') + chalk.gray('# Revoke exposed keys (provider guides)'));
|
|
154
|
-
console.log(chalk.white(' npx ship-safe
|
|
271
|
+
console.log(chalk.white(' npx ship-safe deps . ') + chalk.gray('# Audit dependencies for CVEs'));
|
|
272
|
+
console.log(chalk.white(' npx ship-safe score . ') + chalk.gray('# Security health score (0-100)'));
|
|
155
273
|
console.log(chalk.white(' npx ship-safe guard ') + chalk.gray('# Block git push if secrets found'));
|
|
156
|
-
console.log(chalk.white(' npx ship-safe checklist ') + chalk.gray('# Run security checklist'));
|
|
157
274
|
console.log(chalk.white(' npx ship-safe init ') + chalk.gray('# Add security configs to your project'));
|
|
158
275
|
console.log(chalk.white('\n npx ship-safe --help ') + chalk.gray('# Show all options'));
|
|
159
276
|
console.log();
|