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,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": "6.0.0",
4
- "description": "AI-powered multi-agent security platform. 17 agents scan 80+ attack classes with LLM-powered deep analysis. Red team your code before attackers do.",
3
+ "version": "6.1.1",
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"
@@ -53,6 +53,7 @@
53
53
  },
54
54
  "files": [
55
55
  "cli/",
56
+ "!cli/__tests__/",
56
57
  "checklists/",
57
58
  "configs/",
58
59
  "snippets/",