@guava-parity/guard-scanner 15.0.0 → 16.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.
Files changed (61) hide show
  1. package/README.md +208 -42
  2. package/README_ja.md +252 -0
  3. package/SKILL.md +40 -11
  4. package/dist/cli.cjs +5997 -0
  5. package/dist/cli.d.mts +1 -0
  6. package/dist/cli.d.ts +1 -0
  7. package/dist/cli.mjs +6003 -0
  8. package/dist/index.cjs +4825 -0
  9. package/dist/index.d.mts +17 -0
  10. package/dist/index.d.ts +17 -0
  11. package/dist/index.mjs +4798 -0
  12. package/dist/mcp-server.cjs +4756 -0
  13. package/dist/mcp-server.d.mts +1 -0
  14. package/dist/mcp-server.d.ts +1 -0
  15. package/dist/mcp-server.mjs +4767 -0
  16. package/dist/openclaw-plugin.cjs +4863 -0
  17. package/dist/openclaw-plugin.d.mts +11 -0
  18. package/dist/openclaw-plugin.d.ts +11 -0
  19. package/dist/openclaw-plugin.mjs +4847 -34
  20. package/dist/types.cjs +18 -0
  21. package/dist/types.d.mts +215 -0
  22. package/dist/types.d.ts +215 -0
  23. package/dist/types.mjs +1 -0
  24. package/docs/data/benchmark-ledger.json +1428 -0
  25. package/docs/data/corpus-metrics.json +3 -3
  26. package/docs/data/fp-ledger.json +18 -0
  27. package/docs/data/quality-contract.json +36 -0
  28. package/docs/generated/openclaw-upstream-status.json +13 -13
  29. package/docs/openclaw-compatibility-audit.md +3 -2
  30. package/docs/openclaw-continuous-compatibility-plan.md +2 -1
  31. package/docs/spec/capabilities.json +137 -5
  32. package/docs/spec/plugin-trust.json +11 -0
  33. package/hooks/{context.js → context.ts} +1 -0
  34. package/openclaw-plugin.mts +21 -5
  35. package/openclaw.plugin.json +2 -2
  36. package/package.json +58 -20
  37. package/src/asset-auditor.js +0 -508
  38. package/src/ci-reporter.js +0 -135
  39. package/src/cli.js +0 -434
  40. package/src/core/content-loader.js +0 -42
  41. package/src/core/inventory.js +0 -73
  42. package/src/core/report-adapters.js +0 -171
  43. package/src/core/risk-engine.js +0 -93
  44. package/src/core/rule-registry.js +0 -73
  45. package/src/core/semantic-validators.js +0 -85
  46. package/src/finding-schema.js +0 -191
  47. package/src/hooks/context.ts +0 -49
  48. package/src/html-template.js +0 -239
  49. package/src/ioc-db.js +0 -54
  50. package/src/mcp-server.js +0 -653
  51. package/src/openclaw-upstream.js +0 -128
  52. package/src/patterns.js +0 -629
  53. package/src/policy-engine.js +0 -32
  54. package/src/quarantine.js +0 -41
  55. package/src/runtime-guard.js +0 -384
  56. package/src/scanner.js +0 -1042
  57. package/src/skill-crawler.js +0 -254
  58. package/src/threat-model.js +0 -50
  59. package/src/validation-layer.js +0 -39
  60. package/src/vt-client.js +0 -202
  61. package/src/watcher.js +0 -170
@@ -1,254 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * guard-scanner — Skill Crawler
4
- *
5
- * @security-manifest
6
- * env-read: [GITHUB_TOKEN (optional, for higher rate limits)]
7
- * env-write: []
8
- * network: [GitHub REST API, raw.githubusercontent.com, ClawHub registry]
9
- * fs-read: []
10
- * fs-write: []
11
- * exec: none
12
- * purpose: Crawl ClawHub/GitHub for SKILL.md files and scan for threats
13
- */
14
-
15
- const { httpGet } = require('./asset-auditor.js');
16
- const { GuardScanner } = require('./scanner.js');
17
-
18
- const CRAWLER_VERSION = '1.0.0';
19
-
20
- // ClawHub skills repo (openclaw/skills on GitHub)
21
- const CLAWHUB_OWNER = 'openclaw';
22
- const CLAWHUB_REPO = 'skills';
23
- const CLAWHUB_BRANCH = 'main';
24
-
25
- class SkillCrawler {
26
- constructor(options = {}) {
27
- this.verbose = options.verbose || false;
28
- this.quiet = options.quiet || false;
29
- this.concurrency = options.concurrency || 5;
30
- this.scanner = new GuardScanner({
31
- verbose: false,
32
- soulLock: true,
33
- quiet: true,
34
- });
35
- this._httpGet = options._httpGet || httpGet;
36
- this.results = [];
37
- this.errors = [];
38
- }
39
-
40
- /**
41
- * Crawl ClawHub (openclaw/skills) for SKILL.md files
42
- * Uses GitHub tree API to list all SKILL.md paths, then fetches each
43
- */
44
- async crawlClawHub(opts = {}) {
45
- const maxSkills = opts.maxSkills || 50;
46
- if (!this.quiet) console.log(`\n🔍 Crawling ClawHub (${CLAWHUB_OWNER}/${CLAWHUB_REPO})...`);
47
-
48
- try {
49
- // Get recursive tree to find all SKILL.md files
50
- const treeUrl = `https://api.github.com/repos/${CLAWHUB_OWNER}/${CLAWHUB_REPO}/git/trees/${CLAWHUB_BRANCH}?recursive=1`;
51
- const response = await this._httpGet(treeUrl, {
52
- headers: this._getHeaders(),
53
- });
54
-
55
- if (response.status !== 200) {
56
- this.errors.push({ source: 'clawhub', error: `API returned ${response.status}` });
57
- return this.results;
58
- }
59
-
60
- const tree = response.data.tree || [];
61
- const skillMds = tree
62
- .filter(item => item.type === 'blob' && /SKILL\.md$/i.test(item.path))
63
- .slice(0, maxSkills);
64
-
65
- if (!this.quiet) console.log(`📦 Found ${skillMds.length} SKILL.md files`);
66
-
67
- // Batch fetch and scan
68
- await this._batchProcess(skillMds.map(item => ({
69
- source: 'clawhub',
70
- path: item.path,
71
- rawUrl: `https://raw.githubusercontent.com/${CLAWHUB_OWNER}/${CLAWHUB_REPO}/${CLAWHUB_BRANCH}/${item.path}`,
72
- name: this._extractSkillName(item.path),
73
- })));
74
-
75
- } catch (e) {
76
- this.errors.push({ source: 'clawhub', error: e.message });
77
- }
78
-
79
- return this.results;
80
- }
81
-
82
- /**
83
- * Crawl GitHub code search for SKILL.md files matching a query
84
- * e.g. query "polymarket" finds gambling/trading skills
85
- */
86
- async crawlGitHub(query, opts = {}) {
87
- const maxResults = opts.maxResults || 20;
88
- if (!this.quiet) console.log(`\n🔍 GitHub code search: "${query}" + SKILL.md...`);
89
-
90
- try {
91
- const searchUrl = `https://api.github.com/search/code?q=${encodeURIComponent(query)}+filename:SKILL.md&per_page=${maxResults}`;
92
- const response = await this._httpGet(searchUrl, {
93
- headers: this._getHeaders(),
94
- });
95
-
96
- if (response.status !== 200) {
97
- this.errors.push({ source: 'github', error: `Search API returned ${response.status}` });
98
- return this.results;
99
- }
100
-
101
- const items = (response.data.items || []).slice(0, maxResults);
102
- if (!this.quiet) console.log(`📦 Found ${items.length} SKILL.md matches`);
103
-
104
- await this._batchProcess(items.map(item => ({
105
- source: 'github',
106
- path: item.path,
107
- rawUrl: item.html_url
108
- .replace('github.com', 'raw.githubusercontent.com')
109
- .replace('/blob/', '/'),
110
- name: item.repository?.full_name || item.path,
111
- repo: item.repository?.full_name,
112
- })));
113
-
114
- } catch (e) {
115
- this.errors.push({ source: 'github', error: e.message });
116
- }
117
-
118
- return this.results;
119
- }
120
-
121
- /**
122
- * Scan a single SKILL.md URL
123
- */
124
- async scanUrl(url, name = 'unknown') {
125
- try {
126
- const response = await this._httpGet(url);
127
- if (response.status !== 200) {
128
- this.errors.push({ source: 'url', url, error: `HTTP ${response.status}` });
129
- return null;
130
- }
131
-
132
- const content = typeof response.data === 'string'
133
- ? response.data
134
- : JSON.stringify(response.data);
135
-
136
- const scanResult = this.scanner.scanText(content);
137
-
138
- const result = {
139
- name,
140
- url,
141
- content_length: content.length,
142
- safe: scanResult.safe,
143
- risk: scanResult.risk,
144
- detection_count: scanResult.detections.length,
145
- detections: scanResult.detections,
146
- scanned_at: new Date().toISOString(),
147
- };
148
-
149
- this.results.push(result);
150
- return result;
151
-
152
- } catch (e) {
153
- this.errors.push({ source: 'url', url, error: e.message });
154
- return null;
155
- }
156
- }
157
-
158
- /**
159
- * Process items in batches with concurrency control
160
- */
161
- async _batchProcess(items) {
162
- for (let i = 0; i < items.length; i += this.concurrency) {
163
- const batch = items.slice(i, i + this.concurrency);
164
- const promises = batch.map(item => this.scanUrl(item.rawUrl, item.name));
165
- const results = await Promise.allSettled(promises);
166
-
167
- // Log progress
168
- if (!this.quiet) {
169
- for (let j = 0; j < batch.length; j++) {
170
- const r = results[j];
171
- if (r.status === 'fulfilled' && r.value) {
172
- const icon = r.value.safe ? '🟢' : '🔴';
173
- console.log(`${icon} ${batch[j].name} — risk: ${r.value.risk} (${r.value.detection_count} findings)`);
174
- } else {
175
- console.log(`⚠️ ${batch[j].name} — fetch failed`);
176
- }
177
- }
178
- }
179
- }
180
- }
181
-
182
- /**
183
- * Extract skill name from path like "skills/author/skill-name/SKILL.md"
184
- */
185
- _extractSkillName(filePath) {
186
- const parts = filePath.split('/');
187
- // typically: skills/<author>/<skill-name>/SKILL.md
188
- if (parts.length >= 3) {
189
- return `${parts[parts.length - 3]}/${parts[parts.length - 2]}`;
190
- }
191
- return parts.slice(0, -1).join('/');
192
- }
193
-
194
- _getHeaders() {
195
- const headers = { 'User-Agent': `guard-scanner-crawler/${CRAWLER_VERSION}` };
196
- if (process.env.GITHUB_TOKEN) {
197
- headers['Authorization'] = `token ${process.env.GITHUB_TOKEN}`;
198
- }
199
- return headers;
200
- }
201
-
202
- // ── Output ────────────────────────────────────────────────────
203
-
204
- getSummary() {
205
- const total = this.results.length;
206
- const safe = this.results.filter(r => r.safe).length;
207
- const unsafe = total - safe;
208
- const highRisk = this.results.filter(r => r.risk >= 80).length;
209
-
210
- return {
211
- total,
212
- safe,
213
- unsafe,
214
- highRisk,
215
- errors: this.errors.length,
216
- results: this.results.sort((a, b) => b.risk - a.risk),
217
- };
218
- }
219
-
220
- toJSON() {
221
- return {
222
- scanner: `guard-scanner-crawler/${CRAWLER_VERSION}`,
223
- timestamp: new Date().toISOString(),
224
- ...this.getSummary(),
225
- };
226
- }
227
-
228
- printSummary() {
229
- const s = this.getSummary();
230
- console.log(`\n${'═'.repeat(54)}`);
231
- console.log(`📊 Crawler Scan Summary`);
232
- console.log(`${'─'.repeat(54)}`);
233
- console.log(` Scanned: ${s.total}`);
234
- console.log(` 🟢 Safe: ${s.safe}`);
235
- console.log(` 🔴 Unsafe: ${s.unsafe}`);
236
- console.log(` 💀 High Risk: ${s.highRisk}`);
237
- if (s.errors > 0) console.log(` ⚠️ Errors: ${s.errors}`);
238
- console.log(`${'═'.repeat(54)}\n`);
239
-
240
- if (s.unsafe > 0) {
241
- console.log(`⚠️ Unsafe skills detected:`);
242
- for (const r of s.results.filter(r => !r.safe)) {
243
- console.log(` 🔴 ${r.name} (risk: ${r.risk}, ${r.detection_count} findings)`);
244
- if (this.verbose) {
245
- for (const d of r.detections.slice(0, 5)) {
246
- console.log(` └─ [${d.severity}] ${d.desc}`);
247
- }
248
- }
249
- }
250
- }
251
- }
252
- }
253
-
254
- module.exports = { SkillCrawler, CRAWLER_VERSION };
@@ -1,50 +0,0 @@
1
- /**
2
- * Threat Model Layer
3
- * Generates a threat model by identifying capabilities (network, exec, fs, etc.)
4
- * within a given context/codebase to contextualize heuristic pattern findings.
5
- */
6
-
7
- const CAPABILITY_PATTERNS = {
8
- network: /(?:fetch|axios|http\.get|https\.request|XMLHttpRequest|WebSocket)/i,
9
- exec: /(?:exec|spawn|child_process|eval|Function|system)/i,
10
- fs_read: /(?:readFileSync|readFile|createReadStream)/i,
11
- fs_write: /(?:writeFileSync|writeFile|createWriteStream|appendFile)/i,
12
- env_access: /(?:process\.env)/i
13
- };
14
-
15
- function generateModel(codeContent) {
16
- const capabilities = {
17
- network: false,
18
- exec: false,
19
- fs_read: false,
20
- fs_write: false,
21
- env_access: false
22
- };
23
-
24
- let riskScore = 0;
25
-
26
- for (const [cap, regex] of Object.entries(CAPABILITY_PATTERNS)) {
27
- if (regex.test(codeContent)) {
28
- capabilities[cap] = true;
29
- riskScore += 10; // Base score for having a risky capability
30
- }
31
- }
32
-
33
- // Capability compounding (e.g. read + network = exfil risk)
34
- if (capabilities.fs_read && capabilities.network) {
35
- riskScore += 20;
36
- }
37
- if (capabilities.env_access && capabilities.network) {
38
- riskScore += 30; // High risk of credential exfiltration
39
- }
40
-
41
- return {
42
- capabilities,
43
- riskScore,
44
- summary: `Capabilities detected: ${Object.keys(capabilities).filter(k => capabilities[k]).join(', ')}`
45
- };
46
- }
47
-
48
- module.exports = {
49
- generateModel
50
- };
@@ -1,39 +0,0 @@
1
- /**
2
- * Validation Layer
3
- * Evaluates heuristic findings against contextual evidence to separate
4
- * "validated" threats from "heuristic-only" (potential false positives).
5
- */
6
-
7
- function validateFindings(findings, context) {
8
- return findings.map(finding => {
9
- let status = 'heuristic-only';
10
-
11
- // Contextual Validation Rules
12
-
13
- // 1. If it's a prompt injection but found inside a code block, it might be a false positive
14
- // (e.g., someone writing an article about prompt injection)
15
- if (finding.id.startsWith('PI_')) {
16
- if (context.isInCodeBlock(finding.text)) {
17
- status = 'heuristic-only'; // False positive likely
18
- } else {
19
- status = 'validated';
20
- }
21
- }
22
-
23
- // 2. If it's malicious code, verify if the execution environment allows it
24
- if (finding.id.startsWith('MAL_')) {
25
- if (context.isExecutable(finding.text)) {
26
- status = 'validated';
27
- }
28
- }
29
-
30
- return {
31
- ...finding,
32
- status
33
- };
34
- });
35
- }
36
-
37
- module.exports = {
38
- validateFindings
39
- };
package/src/vt-client.js DELETED
@@ -1,202 +0,0 @@
1
- /**
2
- * guard-scanner v7 — VirusTotal API v3 Client
3
- *
4
- * @security-manifest
5
- * env-read: [VT_API_KEY]
6
- * env-write: []
7
- * network: [virustotal.com API v3]
8
- * fs-read: [files for SHA256 hashing]
9
- * fs-write: []
10
- * exec: none
11
- * purpose: VirusTotal threat intelligence integration
12
- */
13
-
14
- const https = require('https');
15
- const crypto = require('crypto');
16
- const fs = require('fs');
17
-
18
- const VT_API_BASE = 'https://www.virustotal.com/api/v3';
19
- const VT_RATE_LIMIT = 4; // requests per minute (free tier)
20
-
21
- class VTClient {
22
- constructor(apiKey, options = {}) {
23
- if (!apiKey) throw new Error('VirusTotal API key is required. Set VT_API_KEY environment variable.');
24
- this.apiKey = apiKey;
25
- this.timeout = options.timeout || 15000;
26
- this.verbose = options.verbose || false;
27
- this._requestCount = 0;
28
- this._windowStart = Date.now();
29
- this._httpGet = options._httpGet || null; // DI for testing
30
- this._httpPost = options._httpPost || null;
31
- }
32
-
33
- // ── Rate limiter (4 req/min free tier) ──────────────────────
34
- async _throttle() {
35
- const now = Date.now();
36
- const elapsed = now - this._windowStart;
37
- if (elapsed >= 60000) {
38
- this._requestCount = 0;
39
- this._windowStart = now;
40
- }
41
- if (this._requestCount >= VT_RATE_LIMIT) {
42
- const waitMs = 60000 - elapsed + 100;
43
- if (this.verbose) console.log(`⏳ VT rate limit: waiting ${Math.ceil(waitMs / 1000)}s`);
44
- await new Promise(r => setTimeout(r, waitMs));
45
- this._requestCount = 0;
46
- this._windowStart = Date.now();
47
- }
48
- this._requestCount++;
49
- }
50
-
51
- // ── HTTP helpers ────────────────────────────────────────────
52
- async _get(path) {
53
- await this._throttle();
54
- if (this._httpGet) return this._httpGet(`${VT_API_BASE}${path}`);
55
-
56
- return new Promise((resolve, reject) => {
57
- const req = https.request({
58
- hostname: 'www.virustotal.com',
59
- path: `/api/v3${path}`,
60
- method: 'GET',
61
- headers: {
62
- 'x-apikey': this.apiKey,
63
- 'Accept': 'application/json',
64
- },
65
- }, (res) => {
66
- let data = '';
67
- res.on('data', chunk => { data += chunk; });
68
- res.on('end', () => {
69
- try {
70
- const parsed = JSON.parse(data);
71
- if (res.statusCode === 429) {
72
- reject(new Error('VT rate limit exceeded'));
73
- } else if (res.statusCode === 404) {
74
- resolve({ found: false, data: null });
75
- } else if (res.statusCode >= 200 && res.statusCode < 300) {
76
- resolve({ found: true, data: parsed });
77
- } else {
78
- reject(new Error(`VT API error ${res.statusCode}: ${JSON.stringify(parsed).substring(0, 200)}`));
79
- }
80
- } catch (e) {
81
- reject(new Error(`VT response parse error: ${e.message}`));
82
- }
83
- });
84
- });
85
- req.on('error', reject);
86
- req.setTimeout(this.timeout, () => { req.destroy(); reject(new Error('VT API timeout')); });
87
- req.end();
88
- });
89
- }
90
-
91
- // ── File Hash Lookup ───────────────────────────────────────
92
- async lookupHash(hash) {
93
- if (!hash || hash.length < 32) throw new Error('Invalid hash: provide MD5, SHA1, or SHA256');
94
- const result = await this._get(`/files/${hash}`);
95
-
96
- if (!result.found) {
97
- return { found: false, hash, malicious: 0, suspicious: 0, harmless: 0, undetected: 0, engines: {} };
98
- }
99
-
100
- const attrs = result.data.data?.attributes || {};
101
- const stats = attrs.last_analysis_stats || {};
102
- const results = attrs.last_analysis_results || {};
103
-
104
- // Extract detected engines
105
- const detectedEngines = {};
106
- for (const [engine, info] of Object.entries(results)) {
107
- if (info.category === 'malicious' || info.category === 'suspicious') {
108
- detectedEngines[engine] = { category: info.category, result: info.result };
109
- }
110
- }
111
-
112
- return {
113
- found: true,
114
- hash,
115
- malicious: stats.malicious || 0,
116
- suspicious: stats.suspicious || 0,
117
- harmless: stats.harmless || 0,
118
- undetected: stats.undetected || 0,
119
- engines: detectedEngines,
120
- reputation: attrs.reputation || 0,
121
- tags: attrs.tags || [],
122
- };
123
- }
124
-
125
- // ── URL Scan ───────────────────────────────────────────────
126
- async scanURL(url) {
127
- if (!url) throw new Error('URL is required');
128
- // URL ID = base64url of the URL
129
- const urlId = Buffer.from(url).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
130
- const result = await this._get(`/urls/${urlId}`);
131
-
132
- if (!result.found) {
133
- return { found: false, url, malicious: 0, suspicious: 0, harmless: 0 };
134
- }
135
-
136
- const attrs = result.data.data?.attributes || {};
137
- const stats = attrs.last_analysis_stats || {};
138
-
139
- return {
140
- found: true,
141
- url,
142
- malicious: stats.malicious || 0,
143
- suspicious: stats.suspicious || 0,
144
- harmless: stats.harmless || 0,
145
- categories: attrs.categories || {},
146
- };
147
- }
148
-
149
- // ── Domain Report ──────────────────────────────────────────
150
- async checkDomain(domain) {
151
- if (!domain) throw new Error('Domain is required');
152
- const result = await this._get(`/domains/${domain}`);
153
-
154
- if (!result.found) {
155
- return { found: false, domain, reputation: 0, malicious: 0 };
156
- }
157
-
158
- const attrs = result.data.data?.attributes || {};
159
- const stats = attrs.last_analysis_stats || {};
160
-
161
- return {
162
- found: true,
163
- domain,
164
- reputation: attrs.reputation || 0,
165
- malicious: stats.malicious || 0,
166
- suspicious: stats.suspicious || 0,
167
- categories: attrs.categories || {},
168
- registrar: attrs.registrar || 'unknown',
169
- };
170
- }
171
-
172
- // ── IP Report ──────────────────────────────────────────────
173
- async checkIP(ip) {
174
- if (!ip) throw new Error('IP address is required');
175
- const result = await this._get(`/ip_addresses/${ip}`);
176
-
177
- if (!result.found) {
178
- return { found: false, ip, reputation: 0, malicious: 0 };
179
- }
180
-
181
- const attrs = result.data.data?.attributes || {};
182
- const stats = attrs.last_analysis_stats || {};
183
-
184
- return {
185
- found: true,
186
- ip,
187
- reputation: attrs.reputation || 0,
188
- malicious: stats.malicious || 0,
189
- suspicious: stats.suspicious || 0,
190
- country: attrs.country || 'unknown',
191
- as_owner: attrs.as_owner || 'unknown',
192
- };
193
- }
194
-
195
- // ── File SHA256 helper ──────────────────────────────────────
196
- static hashFile(filePath) {
197
- const content = fs.readFileSync(filePath);
198
- return crypto.createHash('sha256').update(content).digest('hex');
199
- }
200
- }
201
-
202
- module.exports = { VTClient, VT_API_BASE, VT_RATE_LIMIT };