ship-safe 3.2.0 → 4.1.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 +209 -437
- package/cli/agents/api-fuzzer.js +224 -0
- package/cli/agents/auth-bypass-agent.js +326 -0
- package/cli/agents/base-agent.js +253 -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 +157 -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 +85 -3
- package/cli/commands/audit.js +620 -0
- package/cli/commands/red-team.js +315 -0
- package/cli/commands/scan.js +79 -8
- package/cli/commands/watch.js +160 -0
- package/cli/index.js +39 -1
- package/cli/providers/llm-provider.js +288 -0
- package/cli/utils/cache-manager.js +258 -0
- package/cli/utils/patterns.js +95 -0
- package/package.json +18 -14
|
@@ -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
|
@@ -31,6 +31,11 @@ import { rotateCommand } from '../commands/rotate.js';
|
|
|
31
31
|
import { agentCommand } from '../commands/agent.js';
|
|
32
32
|
import { depsCommand } from '../commands/deps.js';
|
|
33
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';
|
|
34
39
|
|
|
35
40
|
// =============================================================================
|
|
36
41
|
// CLI CONFIGURATION
|
|
@@ -77,6 +82,7 @@ program
|
|
|
77
82
|
.option('--json', 'Output results as JSON (useful for CI)')
|
|
78
83
|
.option('--sarif', 'Output results in SARIF format (for GitHub Code Scanning)')
|
|
79
84
|
.option('--include-tests', 'Also scan test files (excluded by default to reduce false positives)')
|
|
85
|
+
.option('--no-cache', 'Force full rescan (ignore cached results)')
|
|
80
86
|
.action(scanCommand);
|
|
81
87
|
|
|
82
88
|
// -----------------------------------------------------------------------------
|
|
@@ -174,6 +180,76 @@ program
|
|
|
174
180
|
.option('--no-deps', 'Skip dependency audit')
|
|
175
181
|
.action(scoreCommand);
|
|
176
182
|
|
|
183
|
+
// -----------------------------------------------------------------------------
|
|
184
|
+
// AUDIT COMMAND (v4.0 — Full Security Audit)
|
|
185
|
+
// -----------------------------------------------------------------------------
|
|
186
|
+
program
|
|
187
|
+
.command('audit [path]')
|
|
188
|
+
.description('Full security audit: secrets + 12 agents + deps + score + remediation plan')
|
|
189
|
+
.option('--json', 'Output results as JSON')
|
|
190
|
+
.option('--sarif', 'Output results in SARIF format')
|
|
191
|
+
.option('--html [file]', 'HTML report path (default: ship-safe-report.html)')
|
|
192
|
+
.option('--no-deps', 'Skip dependency audit')
|
|
193
|
+
.option('--no-ai', 'Skip AI classification')
|
|
194
|
+
.option('--no-cache', 'Force full rescan (ignore cached results)')
|
|
195
|
+
.option('-v, --verbose', 'Verbose output')
|
|
196
|
+
.action(auditCommand);
|
|
197
|
+
|
|
198
|
+
// -----------------------------------------------------------------------------
|
|
199
|
+
// RED TEAM COMMAND (v4.0 — Multi-Agent Security Audit)
|
|
200
|
+
// -----------------------------------------------------------------------------
|
|
201
|
+
program
|
|
202
|
+
.command('red-team [path]')
|
|
203
|
+
.description('Multi-agent security audit: 12 agents scan for 50+ attack classes')
|
|
204
|
+
.option('--agents <list>', 'Comma-separated list of agents to run')
|
|
205
|
+
.option('--json', 'Output results as JSON')
|
|
206
|
+
.option('--sarif', 'Output results in SARIF format')
|
|
207
|
+
.option('--html [file]', 'Generate HTML security report')
|
|
208
|
+
.option('--sbom [file]', 'Generate CycloneDX SBOM')
|
|
209
|
+
.option('--no-deps', 'Skip dependency audit')
|
|
210
|
+
.option('--no-ai', 'Skip AI classification')
|
|
211
|
+
.option('-v, --verbose', 'Verbose output')
|
|
212
|
+
.action(redTeamCommand);
|
|
213
|
+
|
|
214
|
+
// -----------------------------------------------------------------------------
|
|
215
|
+
// WATCH COMMAND
|
|
216
|
+
// -----------------------------------------------------------------------------
|
|
217
|
+
program
|
|
218
|
+
.command('watch [path]')
|
|
219
|
+
.description('Continuous monitoring: watch files for security issues in real-time')
|
|
220
|
+
.option('--poll', 'Use polling mode (for network drives)')
|
|
221
|
+
.action(watchCommand);
|
|
222
|
+
|
|
223
|
+
// -----------------------------------------------------------------------------
|
|
224
|
+
// SBOM COMMAND
|
|
225
|
+
// -----------------------------------------------------------------------------
|
|
226
|
+
program
|
|
227
|
+
.command('sbom [path]')
|
|
228
|
+
.description('Generate Software Bill of Materials (CycloneDX SBOM)')
|
|
229
|
+
.option('-o, --output <file>', 'Output file path', 'sbom.json')
|
|
230
|
+
.action((targetPath = '.', options) => {
|
|
231
|
+
const absolutePath = join(process.cwd(), targetPath);
|
|
232
|
+
const sbom = new SBOMGenerator();
|
|
233
|
+
sbom.generateToFile(absolutePath, options.output);
|
|
234
|
+
console.log(chalk.green(`✔ SBOM saved to ${options.output}`));
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// -----------------------------------------------------------------------------
|
|
238
|
+
// POLICY COMMAND
|
|
239
|
+
// -----------------------------------------------------------------------------
|
|
240
|
+
program
|
|
241
|
+
.command('policy <action>')
|
|
242
|
+
.description('Manage security policies (init: create policy template)')
|
|
243
|
+
.action((action) => {
|
|
244
|
+
if (action === 'init') {
|
|
245
|
+
const policyPath = PolicyEngine.generateTemplate(process.cwd());
|
|
246
|
+
console.log(chalk.green(`✔ Policy template created: ${policyPath}`));
|
|
247
|
+
console.log(chalk.gray(' Edit .ship-safe.policy.json to configure your security policy.'));
|
|
248
|
+
} else {
|
|
249
|
+
console.log(chalk.yellow(`Unknown policy action: ${action}. Use: policy init`));
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
177
253
|
// -----------------------------------------------------------------------------
|
|
178
254
|
// PARSE AND RUN
|
|
179
255
|
// -----------------------------------------------------------------------------
|
|
@@ -182,15 +258,21 @@ program
|
|
|
182
258
|
if (process.argv.length === 2) {
|
|
183
259
|
console.log(banner);
|
|
184
260
|
console.log(chalk.yellow('\nQuick start:\n'));
|
|
261
|
+
console.log(chalk.cyan.bold(' v4.0 — Full Security Audit'));
|
|
262
|
+
console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets + agents + deps + remediation plan'));
|
|
263
|
+
console.log(chalk.white(' npx ship-safe red-team . ') + chalk.gray('# 12-agent red team scan (50+ attack classes)'));
|
|
264
|
+
console.log(chalk.white(' npx ship-safe watch . ') + chalk.gray('# Continuous monitoring mode'));
|
|
265
|
+
console.log(chalk.white(' npx ship-safe sbom . ') + chalk.gray('# Generate CycloneDX SBOM'));
|
|
266
|
+
console.log(chalk.white(' npx ship-safe policy init ') + chalk.gray('# Create security policy template'));
|
|
267
|
+
console.log();
|
|
268
|
+
console.log(chalk.gray(' Core commands:'));
|
|
185
269
|
console.log(chalk.white(' npx ship-safe agent . ') + chalk.gray('# AI audit: scan + classify + auto-fix'));
|
|
186
270
|
console.log(chalk.white(' npx ship-safe scan . ') + chalk.gray('# Scan for secrets'));
|
|
187
271
|
console.log(chalk.white(' npx ship-safe remediate . ') + chalk.gray('# Auto-fix: rewrite code + write .env'));
|
|
188
272
|
console.log(chalk.white(' npx ship-safe rotate . ') + chalk.gray('# Revoke exposed keys (provider guides)'));
|
|
189
|
-
console.log(chalk.white(' npx ship-safe fix ') + chalk.gray('# Generate .env.example from secrets'));
|
|
190
|
-
console.log(chalk.white(' npx ship-safe guard ') + chalk.gray('# Block git push if secrets found'));
|
|
191
|
-
console.log(chalk.white(' npx ship-safe checklist ') + chalk.gray('# Run security checklist'));
|
|
192
273
|
console.log(chalk.white(' npx ship-safe deps . ') + chalk.gray('# Audit dependencies for CVEs'));
|
|
193
274
|
console.log(chalk.white(' npx ship-safe score . ') + chalk.gray('# Security health score (0-100)'));
|
|
275
|
+
console.log(chalk.white(' npx ship-safe guard ') + chalk.gray('# Block git push if secrets found'));
|
|
194
276
|
console.log(chalk.white(' npx ship-safe init ') + chalk.gray('# Add security configs to your project'));
|
|
195
277
|
console.log(chalk.white('\n npx ship-safe --help ') + chalk.gray('# Show all options'));
|
|
196
278
|
console.log();
|