ship-safe 6.0.0 → 6.1.1

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.
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Scan Skill Command
3
+ * ===================
4
+ *
5
+ * Downloads and analyzes an AI agent skill before installation.
6
+ * Checks for malicious patterns, permission abuse, typosquatting,
7
+ * and known threat intelligence indicators.
8
+ *
9
+ * USAGE:
10
+ * ship-safe scan-skill <url> Analyze a skill from URL
11
+ * ship-safe scan-skill <path> Analyze a local skill file
12
+ * ship-safe scan-skill . --all Scan all skills in openclaw.json
13
+ */
14
+
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import chalk from 'chalk';
18
+ import { createHash } from 'crypto';
19
+ import * as output from '../utils/output.js';
20
+ import { ThreatIntel } from '../utils/threat-intel.js';
21
+
22
+ // =============================================================================
23
+ // POPULAR SKILL NAMES (for typosquatting detection)
24
+ // =============================================================================
25
+
26
+ const POPULAR_SKILLS = [
27
+ 'web-search', 'web-browser', 'file-manager', 'code-runner',
28
+ 'git-helper', 'database-query', 'api-tester', 'image-gen',
29
+ 'text-to-speech', 'pdf-reader', 'email-sender', 'slack-bot',
30
+ 'github-helper', 'docker-manager', 'kubernetes-helper',
31
+ 'aws-helper', 'terraform-helper', 'memory-store',
32
+ 'calculator', 'translator', 'summarizer', 'code-review',
33
+ ];
34
+
35
+ // =============================================================================
36
+ // MALICIOUS PATTERNS
37
+ // =============================================================================
38
+
39
+ const SKILL_PATTERNS = [
40
+ { name: 'Shell execution', regex: /(?:child_process|exec|spawn|execSync|execFile|os\.system|subprocess|shell_exec|system\()/gi, severity: 'critical' },
41
+ { name: 'Outbound HTTP to non-localhost', regex: /(?:fetch|axios|http\.get|requests\.get|urllib|wget|curl)\s*\(\s*['"`]https?:\/\/(?!(?:localhost|127\.0\.0\.1|::1))/gi, severity: 'high' },
42
+ { name: 'Data exfiltration service', regex: /(?:webhook\.site|requestbin\.com|hookbin\.com|pipedream\.net|ngrok\.io|ngrok\.app|burpcollaborator|interact\.sh)/gi, severity: 'critical' },
43
+ { name: 'Environment variable access', regex: /(?:process\.env|os\.environ|os\.getenv|ENV\[|System\.getenv)/gi, severity: 'medium' },
44
+ { name: 'File system write', regex: /(?:fs\.writeFile|fs\.appendFile|writeFileSync|open\(.+['"]w['"]|fwrite|file_put_contents)/gi, severity: 'medium' },
45
+ { name: 'Base64 decode + execute', regex: /(?:atob|Buffer\.from|base64\.b64decode|base64_decode)\s*\([^)]*\)\s*(?:\.|\))\s*(?:eval|exec|Function)/gi, severity: 'critical' },
46
+ { name: 'Dynamic code evaluation', regex: /(?:eval\s*\(|new\s+Function\s*\(|exec\s*\(|compile\s*\()/gi, severity: 'high' },
47
+ { name: 'Crypto operations', regex: /(?:crypto\.createCipher|crypto\.createDecipher|CryptoJS|forge\.cipher)/gi, severity: 'medium' },
48
+ { name: 'Network listener', regex: /(?:createServer|listen\s*\(\s*\d|bind\s*\(\s*['"]0\.0\.0\.0)/gi, severity: 'high' },
49
+ { name: 'Encoded payload block', regex: /[A-Za-z0-9+\/]{60,}={0,2}/g, severity: 'medium' },
50
+ ];
51
+
52
+ // =============================================================================
53
+ // MAIN COMMAND
54
+ // =============================================================================
55
+
56
+ export async function scanSkillCommand(target, options = {}) {
57
+ if (!target) {
58
+ output.error('Usage: ship-safe scan-skill <url|path>');
59
+ output.info(' Analyze an AI agent skill for security issues before installing it.');
60
+ process.exit(1);
61
+ }
62
+
63
+ console.log();
64
+ output.header('Ship Safe — Skill Security Analysis');
65
+ console.log();
66
+
67
+ // If --all flag, scan all skills from openclaw.json
68
+ if (options.all) {
69
+ return scanAllSkills(path.resolve(target));
70
+ }
71
+
72
+ // Determine if URL or local file
73
+ let content, skillName, source;
74
+
75
+ if (target.startsWith('http://') || target.startsWith('https://')) {
76
+ console.log(chalk.gray(` Fetching skill from: ${target}`));
77
+ try {
78
+ const response = await fetch(target);
79
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
80
+ content = await response.text();
81
+ skillName = new URL(target).pathname.split('/').pop() || 'remote-skill';
82
+ source = target;
83
+ } catch (err) {
84
+ output.error(`Failed to fetch skill: ${err.message}`);
85
+ process.exit(1);
86
+ }
87
+ } else {
88
+ const filePath = path.resolve(target);
89
+ if (!fs.existsSync(filePath)) {
90
+ output.error(`File not found: ${filePath}`);
91
+ process.exit(1);
92
+ }
93
+ content = fs.readFileSync(filePath, 'utf-8');
94
+ skillName = path.basename(filePath);
95
+ source = filePath;
96
+ }
97
+
98
+ console.log(chalk.gray(` Skill: ${skillName}`));
99
+ console.log(chalk.gray(` Size: ${content.length} bytes`));
100
+ console.log();
101
+
102
+ const findings = analyzeSkill(content, skillName, source);
103
+
104
+ if (options.json) {
105
+ console.log(JSON.stringify({ skill: skillName, source, findings, summary: getSummary(findings) }, null, 2));
106
+ return;
107
+ }
108
+
109
+ printSkillFindings(findings, skillName);
110
+ }
111
+
112
+ // =============================================================================
113
+ // SKILL ANALYSIS
114
+ // =============================================================================
115
+
116
+ function analyzeSkill(content, skillName, source) {
117
+ const findings = [];
118
+
119
+ // 1. Static pattern analysis
120
+ const lines = content.split('\n');
121
+ for (let i = 0; i < lines.length; i++) {
122
+ const line = lines[i];
123
+ for (const pattern of SKILL_PATTERNS) {
124
+ pattern.regex.lastIndex = 0;
125
+ if (pattern.regex.test(line)) {
126
+ findings.push({
127
+ check: 'static-analysis',
128
+ name: pattern.name,
129
+ severity: pattern.severity,
130
+ line: i + 1,
131
+ matched: line.trim().slice(0, 100),
132
+ });
133
+ }
134
+ }
135
+ }
136
+
137
+ // 2. Permission manifest audit (if JSON)
138
+ try {
139
+ const manifest = JSON.parse(content);
140
+ if (manifest.permissions) {
141
+ const dangerous = ['shell', 'exec', 'system', 'network', 'filesystem', 'admin', 'root'];
142
+ for (const perm of (Array.isArray(manifest.permissions) ? manifest.permissions : [])) {
143
+ const permStr = typeof perm === 'string' ? perm : perm.name || '';
144
+ if (dangerous.some(d => permStr.toLowerCase().includes(d))) {
145
+ findings.push({
146
+ check: 'permission-audit',
147
+ name: `Dangerous permission: ${permStr}`,
148
+ severity: 'high',
149
+ line: 0,
150
+ matched: `permissions: [${permStr}]`,
151
+ });
152
+ }
153
+ }
154
+ }
155
+
156
+ // Check for suspicious fields
157
+ if (manifest.postInstall || manifest.postinstall) {
158
+ findings.push({
159
+ check: 'permission-audit',
160
+ name: 'Post-install script defined',
161
+ severity: 'high',
162
+ line: 0,
163
+ matched: 'postInstall hook detected',
164
+ });
165
+ }
166
+ } catch { /* Not JSON, skip manifest audit */ }
167
+
168
+ // 3. Typosquatting detection
169
+ const typosquatResult = checkTyposquatting(skillName);
170
+ if (typosquatResult) {
171
+ findings.push({
172
+ check: 'typosquatting',
173
+ name: `Possible typosquat of "${typosquatResult.target}"`,
174
+ severity: 'high',
175
+ line: 0,
176
+ matched: `Levenshtein distance: ${typosquatResult.distance} from "${typosquatResult.target}"`,
177
+ });
178
+ }
179
+
180
+ // 4. Threat intel hash check
181
+ const hash = createHash('sha256').update(content).digest('hex');
182
+ const intelMatch = ThreatIntel.lookupHash(hash);
183
+ if (intelMatch) {
184
+ findings.push({
185
+ check: 'threat-intel',
186
+ name: `Known malicious skill: ${intelMatch.name}`,
187
+ severity: 'critical',
188
+ line: 0,
189
+ matched: `SHA-256: ${hash} — ${intelMatch.description}`,
190
+ });
191
+ }
192
+
193
+ // 5. Threat intel signature check
194
+ const sigMatches = ThreatIntel.matchSignatures(content);
195
+ for (const sig of sigMatches) {
196
+ findings.push({
197
+ check: 'threat-intel',
198
+ name: `Threat intel signature match: ${sig.description}`,
199
+ severity: sig.severity || 'critical',
200
+ line: 0,
201
+ matched: `Pattern: ${sig.pattern}`,
202
+ });
203
+ }
204
+
205
+ return findings;
206
+ }
207
+
208
+ // =============================================================================
209
+ // TYPOSQUATTING
210
+ // =============================================================================
211
+
212
+ function levenshtein(a, b) {
213
+ const matrix = [];
214
+ for (let i = 0; i <= b.length; i++) matrix[i] = [i];
215
+ for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
216
+ for (let i = 1; i <= b.length; i++) {
217
+ for (let j = 1; j <= a.length; j++) {
218
+ matrix[i][j] = b[i - 1] === a[j - 1]
219
+ ? matrix[i - 1][j - 1]
220
+ : Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
221
+ }
222
+ }
223
+ return matrix[b.length][a.length];
224
+ }
225
+
226
+ function checkTyposquatting(skillName) {
227
+ const name = skillName.toLowerCase().replace(/[^a-z0-9-]/g, '');
228
+ for (const popular of POPULAR_SKILLS) {
229
+ const distance = levenshtein(name, popular);
230
+ if (distance > 0 && distance <= 2 && name !== popular) {
231
+ return { target: popular, distance };
232
+ }
233
+ }
234
+ return null;
235
+ }
236
+
237
+ // =============================================================================
238
+ // SCAN ALL SKILLS IN PROJECT
239
+ // =============================================================================
240
+
241
+ async function scanAllSkills(rootPath) {
242
+ const openclawPath = path.join(rootPath, 'openclaw.json');
243
+ if (!fs.existsSync(openclawPath)) {
244
+ output.warning('No openclaw.json found. Nothing to scan.');
245
+ return;
246
+ }
247
+
248
+ try {
249
+ const config = JSON.parse(fs.readFileSync(openclawPath, 'utf-8'));
250
+ const skills = config.skills || [];
251
+
252
+ if (skills.length === 0) {
253
+ output.info('No skills defined in openclaw.json.');
254
+ return;
255
+ }
256
+
257
+ console.log(chalk.gray(` Found ${skills.length} skill(s) in openclaw.json`));
258
+ console.log();
259
+
260
+ for (const skill of skills) {
261
+ const url = typeof skill === 'string' ? skill : skill.source || skill.url;
262
+ const name = typeof skill === 'string' ? skill : skill.name || 'unnamed';
263
+
264
+ if (url && (url.startsWith('http://') || url.startsWith('https://'))) {
265
+ console.log(chalk.cyan(` Scanning skill: ${name}`));
266
+ try {
267
+ const response = await fetch(url);
268
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
269
+ const content = await response.text();
270
+ const findings = analyzeSkill(content, name, url);
271
+ if (findings.length > 0) {
272
+ printSkillFindings(findings, name);
273
+ } else {
274
+ console.log(chalk.green(` ✔ Clean`));
275
+ }
276
+ } catch (err) {
277
+ console.log(chalk.yellow(` ⚠ Could not fetch: ${err.message}`));
278
+ }
279
+ } else {
280
+ console.log(chalk.gray(` → ${name}: local skill (static analysis only)`));
281
+ }
282
+ console.log();
283
+ }
284
+ } catch (err) {
285
+ output.error(`Failed to parse openclaw.json: ${err.message}`);
286
+ }
287
+ }
288
+
289
+ // =============================================================================
290
+ // OUTPUT
291
+ // =============================================================================
292
+
293
+ function printSkillFindings(findings, skillName) {
294
+ const summary = getSummary(findings);
295
+
296
+ if (findings.length === 0) {
297
+ console.log(chalk.green.bold(` ✔ ${skillName}: No security issues found.`));
298
+ console.log();
299
+ return;
300
+ }
301
+
302
+ console.log(chalk.red.bold(` ✘ ${skillName}: ${findings.length} issue(s) found`));
303
+ console.log();
304
+
305
+ for (const f of findings) {
306
+ const sevColor = f.severity === 'critical' ? chalk.red.bold
307
+ : f.severity === 'high' ? chalk.yellow
308
+ : chalk.blue;
309
+
310
+ console.log(` ${sevColor(`[${f.severity.toUpperCase()}]`)} ${chalk.white(f.name)}`);
311
+ if (f.line > 0) console.log(chalk.gray(` Line ${f.line}: ${f.matched}`));
312
+ else if (f.matched) console.log(chalk.gray(` ${f.matched}`));
313
+ }
314
+ console.log();
315
+
316
+ if (summary.critical > 0) {
317
+ console.log(chalk.red.bold(' ⚠ DO NOT INSTALL this skill — critical security issues detected.'));
318
+ console.log();
319
+ }
320
+ }
321
+
322
+ function getSummary(findings) {
323
+ return {
324
+ total: findings.length,
325
+ critical: findings.filter(f => f.severity === 'critical').length,
326
+ high: findings.filter(f => f.severity === 'high').length,
327
+ medium: findings.filter(f => f.severity === 'medium').length,
328
+ };
329
+ }
@@ -457,6 +457,8 @@ function outputPretty(results, filesScanned, rootPath) {
457
457
  console.log();
458
458
  console.log(chalk.gray('Suppress a finding: add # ship-safe-ignore as a comment on that line'));
459
459
  console.log(chalk.gray('Exclude a path: add it to .ship-safeignore'));
460
+ console.log();
461
+ console.log(chalk.gray('Track findings over time: ') + chalk.cyan('https://shipsafecli.com'));
460
462
 
461
463
  if (secretResults.length > 0) output.recommendations();
462
464
  if (vulnResults.length > 0) output.vulnRecommendations();
@@ -299,6 +299,7 @@ function printScore(score, grade, ctx) {
299
299
  }
300
300
 
301
301
  console.log(chalk.cyan('='.repeat(60)));
302
+ console.log(chalk.gray(' Share your score & track trends: ') + chalk.cyan('https://shipsafecli.com'));
302
303
  console.log();
303
304
  }
304
305
 
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Update Intel Command
3
+ * =====================
4
+ *
5
+ * Updates the local threat intelligence feed from the remote source.
6
+ *
7
+ * USAGE:
8
+ * ship-safe update-intel Fetch latest threat intel
9
+ * ship-safe update-intel --url <url> Use custom feed URL
10
+ */
11
+
12
+ import chalk from 'chalk';
13
+ import * as output from '../utils/output.js';
14
+ import { ThreatIntel } from '../utils/threat-intel.js';
15
+
16
+ export async function updateIntelCommand(options = {}) {
17
+ console.log();
18
+ output.header('Ship Safe — Threat Intelligence Update');
19
+ console.log();
20
+
21
+ const currentStats = ThreatIntel.stats();
22
+ console.log(chalk.gray(` Current version: ${currentStats.version}`));
23
+ console.log(chalk.gray(` Last updated: ${currentStats.updated || 'unknown'}`));
24
+ console.log(chalk.gray(` Indicators: ${currentStats.hashes} hashes, ${currentStats.servers} servers, ${currentStats.signatures} signatures`));
25
+ console.log();
26
+
27
+ console.log(chalk.cyan(' Checking for updates...'));
28
+
29
+ const result = await ThreatIntel.update(options.url);
30
+
31
+ if (result.error) {
32
+ output.error('Update failed: ' + result.error); // ship-safe-ignore
33
+ console.log(chalk.gray(' The local seed data will still be used for scanning.'));
34
+ console.log(chalk.gray(' Check your network connection and try again.'));
35
+ console.log();
36
+ return;
37
+ }
38
+
39
+ if (!result.updated) {
40
+ console.log(chalk.green(' ✔ Already up to date.'));
41
+ console.log();
42
+ return;
43
+ }
44
+
45
+ console.log();
46
+ console.log(chalk.green.bold(' ✔ Threat intelligence updated!'));
47
+ console.log();
48
+ console.log(` ${chalk.gray('Version:')} ${result.oldVersion} → ${chalk.cyan(result.newVersion)}`);
49
+ if (result.stats) {
50
+ console.log(` ${chalk.gray('Malicious skill hashes:')} ${result.stats.hashes}`);
51
+ console.log(` ${chalk.gray('Compromised MCP servers:')} ${result.stats.servers}`);
52
+ console.log(` ${chalk.gray('Config signatures:')} ${result.stats.signatures}`);
53
+ }
54
+ console.log();
55
+ }
@@ -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();
@@ -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
@@ -53,6 +53,8 @@ export { APIFuzzer } from './agents/api-fuzzer.js';
53
53
  export { SupabaseRLSAgent } from './agents/supabase-rls-agent.js';
54
54
  export { VibeCodingAgent } from './agents/vibe-coding-agent.js';
55
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';
56
58
 
57
59
  // ── Supporting Modules ────────────────────────────────────────────────────────
58
60
  export { ScoringEngine, GRADES, CATEGORIES } from './agents/scoring-engine.js';