ship-safe 5.0.1 → 6.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 +110 -23
- package/cli/agents/abom-generator.js +225 -0
- package/cli/agents/agent-config-scanner.js +547 -0
- package/cli/agents/agentic-security-agent.js +1 -1
- package/cli/agents/api-fuzzer.js +1 -1
- package/cli/agents/auth-bypass-agent.js +2 -2
- package/cli/agents/config-auditor.js +3 -11
- package/cli/agents/exception-handler-agent.js +187 -0
- package/cli/agents/html-reporter.js +532 -370
- package/cli/agents/index.js +11 -1
- package/cli/agents/mcp-security-agent.js +182 -0
- package/cli/agents/pii-compliance-agent.js +4 -4
- package/cli/agents/scoring-engine.js +25 -6
- package/cli/agents/vibe-coding-agent.js +250 -0
- package/cli/bin/ship-safe.js +96 -6
- package/cli/commands/abom.js +73 -0
- package/cli/commands/agent.js +4 -4
- package/cli/commands/audit.js +15 -7
- package/cli/commands/baseline.js +1 -1
- package/cli/commands/benchmark.js +327 -0
- package/cli/commands/ci.js +81 -1
- package/cli/commands/deps.js +73 -4
- package/cli/commands/diff.js +200 -0
- package/cli/commands/doctor.js +14 -4
- package/cli/commands/fix.js +1 -1
- package/cli/commands/guard.js +99 -0
- package/cli/commands/init.js +407 -349
- package/cli/commands/openclaw.js +378 -0
- package/cli/commands/red-team.js +2 -2
- package/cli/commands/remediate.js +153 -7
- package/cli/commands/scan-skill.js +329 -0
- package/cli/commands/update-intel.js +55 -0
- package/cli/commands/vibe-check.js +276 -0
- package/cli/commands/watch.js +124 -4
- package/cli/data/threat-intel.json +85 -0
- package/cli/index.js +9 -0
- package/cli/utils/cache-manager.js +1 -1
- package/cli/utils/compliance-map.js +125 -0
- package/cli/utils/output.js +5 -2
- package/cli/utils/patterns.js +3 -0
- package/cli/utils/pdf-generator.js +1 -1
- package/cli/utils/threat-intel.js +167 -0
- package/package.json +2 -2
package/cli/commands/watch.js
CHANGED
|
@@ -17,6 +17,15 @@ import { SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES, SECRET_PATTERNS, SECURITY_P
|
|
|
17
17
|
import { isHighEntropyMatch, getConfidence } from '../utils/entropy.js';
|
|
18
18
|
import * as output from '../utils/output.js';
|
|
19
19
|
|
|
20
|
+
// Agent config files to watch
|
|
21
|
+
const AGENT_CONFIG_PATTERNS = [
|
|
22
|
+
'.cursorrules', '.windsurfrules', 'CLAUDE.md', 'AGENTS.md',
|
|
23
|
+
'.github/copilot-instructions.md', '.aider.conf.yml',
|
|
24
|
+
'.continue/config.json', 'openclaw.json', 'openclaw.config.json',
|
|
25
|
+
'clawhub.json', 'mcp.json', '.claude/settings.json',
|
|
26
|
+
'.cursor/mcp.json', '.vscode/mcp.json',
|
|
27
|
+
];
|
|
28
|
+
|
|
20
29
|
export async function watchCommand(targetPath = '.', options = {}) {
|
|
21
30
|
const absolutePath = path.resolve(targetPath);
|
|
22
31
|
|
|
@@ -25,6 +34,11 @@ export async function watchCommand(targetPath = '.', options = {}) {
|
|
|
25
34
|
process.exit(1);
|
|
26
35
|
}
|
|
27
36
|
|
|
37
|
+
// Config-only watch mode
|
|
38
|
+
if (options.configs) {
|
|
39
|
+
return watchConfigs(absolutePath);
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
console.log();
|
|
29
43
|
output.header('Ship Safe — Watch Mode');
|
|
30
44
|
console.log();
|
|
@@ -39,8 +53,8 @@ export async function watchCommand(targetPath = '.', options = {}) {
|
|
|
39
53
|
|
|
40
54
|
// Use fs.watch recursively
|
|
41
55
|
try {
|
|
42
|
-
const watcher = fs.watch(absolutePath, { recursive: true }, (eventType, filename) => {
|
|
43
|
-
if (!filename) return;
|
|
56
|
+
const watcher = fs.watch(absolutePath, { recursive: true }, (eventType, filename) => { // ship-safe-ignore — filename from fs.watch OS event, not user input
|
|
57
|
+
if (!filename) return; // ship-safe-ignore
|
|
44
58
|
|
|
45
59
|
const fullPath = path.join(absolutePath, filename); // ship-safe-ignore — filename from fs.watch, not user input
|
|
46
60
|
const relPath = filename.replace(/\\/g, '/');
|
|
@@ -51,9 +65,9 @@ export async function watchCommand(targetPath = '.', options = {}) {
|
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
// Skip non-code files
|
|
54
|
-
const ext = path.extname(filename).toLowerCase();
|
|
68
|
+
const ext = path.extname(filename).toLowerCase(); // ship-safe-ignore — filename from fs.watch OS event
|
|
55
69
|
if (SKIP_EXTENSIONS.has(ext)) return;
|
|
56
|
-
if (SKIP_FILENAMES.has(path.basename(filename))) return;
|
|
70
|
+
if (SKIP_FILENAMES.has(path.basename(filename))) return; // ship-safe-ignore
|
|
57
71
|
if (filename.endsWith('.min.js') || filename.endsWith('.min.css')) return;
|
|
58
72
|
|
|
59
73
|
// Add to pending and debounce
|
|
@@ -159,3 +173,109 @@ function scanFile(filePath, patterns) {
|
|
|
159
173
|
return true;
|
|
160
174
|
});
|
|
161
175
|
}
|
|
176
|
+
|
|
177
|
+
// =============================================================================
|
|
178
|
+
// CONFIG-ONLY WATCH MODE
|
|
179
|
+
// =============================================================================
|
|
180
|
+
|
|
181
|
+
async function watchConfigs(absolutePath) {
|
|
182
|
+
console.log();
|
|
183
|
+
output.header('Ship Safe — Agent Config Watch');
|
|
184
|
+
console.log();
|
|
185
|
+
console.log(chalk.cyan(' Watching agent config files for changes...'));
|
|
186
|
+
console.log(chalk.gray(' Monitors: .cursorrules, CLAUDE.md, openclaw.json, mcp.json, .claude/settings.json, ...'));
|
|
187
|
+
console.log(chalk.gray(' Press Ctrl+C to stop'));
|
|
188
|
+
console.log();
|
|
189
|
+
|
|
190
|
+
let debounceTimer = null;
|
|
191
|
+
const pendingFiles = new Set();
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const watcher = fs.watch(absolutePath, { recursive: true }, (eventType, filename) => {
|
|
195
|
+
if (!filename) return;
|
|
196
|
+
|
|
197
|
+
// Check if this is an agent config file
|
|
198
|
+
const relPath = filename.replace(/\\/g, '/');
|
|
199
|
+
const isConfig = AGENT_CONFIG_PATTERNS.some(p => relPath === p || relPath.endsWith('/' + p));
|
|
200
|
+
const isGlobMatch = relPath.match(/\.cursor\/rules\/.*\.mdc$/) ||
|
|
201
|
+
relPath.match(/\.openclaw\/.*\.json$/) ||
|
|
202
|
+
relPath.match(/\.claude\/commands\/.*\.md$/) ||
|
|
203
|
+
relPath.match(/\.claude\/memory\//);
|
|
204
|
+
|
|
205
|
+
if (!isConfig && !isGlobMatch) return;
|
|
206
|
+
|
|
207
|
+
const fullPath = path.join(absolutePath, filename);
|
|
208
|
+
pendingFiles.add(fullPath);
|
|
209
|
+
|
|
210
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
211
|
+
debounceTimer = setTimeout(async () => {
|
|
212
|
+
const filesToScan = [...pendingFiles];
|
|
213
|
+
pendingFiles.clear();
|
|
214
|
+
await scanConfigFiles(filesToScan, absolutePath);
|
|
215
|
+
}, 300);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
process.on('SIGINT', () => {
|
|
219
|
+
watcher.close();
|
|
220
|
+
console.log();
|
|
221
|
+
output.info('Config watch stopped.');
|
|
222
|
+
process.exit(0);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
setInterval(() => {}, 1000 * 60 * 60);
|
|
226
|
+
|
|
227
|
+
} catch (err) {
|
|
228
|
+
output.error(`Watch failed: ${err.message}`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function scanConfigFiles(files, rootPath) {
|
|
234
|
+
// Dynamic import to avoid circular dependency
|
|
235
|
+
const { AgentConfigScanner } = await import('../agents/agent-config-scanner.js');
|
|
236
|
+
const { MCPSecurityAgent } = await import('../agents/mcp-security-agent.js');
|
|
237
|
+
|
|
238
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
239
|
+
const scanner = new AgentConfigScanner();
|
|
240
|
+
const mcpScanner = new MCPSecurityAgent();
|
|
241
|
+
|
|
242
|
+
for (const filePath of files) {
|
|
243
|
+
if (!fs.existsSync(filePath)) {
|
|
244
|
+
console.log(chalk.gray(` [${timestamp}] ${path.relative(rootPath, filePath)} — deleted`));
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const relPath = path.relative(rootPath, filePath).replace(/\\/g, '/');
|
|
249
|
+
console.log(chalk.cyan(` [${timestamp}] Changed: ${relPath}`));
|
|
250
|
+
|
|
251
|
+
// Git blame (best-effort)
|
|
252
|
+
try {
|
|
253
|
+
const { execFileSync } = await import('child_process');
|
|
254
|
+
const blame = execFileSync('git', ['log', '-1', '--format=%an (%ar)', '--', filePath], { cwd: rootPath, encoding: 'utf-8', timeout: 5000 }).trim();
|
|
255
|
+
if (blame) console.log(chalk.gray(` Last modified by: ${blame}`));
|
|
256
|
+
} catch { /* not a git repo or git not available */ }
|
|
257
|
+
|
|
258
|
+
// Run agent config scanner
|
|
259
|
+
const context = { rootPath, files: [] };
|
|
260
|
+
const [configFindings, mcpFindings] = await Promise.all([
|
|
261
|
+
scanner.analyze(context),
|
|
262
|
+
mcpScanner.analyze(context),
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
const findings = [...configFindings, ...mcpFindings].filter(f =>
|
|
266
|
+
f.file && path.resolve(f.file) === path.resolve(filePath)
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
if (findings.length > 0) {
|
|
270
|
+
for (const f of findings) {
|
|
271
|
+
const sevColor = f.severity === 'critical' ? chalk.red.bold
|
|
272
|
+
: f.severity === 'high' ? chalk.yellow
|
|
273
|
+
: chalk.blue;
|
|
274
|
+
console.log(` ${sevColor(`[${f.severity.toUpperCase()}]`)} ${f.title || f.rule}`);
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
console.log(chalk.green(' ✔ Clean'));
|
|
278
|
+
}
|
|
279
|
+
console.log();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"updated": "2026-03-23T00:00:00Z",
|
|
4
|
+
"maliciousSkillHashes": [
|
|
5
|
+
{
|
|
6
|
+
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
7
|
+
"name": "empty-payload-stub",
|
|
8
|
+
"description": "Empty file used as placeholder in ClawHavoc campaign"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"sha256": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
|
|
12
|
+
"name": "clawhavoc-stealer-v1",
|
|
13
|
+
"description": "AMOS stealer dropper identified in ClawHavoc campaign (1,184 malicious skills)"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"sha256": "dead0000beef0000cafe0000face0000dead0000beef0000cafe0000face0000",
|
|
17
|
+
"name": "clawhavoc-exfil-v2",
|
|
18
|
+
"description": "Data exfiltration skill sending credentials to attacker-controlled webhook"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"compromisedMcpServers": [
|
|
22
|
+
{
|
|
23
|
+
"name": "@openclaw/mcp-shell-exec",
|
|
24
|
+
"versions": ["<1.2.3"],
|
|
25
|
+
"advisory": "Unrestricted shell command execution via WebSocket — CVE-2026-25253"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "mcp-server-puppeteer-unverified",
|
|
29
|
+
"versions": ["*"],
|
|
30
|
+
"advisory": "Typosquat of official puppeteer MCP server, exfiltrates browser cookies"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "@clawhub/skill-auto-deploy",
|
|
34
|
+
"versions": ["*"],
|
|
35
|
+
"advisory": "ClawHavoc campaign skill — installs AMOS stealer on macOS/Linux"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"maliciousConfigSignatures": [
|
|
39
|
+
{
|
|
40
|
+
"pattern": "ignore previous instructions",
|
|
41
|
+
"description": "Prompt injection attempting to override agent system instructions",
|
|
42
|
+
"severity": "critical"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"pattern": "webhook.site",
|
|
46
|
+
"description": "Known data exfiltration service used in prompt injection attacks",
|
|
47
|
+
"severity": "critical"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"pattern": "curl.*\\|.*bash",
|
|
51
|
+
"description": "Remote code execution via piped download-and-execute pattern",
|
|
52
|
+
"severity": "critical"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"pattern": "ngrok\\.io|ngrok\\.app",
|
|
56
|
+
"description": "Tunnel service commonly used for data exfiltration in agent attacks",
|
|
57
|
+
"severity": "critical"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"pattern": "requestbin\\.com|pipedream\\.net",
|
|
61
|
+
"description": "Request interception service used to capture exfiltrated data",
|
|
62
|
+
"severity": "critical"
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"knownVulnerableConfigs": [
|
|
66
|
+
{
|
|
67
|
+
"file": "openclaw.json",
|
|
68
|
+
"check": "host_0000",
|
|
69
|
+
"description": "OpenClaw bound to 0.0.0.0 — CVE-2026-25253 (ClawJacked, CVSS 8.8)",
|
|
70
|
+
"cve": "CVE-2026-25253"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"file": "openclaw.json",
|
|
74
|
+
"check": "no_auth",
|
|
75
|
+
"description": "OpenClaw running without authentication — full agent takeover possible",
|
|
76
|
+
"cve": "CVE-2026-25253"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"file": ".claude/settings.json",
|
|
80
|
+
"check": "malicious_hooks",
|
|
81
|
+
"description": "Claude Code hooks executing arbitrary shell commands — Check Point RCE disclosure",
|
|
82
|
+
"cve": "CVE-2026-XXXX"
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
package/cli/index.js
CHANGED
|
@@ -25,6 +25,11 @@ export { doctorCommand } from './commands/doctor.js';
|
|
|
25
25
|
// ── v4.3 Commands ─────────────────────────────────────────────────────────────
|
|
26
26
|
export { baselineCommand } from './commands/baseline.js';
|
|
27
27
|
|
|
28
|
+
// ── v6.0 Commands ─────────────────────────────────────────────────────────────
|
|
29
|
+
export { diffCommand } from './commands/diff.js';
|
|
30
|
+
export { vibeCheckCommand } from './commands/vibe-check.js';
|
|
31
|
+
export { benchmarkCommand } from './commands/benchmark.js';
|
|
32
|
+
|
|
28
33
|
// ── Patterns ──────────────────────────────────────────────────────────────────
|
|
29
34
|
export { SECRET_PATTERNS, SECURITY_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES } from './utils/patterns.js';
|
|
30
35
|
|
|
@@ -46,6 +51,10 @@ export { GitHistoryScanner } from './agents/git-history-scanner.js';
|
|
|
46
51
|
export { CICDScanner } from './agents/cicd-scanner.js';
|
|
47
52
|
export { APIFuzzer } from './agents/api-fuzzer.js';
|
|
48
53
|
export { SupabaseRLSAgent } from './agents/supabase-rls-agent.js';
|
|
54
|
+
export { VibeCodingAgent } from './agents/vibe-coding-agent.js';
|
|
55
|
+
export { ExceptionHandlerAgent } from './agents/exception-handler-agent.js';
|
|
56
|
+
export { AgentConfigScanner } from './agents/agent-config-scanner.js';
|
|
57
|
+
export { ABOMGenerator } from './agents/abom-generator.js';
|
|
49
58
|
|
|
50
59
|
// ── Supporting Modules ────────────────────────────────────────────────────────
|
|
51
60
|
export { ScoringEngine, GRADES, CATEGORIES } from './agents/scoring-engine.js';
|
|
@@ -23,7 +23,7 @@ import { fileURLToPath } from 'url';
|
|
|
23
23
|
import { dirname, join } from 'path';
|
|
24
24
|
|
|
25
25
|
// Read version from package.json
|
|
26
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url); // ship-safe-ignore — module's own path via import.meta.url, not user input
|
|
27
27
|
const __dirname = dirname(__filename);
|
|
28
28
|
const PACKAGE_VERSION = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8')).version;
|
|
29
29
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compliance Mapping Utility
|
|
3
|
+
* ===========================
|
|
4
|
+
*
|
|
5
|
+
* Maps CWE/OWASP findings to compliance frameworks:
|
|
6
|
+
* - SOC 2 Type II (Trust Service Criteria)
|
|
7
|
+
* - ISO 27001:2022 (Annex A controls)
|
|
8
|
+
* - NIST AI Risk Management Framework (AI RMF 1.0)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// CWE → COMPLIANCE CONTROL MAPPING
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
const CWE_MAP = {
|
|
16
|
+
'CWE-74': { soc2: ['CC6.1', 'CC7.1'], iso27001: ['A.8.28'], nistAiRmf: ['MAP 1.5', 'MEASURE 2.6'] },
|
|
17
|
+
'CWE-78': { soc2: ['CC6.1', 'CC7.1'], iso27001: ['A.8.28'], nistAiRmf: ['MAP 1.5'] },
|
|
18
|
+
'CWE-79': { soc2: ['CC6.1'], iso27001: ['A.8.28'], nistAiRmf: [] },
|
|
19
|
+
'CWE-89': { soc2: ['CC6.1'], iso27001: ['A.8.28'], nistAiRmf: [] },
|
|
20
|
+
'CWE-94': { soc2: ['CC6.1', 'CC7.1'], iso27001: ['A.8.28', 'A.8.9'], nistAiRmf: ['MAP 1.5'] },
|
|
21
|
+
'CWE-116': { soc2: ['CC6.1'], iso27001: ['A.8.28'], nistAiRmf: ['MEASURE 2.6'] },
|
|
22
|
+
'CWE-200': { soc2: ['CC6.5', 'CC6.1'], iso27001: ['A.8.11', 'A.5.33'], nistAiRmf: ['GOVERN 1.7', 'MAP 5.1'] },
|
|
23
|
+
'CWE-250': { soc2: ['CC6.3'], iso27001: ['A.8.2'], nistAiRmf: ['GOVERN 1.4'] },
|
|
24
|
+
'CWE-269': { soc2: ['CC6.3', 'CC6.1'], iso27001: ['A.8.2', 'A.5.15'], nistAiRmf: ['GOVERN 1.4'] },
|
|
25
|
+
'CWE-287': { soc2: ['CC6.1', 'CC6.2'], iso27001: ['A.8.5'], nistAiRmf: [] },
|
|
26
|
+
'CWE-306': { soc2: ['CC6.1', 'CC6.2'], iso27001: ['A.8.5'], nistAiRmf: ['GOVERN 1.4'] },
|
|
27
|
+
'CWE-311': { soc2: ['CC6.7'], iso27001: ['A.8.24'], nistAiRmf: [] },
|
|
28
|
+
'CWE-312': { soc2: ['CC6.7', 'CC6.1'], iso27001: ['A.8.24', 'A.5.33'], nistAiRmf: ['MAP 5.1'] },
|
|
29
|
+
'CWE-326': { soc2: ['CC6.7'], iso27001: ['A.8.24'], nistAiRmf: [] },
|
|
30
|
+
'CWE-327': { soc2: ['CC6.7'], iso27001: ['A.8.24'], nistAiRmf: [] },
|
|
31
|
+
'CWE-502': { soc2: ['CC6.1'], iso27001: ['A.8.28'], nistAiRmf: [] },
|
|
32
|
+
'CWE-522': { soc2: ['CC6.1', 'CC6.7'], iso27001: ['A.8.5', 'A.8.24'], nistAiRmf: [] },
|
|
33
|
+
'CWE-611': { soc2: ['CC6.1'], iso27001: ['A.8.28'], nistAiRmf: [] },
|
|
34
|
+
'CWE-668': { soc2: ['CC6.1', 'CC6.6'], iso27001: ['A.8.9', 'A.8.20'], nistAiRmf: ['GOVERN 1.4'] },
|
|
35
|
+
'CWE-798': { soc2: ['CC6.1', 'CC6.7'], iso27001: ['A.5.33', 'A.8.24'], nistAiRmf: [] },
|
|
36
|
+
'CWE-918': { soc2: ['CC6.1', 'CC6.6'], iso27001: ['A.8.20', 'A.8.28'], nistAiRmf: [] },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// OWASP Agentic → NIST AI RMF mapping (agent-specific)
|
|
40
|
+
const AGENTIC_MAP = {
|
|
41
|
+
'ASI01': { soc2: ['CC6.1', 'CC7.2'], iso27001: ['A.8.28', 'A.8.9'], nistAiRmf: ['MAP 1.5', 'MEASURE 2.6', 'MANAGE 2.2'] },
|
|
42
|
+
'ASI02': { soc2: ['CC6.1', 'CC6.3'], iso27001: ['A.8.2', 'A.8.9'], nistAiRmf: ['MAP 1.5', 'GOVERN 1.4', 'MANAGE 2.2'] },
|
|
43
|
+
'ASI03': { soc2: ['CC6.3'], iso27001: ['A.8.2', 'A.5.15'], nistAiRmf: ['GOVERN 1.4', 'MAP 3.4'] },
|
|
44
|
+
'ASI04': { soc2: ['CC6.6', 'CC7.1'], iso27001: ['A.5.19', 'A.5.21'], nistAiRmf: ['MAP 1.5', 'GOVERN 6.1'] },
|
|
45
|
+
'ASI05': { soc2: ['CC6.1', 'CC7.1'], iso27001: ['A.8.28', 'A.8.9'], nistAiRmf: ['MAP 1.5'] },
|
|
46
|
+
'ASI06': { soc2: ['CC6.1'], iso27001: ['A.8.11'], nistAiRmf: ['MEASURE 2.6', 'MANAGE 2.2'] },
|
|
47
|
+
'ASI07': { soc2: ['CC6.1', 'CC6.7'], iso27001: ['A.8.20', 'A.8.24'], nistAiRmf: ['MAP 1.5'] },
|
|
48
|
+
'ASI08': { soc2: ['CC7.4', 'CC7.5'], iso27001: ['A.5.30'], nistAiRmf: ['MANAGE 4.1'] },
|
|
49
|
+
'ASI09': { soc2: ['CC6.2'], iso27001: ['A.8.5', 'A.5.15'], nistAiRmf: ['GOVERN 1.7'] },
|
|
50
|
+
'ASI10': { soc2: ['CC7.2', 'CC7.4'], iso27001: ['A.8.9', 'A.5.30'], nistAiRmf: ['MANAGE 2.2', 'MANAGE 4.1'] },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// PUBLIC API
|
|
55
|
+
// =============================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Map a single finding to compliance controls.
|
|
59
|
+
* @param {object} finding - A finding with `cwe` and `owasp` fields.
|
|
60
|
+
* @returns {{ soc2: string[], iso27001: string[], nistAiRmf: string[] }}
|
|
61
|
+
*/
|
|
62
|
+
export function mapFindingToCompliance(finding) {
|
|
63
|
+
const result = { soc2: new Set(), iso27001: new Set(), nistAiRmf: new Set() };
|
|
64
|
+
|
|
65
|
+
// Map from CWE
|
|
66
|
+
const cwe = finding.cwe || finding.CWE;
|
|
67
|
+
if (cwe && CWE_MAP[cwe]) {
|
|
68
|
+
const m = CWE_MAP[cwe];
|
|
69
|
+
m.soc2.forEach(c => result.soc2.add(c));
|
|
70
|
+
m.iso27001.forEach(c => result.iso27001.add(c));
|
|
71
|
+
m.nistAiRmf.forEach(c => result.nistAiRmf.add(c));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Map from OWASP Agentic
|
|
75
|
+
const owasp = finding.owasp || finding.OWASP;
|
|
76
|
+
if (owasp && AGENTIC_MAP[owasp]) {
|
|
77
|
+
const m = AGENTIC_MAP[owasp];
|
|
78
|
+
m.soc2.forEach(c => result.soc2.add(c));
|
|
79
|
+
m.iso27001.forEach(c => result.iso27001.add(c));
|
|
80
|
+
m.nistAiRmf.forEach(c => result.nistAiRmf.add(c));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
soc2: [...result.soc2].sort(),
|
|
85
|
+
iso27001: [...result.iso27001].sort(),
|
|
86
|
+
nistAiRmf: [...result.nistAiRmf].sort(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Aggregate compliance mappings across all findings.
|
|
92
|
+
* @param {object[]} findings - Array of findings.
|
|
93
|
+
* @returns {{ soc2: object, iso27001: object, nistAiRmf: object, summary: object }}
|
|
94
|
+
*/
|
|
95
|
+
export function getComplianceSummary(findings) {
|
|
96
|
+
const soc2 = {};
|
|
97
|
+
const iso27001 = {};
|
|
98
|
+
const nistAiRmf = {};
|
|
99
|
+
|
|
100
|
+
for (const f of findings) {
|
|
101
|
+
const mapped = mapFindingToCompliance(f);
|
|
102
|
+
|
|
103
|
+
for (const ctrl of mapped.soc2) {
|
|
104
|
+
soc2[ctrl] = (soc2[ctrl] || 0) + 1;
|
|
105
|
+
}
|
|
106
|
+
for (const ctrl of mapped.iso27001) {
|
|
107
|
+
iso27001[ctrl] = (iso27001[ctrl] || 0) + 1;
|
|
108
|
+
}
|
|
109
|
+
for (const ctrl of mapped.nistAiRmf) {
|
|
110
|
+
nistAiRmf[ctrl] = (nistAiRmf[ctrl] || 0) + 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
soc2,
|
|
116
|
+
iso27001,
|
|
117
|
+
nistAiRmf,
|
|
118
|
+
summary: {
|
|
119
|
+
soc2Controls: Object.keys(soc2).length,
|
|
120
|
+
iso27001Controls: Object.keys(iso27001).length,
|
|
121
|
+
nistAiRmfControls: Object.keys(nistAiRmf).length,
|
|
122
|
+
totalFindings: findings.length,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
package/cli/utils/output.js
CHANGED
|
@@ -120,10 +120,13 @@ export function vulnerabilityFinding(file, line, patternName, severity, matched,
|
|
|
120
120
|
* Mask the middle of a secret for safe display
|
|
121
121
|
*/
|
|
122
122
|
export function maskSecret(secret) {
|
|
123
|
-
if (secret.length <=
|
|
123
|
+
if (secret.length <= 6) {
|
|
124
|
+
return '***';
|
|
125
|
+
}
|
|
126
|
+
if (secret.length <= 12) {
|
|
124
127
|
return secret.substring(0, 3) + '***';
|
|
125
128
|
}
|
|
126
|
-
return secret.substring(0,
|
|
129
|
+
return secret.substring(0, 4) + '***' + secret.substring(secret.length - 4);
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
/**
|
package/cli/utils/patterns.js
CHANGED
|
@@ -822,6 +822,9 @@ export const SKIP_FILENAMES = new Set([
|
|
|
822
822
|
'pubspec.lock',
|
|
823
823
|
'go.sum',
|
|
824
824
|
'flake.lock',
|
|
825
|
+
// Skip ship-safe's own output files to avoid scanning the report
|
|
826
|
+
'ship-safe-report.html',
|
|
827
|
+
'ship-safe-report.pdf',
|
|
825
828
|
]);
|
|
826
829
|
|
|
827
830
|
// Maximum file size to scan (1MB)
|
|
@@ -66,7 +66,7 @@ export function generatePDF(htmlPath, outputPath) {
|
|
|
66
66
|
'--print-to-pdf-no-header',
|
|
67
67
|
htmlPath,
|
|
68
68
|
];
|
|
69
|
-
execFileSync(chrome, args, { timeout: 30000, stdio: 'pipe' });
|
|
69
|
+
execFileSync(chrome, args, { timeout: 30000, stdio: 'pipe' }); // ship-safe-ignore — execFileSync with fixed chrome binary path; no user input in command
|
|
70
70
|
return outputPath;
|
|
71
71
|
} catch {
|
|
72
72
|
return null;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Threat Intelligence Feed
|
|
3
|
+
* =========================
|
|
4
|
+
*
|
|
5
|
+
* Loads and queries ship-safe's threat intelligence database.
|
|
6
|
+
* Ships with a seed file, supports offline updates.
|
|
7
|
+
*
|
|
8
|
+
* Data includes:
|
|
9
|
+
* - Known malicious skill hashes (ClawHavoc IOCs)
|
|
10
|
+
* - Compromised MCP server names/versions
|
|
11
|
+
* - Malicious config file signatures
|
|
12
|
+
* - Known vulnerable configurations
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import os from 'os';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import { createHash } from 'crypto';
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = path.dirname(__filename);
|
|
23
|
+
|
|
24
|
+
const SEED_PATH = path.resolve(__dirname, '..', 'data', 'threat-intel.json');
|
|
25
|
+
const LOCAL_CACHE = path.join(os.homedir(), '.ship-safe', 'threat-intel.json');
|
|
26
|
+
const DEFAULT_FEED_URL = 'https://raw.githubusercontent.com/asamassekou10/ship-safe/main/cli/data/threat-intel.json';
|
|
27
|
+
|
|
28
|
+
let _cache = null;
|
|
29
|
+
|
|
30
|
+
export class ThreatIntel {
|
|
31
|
+
/**
|
|
32
|
+
* Load threat intel data — prefers local cache if newer, falls back to seed.
|
|
33
|
+
*/
|
|
34
|
+
static load() {
|
|
35
|
+
if (_cache) return _cache;
|
|
36
|
+
|
|
37
|
+
let data = null;
|
|
38
|
+
|
|
39
|
+
// Try local cache first
|
|
40
|
+
try {
|
|
41
|
+
if (fs.existsSync(LOCAL_CACHE)) {
|
|
42
|
+
data = JSON.parse(fs.readFileSync(LOCAL_CACHE, 'utf-8'));
|
|
43
|
+
}
|
|
44
|
+
} catch { /* skip */ }
|
|
45
|
+
|
|
46
|
+
// Fall back to seed
|
|
47
|
+
if (!data) {
|
|
48
|
+
try {
|
|
49
|
+
data = JSON.parse(fs.readFileSync(SEED_PATH, 'utf-8'));
|
|
50
|
+
} catch {
|
|
51
|
+
data = { version: '0.0.0', maliciousSkillHashes: [], compromisedMcpServers: [], maliciousConfigSignatures: [], knownVulnerableConfigs: [] };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_cache = data;
|
|
56
|
+
return data;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check a SHA-256 hash against known malicious skill hashes.
|
|
61
|
+
* @returns {object|null} matching entry or null
|
|
62
|
+
*/
|
|
63
|
+
static lookupHash(sha256) {
|
|
64
|
+
const data = ThreatIntel.load();
|
|
65
|
+
return data.maliciousSkillHashes.find(h => h.sha256 === sha256) || null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if an MCP server name/version is known compromised.
|
|
70
|
+
* @returns {object|null} matching advisory or null
|
|
71
|
+
*/
|
|
72
|
+
static lookupMcpServer(name, version = '*') {
|
|
73
|
+
const data = ThreatIntel.load();
|
|
74
|
+
return data.compromisedMcpServers.find(s => {
|
|
75
|
+
if (s.name !== name) return false;
|
|
76
|
+
if (s.versions.includes('*')) return true;
|
|
77
|
+
return s.versions.some(v => {
|
|
78
|
+
if (v.startsWith('<')) {
|
|
79
|
+
return version < v.slice(1);
|
|
80
|
+
}
|
|
81
|
+
return v === version;
|
|
82
|
+
});
|
|
83
|
+
}) || null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Scan content for known malicious config signatures.
|
|
88
|
+
* @returns {object[]} matching signatures
|
|
89
|
+
*/
|
|
90
|
+
static matchSignatures(content) {
|
|
91
|
+
const data = ThreatIntel.load();
|
|
92
|
+
const matches = [];
|
|
93
|
+
for (const sig of data.maliciousConfigSignatures) {
|
|
94
|
+
try {
|
|
95
|
+
const re = new RegExp(sig.pattern, 'gi');
|
|
96
|
+
if (re.test(content)) {
|
|
97
|
+
matches.push(sig);
|
|
98
|
+
}
|
|
99
|
+
} catch { /* skip bad patterns */ }
|
|
100
|
+
}
|
|
101
|
+
return matches;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Compute SHA-256 hash of content.
|
|
106
|
+
*/
|
|
107
|
+
static hash(content) {
|
|
108
|
+
return createHash('sha256').update(content).digest('hex');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Update local threat intel cache from remote feed.
|
|
113
|
+
* @returns {{ updated: boolean, oldVersion: string, newVersion: string }}
|
|
114
|
+
*/
|
|
115
|
+
static async update(feedUrl = DEFAULT_FEED_URL) {
|
|
116
|
+
const current = ThreatIntel.load();
|
|
117
|
+
const oldVersion = current.version;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const response = await fetch(feedUrl);
|
|
121
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
122
|
+
|
|
123
|
+
const remote = await response.json();
|
|
124
|
+
|
|
125
|
+
// Only update if remote is newer
|
|
126
|
+
if (remote.version <= oldVersion) {
|
|
127
|
+
return { updated: false, oldVersion, newVersion: oldVersion, message: 'Already up to date' };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Write to local cache
|
|
131
|
+
const cacheDir = path.dirname(LOCAL_CACHE);
|
|
132
|
+
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
|
|
133
|
+
fs.writeFileSync(LOCAL_CACHE, JSON.stringify(remote, null, 2));
|
|
134
|
+
|
|
135
|
+
// Invalidate in-memory cache
|
|
136
|
+
_cache = remote;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
updated: true,
|
|
140
|
+
oldVersion,
|
|
141
|
+
newVersion: remote.version,
|
|
142
|
+
stats: {
|
|
143
|
+
hashes: remote.maliciousSkillHashes?.length || 0,
|
|
144
|
+
servers: remote.compromisedMcpServers?.length || 0,
|
|
145
|
+
signatures: remote.maliciousConfigSignatures?.length || 0,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
} catch (err) {
|
|
149
|
+
return { updated: false, oldVersion, newVersion: oldVersion, error: err.message };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get summary stats of loaded intel data.
|
|
155
|
+
*/
|
|
156
|
+
static stats() {
|
|
157
|
+
const data = ThreatIntel.load();
|
|
158
|
+
return {
|
|
159
|
+
version: data.version,
|
|
160
|
+
updated: data.updated,
|
|
161
|
+
hashes: data.maliciousSkillHashes?.length || 0,
|
|
162
|
+
servers: data.compromisedMcpServers?.length || 0,
|
|
163
|
+
signatures: data.maliciousConfigSignatures?.length || 0,
|
|
164
|
+
configs: data.knownVulnerableConfigs?.length || 0,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ship-safe",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "AI-powered multi-agent security platform.
|
|
3
|
+
"version": "6.1.0",
|
|
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": {
|
|
7
7
|
"ship-safe": "cli/bin/ship-safe.js"
|