@guava-parity/guard-scanner 13.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 (96) hide show
  1. package/README.md +170 -215
  2. package/README_ja.md +252 -0
  3. package/SECURITY.md +12 -4
  4. package/SKILL.md +148 -57
  5. package/dist/cli.cjs +5997 -0
  6. package/dist/cli.d.mts +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.mjs +6003 -0
  9. package/dist/index.cjs +4825 -0
  10. package/dist/index.d.mts +17 -0
  11. package/dist/index.d.ts +17 -0
  12. package/dist/index.mjs +4798 -0
  13. package/dist/mcp-server.cjs +4756 -0
  14. package/dist/mcp-server.d.mts +1 -0
  15. package/dist/mcp-server.d.ts +1 -0
  16. package/dist/mcp-server.mjs +4767 -0
  17. package/dist/openclaw-plugin.cjs +4863 -0
  18. package/dist/openclaw-plugin.d.mts +11 -0
  19. package/dist/openclaw-plugin.d.ts +11 -0
  20. package/dist/openclaw-plugin.mjs +4854 -0
  21. package/dist/types.cjs +18 -0
  22. package/dist/types.d.mts +215 -0
  23. package/dist/types.d.ts +215 -0
  24. package/dist/types.mjs +1 -0
  25. package/docs/EVIDENCE_DRIVEN.md +182 -0
  26. package/docs/banner.png +0 -0
  27. package/docs/data/benchmark-ledger.json +1428 -0
  28. package/docs/data/corpus-metrics.json +11 -0
  29. package/docs/data/fp-ledger.json +18 -0
  30. package/docs/data/latest.json +25837 -2481
  31. package/docs/data/quality-contract.json +36 -0
  32. package/docs/generated/npm-audit-20260312.json +96 -0
  33. package/docs/generated/openclaw-upstream-status.json +25 -0
  34. package/docs/glossary.md +46 -0
  35. package/docs/index.html +1085 -496
  36. package/docs/logo.png +0 -0
  37. package/docs/openclaw-compatibility-audit.md +45 -0
  38. package/docs/openclaw-continuous-compatibility-plan.md +37 -0
  39. package/docs/rules/a2a-contagion.md +68 -0
  40. package/docs/rules/advanced-exfil.md +52 -0
  41. package/docs/rules/agent-protocol.md +108 -0
  42. package/docs/rules/api-abuse.md +68 -0
  43. package/docs/rules/autonomous-risk.md +92 -0
  44. package/docs/rules/config-impact.md +132 -0
  45. package/docs/rules/credential-handling.md +100 -0
  46. package/docs/rules/cve-patterns.md +332 -0
  47. package/docs/rules/data-exposure.md +84 -0
  48. package/docs/rules/exfiltration.md +36 -0
  49. package/docs/rules/financial-access.md +84 -0
  50. package/docs/rules/identity-hijack.md +140 -0
  51. package/docs/rules/inference-manipulation.md +60 -0
  52. package/docs/rules/leaky-skills.md +52 -0
  53. package/docs/rules/malicious-code.md +108 -0
  54. package/docs/rules/mcp-security.md +148 -0
  55. package/docs/rules/memory-poisoning.md +84 -0
  56. package/docs/rules/model-poisoning.md +44 -0
  57. package/docs/rules/obfuscation.md +60 -0
  58. package/docs/rules/persistence.md +108 -0
  59. package/docs/rules/pii-exposure.md +116 -0
  60. package/docs/rules/prompt-injection.md +148 -0
  61. package/docs/rules/prompt-worm.md +44 -0
  62. package/docs/rules/safeguard-bypass.md +44 -0
  63. package/docs/rules/sandbox-escape.md +100 -0
  64. package/docs/rules/secret-detection.md +44 -0
  65. package/docs/rules/supply-chain-v2.md +92 -0
  66. package/docs/rules/suspicious-download.md +60 -0
  67. package/docs/rules/trust-boundary.md +76 -0
  68. package/docs/rules/trust-exploitation.md +92 -0
  69. package/docs/rules/unverifiable-deps.md +84 -0
  70. package/docs/rules/vdb-injection.md +84 -0
  71. package/docs/security-vulnerability-report-20260312.md +53 -0
  72. package/docs/spec/PRD_V2_ARCHITECTURE.md +55 -0
  73. package/docs/spec/capabilities.json +174 -0
  74. package/docs/spec/finding.schema.json +104 -0
  75. package/docs/spec/integration-manifest.md +39 -0
  76. package/docs/spec/plugin-trust.json +11 -0
  77. package/docs/spec/sbom.json +33 -0
  78. package/docs/threat-model.md +65 -0
  79. package/docs/v13-architecture-manifest.md +55 -0
  80. package/hooks/context.ts +306 -0
  81. package/hooks/guard-scanner/plugin.ts +24 -1
  82. package/openclaw-plugin.mts +107 -0
  83. package/openclaw.plugin.json +30 -53
  84. package/package.json +66 -13
  85. package/src/asset-auditor.js +0 -508
  86. package/src/ci-reporter.js +0 -135
  87. package/src/cli.js +0 -294
  88. package/src/html-template.js +0 -239
  89. package/src/ioc-db.js +0 -54
  90. package/src/mcp-server.js +0 -702
  91. package/src/patterns.js +0 -611
  92. package/src/quarantine.js +0 -41
  93. package/src/runtime-guard.js +0 -346
  94. package/src/scanner.js +0 -1157
  95. package/src/vt-client.js +0 -202
  96. package/src/watcher.js +0 -170
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 };
package/src/watcher.js DELETED
@@ -1,170 +0,0 @@
1
- /**
2
- * guard-scanner v8 — Real-time File Watcher
3
- *
4
- * @security-manifest
5
- * env-read: []
6
- * env-write: []
7
- * network: none
8
- * fs-read: [watched directory]
9
- * fs-write: []
10
- * exec: none
11
- * purpose: Real-time file system monitoring for security threats
12
- */
13
-
14
- const fs = require('fs');
15
- const path = require('path');
16
- const { GuardScanner } = require('./scanner.js');
17
- const EventEmitter = require('events');
18
-
19
- class GuardWatcher extends EventEmitter {
20
- constructor(options = {}) {
21
- super();
22
- this.verbose = options.verbose || false;
23
- this.strict = options.strict || false;
24
- this.soulLock = options.soulLock || false;
25
- this.debounceMs = options.debounceMs || 300;
26
- this.quiet = options.quiet || false;
27
- this._watchers = [];
28
- this._debounceTimers = new Map();
29
- this._running = false;
30
- this._scanCount = 0;
31
- this._alertCount = 0;
32
- }
33
-
34
- watch(directory) {
35
- if (!directory) throw new Error('Watch directory is required');
36
- if (!fs.existsSync(directory)) throw new Error(`Directory not found: ${directory}`);
37
- if (!fs.statSync(directory).isDirectory()) throw new Error(`Not a directory: ${directory}`);
38
-
39
- this._running = true;
40
-
41
- if (!this.quiet) {
42
- console.log(`\nšŸ›”ļø guard-scanner watch mode`);
43
- console.log(`šŸ‘ļø Watching: ${path.resolve(directory)}`);
44
- console.log(`⚔ Strict: ${this.strict ? 'ON' : 'OFF'}`);
45
- console.log(`šŸ”’ Soul Lock: ${this.soulLock ? 'ON' : 'OFF'}`);
46
- console.log('───────────────────────────────');
47
- console.log('Press Ctrl+C to stop\n');
48
- }
49
-
50
- try {
51
- const watcher = fs.watch(directory, { recursive: true }, (eventType, filename) => {
52
- if (!filename || !this._running) return;
53
- const fullPath = path.join(directory, filename);
54
-
55
- // Debounce
56
- if (this._debounceTimers.has(fullPath)) {
57
- clearTimeout(this._debounceTimers.get(fullPath));
58
- }
59
- this._debounceTimers.set(fullPath, setTimeout(() => {
60
- this._debounceTimers.delete(fullPath);
61
- this._onFileChange(directory, filename, eventType);
62
- }, this.debounceMs));
63
- });
64
-
65
- this._watchers.push(watcher);
66
- this.emit('watching', { directory: path.resolve(directory) });
67
- } catch (e) {
68
- this.emit('error', e);
69
- }
70
-
71
- return this;
72
- }
73
-
74
- _onFileChange(baseDir, filename, eventType) {
75
- if (!this._running) return;
76
-
77
- // Skip hidden files, node_modules, .git
78
- if (filename.startsWith('.') || filename.includes('node_modules') || filename.includes('.git')) {
79
- return;
80
- }
81
-
82
- // Skip non-code files
83
- const ext = path.extname(filename).toLowerCase();
84
- const codeExts = ['.js', '.ts', '.py', '.sh', '.md', '.json', '.yaml', '.yml', '.toml'];
85
- if (!codeExts.includes(ext)) return;
86
-
87
- const fullPath = path.join(baseDir, filename);
88
- if (!fs.existsSync(fullPath)) {
89
- this.emit('deleted', { file: filename });
90
- return;
91
- }
92
-
93
- this._scanCount++;
94
- const scanner = new GuardScanner({
95
- summaryOnly: true,
96
- strict: this.strict,
97
- soulLock: this.soulLock,
98
- quiet: true,
99
- });
100
-
101
- // Find the skill directory (parent directory of the changed file)
102
- const skillDir = path.dirname(fullPath);
103
- const skillName = path.basename(skillDir);
104
-
105
- try {
106
- scanner.scanSkill(skillDir, skillName);
107
- const findings = scanner.findings[0];
108
-
109
- if (findings && findings.findings.length > 0) {
110
- this._alertCount += findings.findings.length;
111
- const result = {
112
- file: filename,
113
- skill: skillName,
114
- eventType,
115
- verdict: findings.verdict,
116
- risk: findings.risk,
117
- findingCount: findings.findings.length,
118
- findings: findings.findings,
119
- timestamp: new Date().toISOString(),
120
- };
121
-
122
- this.emit('alert', result);
123
-
124
- if (!this.quiet) {
125
- const icon = findings.verdict === 'MALICIOUS' ? '🚨' :
126
- findings.verdict === 'SUSPICIOUS' ? 'āš ļø' : 'šŸ”¶';
127
- console.log(`${icon} [${new Date().toLocaleTimeString()}] ${findings.verdict} — ${filename}`);
128
- console.log(` Risk: ${findings.risk} | Findings: ${findings.findings.length}`);
129
- if (this.verbose) {
130
- for (const f of findings.findings.slice(0, 5)) {
131
- console.log(` → [${f.severity}] ${f.id}: ${f.desc}`);
132
- }
133
- }
134
- }
135
- } else {
136
- this.emit('clean', { file: filename, skill: skillName });
137
- if (this.verbose && !this.quiet) {
138
- console.log(`āœ… [${new Date().toLocaleTimeString()}] CLEAN — ${filename}`);
139
- }
140
- }
141
- } catch (e) {
142
- this.emit('error', { file: filename, error: e.message });
143
- }
144
- }
145
-
146
- stop() {
147
- this._running = false;
148
- for (const w of this._watchers) {
149
- try { w.close(); } catch (e) { /* ignore */ }
150
- }
151
- this._watchers = [];
152
- for (const t of this._debounceTimers.values()) {
153
- clearTimeout(t);
154
- }
155
- this._debounceTimers.clear();
156
- this.emit('stopped', { scanCount: this._scanCount, alertCount: this._alertCount });
157
- return { scanCount: this._scanCount, alertCount: this._alertCount };
158
- }
159
-
160
- getStats() {
161
- return {
162
- running: this._running,
163
- scanCount: this._scanCount,
164
- alertCount: this._alertCount,
165
- watcherCount: this._watchers.length,
166
- };
167
- }
168
- }
169
-
170
- module.exports = { GuardWatcher };