@guava-parity/guard-scanner 9.1.0 → 15.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 (79) hide show
  1. package/README.md +42 -253
  2. package/SECURITY.md +12 -4
  3. package/SKILL.md +121 -59
  4. package/dist/openclaw-plugin.mjs +41 -0
  5. package/docs/EVIDENCE_DRIVEN.md +182 -0
  6. package/docs/banner.png +0 -0
  7. package/docs/data/corpus-metrics.json +11 -0
  8. package/docs/data/latest.json +29845 -0
  9. package/docs/generated/npm-audit-20260312.json +96 -0
  10. package/docs/generated/openclaw-upstream-status.json +25 -0
  11. package/docs/glossary.md +46 -0
  12. package/docs/index.html +1119 -0
  13. package/docs/logo.png +0 -0
  14. package/docs/openclaw-compatibility-audit.md +44 -0
  15. package/docs/openclaw-continuous-compatibility-plan.md +36 -0
  16. package/docs/rules/a2a-contagion.md +68 -0
  17. package/docs/rules/advanced-exfil.md +52 -0
  18. package/docs/rules/agent-protocol.md +108 -0
  19. package/docs/rules/api-abuse.md +68 -0
  20. package/docs/rules/autonomous-risk.md +92 -0
  21. package/docs/rules/config-impact.md +132 -0
  22. package/docs/rules/credential-handling.md +100 -0
  23. package/docs/rules/cve-patterns.md +332 -0
  24. package/docs/rules/data-exposure.md +84 -0
  25. package/docs/rules/exfiltration.md +36 -0
  26. package/docs/rules/financial-access.md +84 -0
  27. package/docs/rules/identity-hijack.md +140 -0
  28. package/docs/rules/inference-manipulation.md +60 -0
  29. package/docs/rules/leaky-skills.md +52 -0
  30. package/docs/rules/malicious-code.md +108 -0
  31. package/docs/rules/mcp-security.md +148 -0
  32. package/docs/rules/memory-poisoning.md +84 -0
  33. package/docs/rules/model-poisoning.md +44 -0
  34. package/docs/rules/obfuscation.md +60 -0
  35. package/docs/rules/persistence.md +108 -0
  36. package/docs/rules/pii-exposure.md +116 -0
  37. package/docs/rules/prompt-injection.md +148 -0
  38. package/docs/rules/prompt-worm.md +44 -0
  39. package/docs/rules/safeguard-bypass.md +44 -0
  40. package/docs/rules/sandbox-escape.md +100 -0
  41. package/docs/rules/secret-detection.md +44 -0
  42. package/docs/rules/supply-chain-v2.md +92 -0
  43. package/docs/rules/suspicious-download.md +60 -0
  44. package/docs/rules/trust-boundary.md +76 -0
  45. package/docs/rules/trust-exploitation.md +92 -0
  46. package/docs/rules/unverifiable-deps.md +84 -0
  47. package/docs/rules/vdb-injection.md +84 -0
  48. package/docs/security-vulnerability-report-20260312.md +53 -0
  49. package/docs/spec/PRD_V2_ARCHITECTURE.md +55 -0
  50. package/docs/spec/capabilities.json +42 -0
  51. package/docs/spec/finding.schema.json +104 -0
  52. package/docs/spec/integration-manifest.md +39 -0
  53. package/docs/spec/sbom.json +33 -0
  54. package/docs/threat-model.md +65 -0
  55. package/docs/v13-architecture-manifest.md +55 -0
  56. package/hooks/context.js +305 -0
  57. package/hooks/guard-scanner/plugin.ts +24 -1
  58. package/openclaw-plugin.mts +91 -0
  59. package/openclaw.plugin.json +30 -53
  60. package/package.json +80 -57
  61. package/src/cli.js +174 -34
  62. package/src/core/content-loader.js +42 -0
  63. package/src/core/inventory.js +73 -0
  64. package/src/core/report-adapters.js +171 -0
  65. package/src/core/risk-engine.js +93 -0
  66. package/src/core/rule-registry.js +73 -0
  67. package/src/core/semantic-validators.js +85 -0
  68. package/src/finding-schema.js +191 -0
  69. package/src/hooks/context.ts +49 -0
  70. package/src/html-template.js +2 -2
  71. package/src/mcp-server.js +192 -5
  72. package/src/openclaw-upstream.js +128 -0
  73. package/src/patterns.js +519 -157
  74. package/src/policy-engine.js +32 -0
  75. package/src/runtime-guard.js +40 -2
  76. package/src/scanner.js +228 -231
  77. package/src/skill-crawler.js +254 -0
  78. package/src/threat-model.js +50 -0
  79. package/src/validation-layer.js +39 -0
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * guard-scanner — HTML Report Template
3
3
  * Dark Glassmorphism + Conic-gradient Risk Gauges
4
- * Zero dependencies. Pure CSS animations.
4
+ * Lightweight HTML report template. Pure CSS animations.
5
5
  */
6
6
 
7
7
  'use strict';
@@ -229,7 +229,7 @@ ${total > 0 ? `<div class="ag">
229
229
  </div>
230
230
 
231
231
  <div class="ft">
232
- guard-scanner v${version} &mdash; Zero dependencies. Zero compromises. 🛡️<br>
232
+ guard-scanner v${version} &mdash; Lightweight runtime footprint (1 dependency: ws). 🛡️<br>
233
233
  Built by <a href="https://github.com/koatora20">Guava 🍈 &amp; Dee</a>
234
234
  </div>
235
235
  </div>
package/src/mcp-server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * guard-scanner MCP Server — Zero-dependency stdio JSON-RPC 2.0
3
+ * guard-scanner MCP Server — Lightweight stdio JSON-RPC 2.0
4
4
  *
5
5
  * @security-manifest
6
6
  * env-read: [VT_API_KEY (optional, for audit vt-scan)]
@@ -15,7 +15,7 @@
15
15
  * Implements: initialize, tools/list, tools/call, notifications
16
16
  *
17
17
  * Tools:
18
- * scan_skill — Scan a directory for security threats (166 patterns)
18
+ * scan_skill — Scan a directory for security threats
19
19
  * scan_text — Scan a code/text snippet inline
20
20
  * check_tool_call — Runtime check before a tool call (26 checks, 5 layers)
21
21
  * audit_assets — Audit npm/GitHub/ClawHub assets for exposure
@@ -27,6 +27,10 @@
27
27
 
28
28
  const { GuardScanner, VERSION, scanToolCall, getCheckStats, LAYER_NAMES } = require('./scanner.js');
29
29
  const { AssetAuditor, AUDIT_VERSION } = require('./asset-auditor.js');
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+ const os = require('os');
33
+ const CAPABILITIES = require('../docs/spec/capabilities.json');
30
34
 
31
35
  // ── MCP Protocol Constants ──
32
36
 
@@ -42,12 +46,85 @@ const SERVER_CAPABILITIES = {
42
46
  tools: {},
43
47
  };
44
48
 
49
+ const STATIC_SUMMARY = `${CAPABILITIES.static_pattern_count} threat patterns across ${CAPABILITIES.threat_category_count} categories`;
50
+
51
+ // ── Async Task Store (run_async / status / result / cancel) ──
52
+
53
+ const TASK_DIR = process.env.GUARD_SCANNER_TASK_DIR || path.join(os.homedir(), '.openclaw', 'guard-scanner', 'tasks');
54
+ const TASK_FILE = path.join(TASK_DIR, 'tasks.json');
55
+
56
+ function ensureTaskStore() {
57
+ fs.mkdirSync(TASK_DIR, { recursive: true });
58
+ if (!fs.existsSync(TASK_FILE)) fs.writeFileSync(TASK_FILE, '{}');
59
+ }
60
+
61
+ function loadTasks() {
62
+ ensureTaskStore();
63
+ try {
64
+ return JSON.parse(fs.readFileSync(TASK_FILE, 'utf8') || '{}');
65
+ } catch {
66
+ return {};
67
+ }
68
+ }
69
+
70
+ function saveTasks(tasks) {
71
+ ensureTaskStore();
72
+ fs.writeFileSync(TASK_FILE, JSON.stringify(tasks, null, 2));
73
+ }
74
+
75
+ function makeTaskId() {
76
+ return `task_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
77
+ }
78
+
79
+ function nowIso() {
80
+ return new Date().toISOString();
81
+ }
82
+
83
+ function runTaskAsync(taskId, toolName, toolArgs) {
84
+ setImmediate(async () => {
85
+ const tasks = loadTasks();
86
+ const t = tasks[taskId];
87
+ if (!t || t.state === 'cancelled') return;
88
+
89
+ t.state = 'running';
90
+ t.startedAt = nowIso();
91
+ t.updatedAt = nowIso();
92
+ saveTasks(tasks);
93
+
94
+ try {
95
+ let result;
96
+ if (toolName === 'scan_skill') result = handleScanSkill(toolArgs);
97
+ else if (toolName === 'scan_text') result = handleScanText(toolArgs);
98
+ else if (toolName === 'check_tool_call') result = handleCheckToolCall(toolArgs);
99
+ else if (toolName === 'audit_assets') result = await handleAuditAssets(toolArgs);
100
+ else if (toolName === 'get_stats') result = handleGetStats(toolArgs);
101
+ else throw new Error(`Unsupported async tool: ${toolName}`);
102
+
103
+ const latest = loadTasks();
104
+ if (!latest[taskId] || latest[taskId].state === 'cancelled') return;
105
+ latest[taskId].state = result?.isError ? 'failed' : 'succeeded';
106
+ latest[taskId].updatedAt = nowIso();
107
+ latest[taskId].finishedAt = nowIso();
108
+ latest[taskId].result = result;
109
+ saveTasks(latest);
110
+ } catch (e) {
111
+ const latest = loadTasks();
112
+ if (!latest[taskId]) return;
113
+ latest[taskId].state = 'failed';
114
+ latest[taskId].updatedAt = nowIso();
115
+ latest[taskId].finishedAt = nowIso();
116
+ latest[taskId].error = e.message;
117
+ saveTasks(latest);
118
+ }
119
+ });
120
+ }
121
+
45
122
  // ── Tool Definitions ──
46
123
 
47
124
  const TOOLS = [
48
125
  {
49
126
  name: 'scan_skill',
50
- description: 'Scan a directory for agent security threats. Detects prompt injection, data exfiltration, credential theft, reverse shells, and 166+ threat patterns across 23 categories. Returns risk score, verdict, and detailed findings.',
127
+ description: `Scan a directory for agent security threats. Detects prompt injection, data exfiltration, credential theft, reverse shells, and ${STATIC_SUMMARY}. Returns risk score, verdict, and detailed findings.`,
51
128
  inputSchema: {
52
129
  type: 'object',
53
130
  properties: {
@@ -141,8 +218,49 @@ const TOOLS = [
141
218
  properties: {},
142
219
  },
143
220
  },
221
+ {
222
+ name: 'experimental.run_async',
223
+ description: '[Experimental] Run a supported guard-scanner tool asynchronously. Returns taskId immediately; use experimental.task_status/experimental.task_result to retrieve output.',
224
+ inputSchema: {
225
+ type: 'object',
226
+ properties: {
227
+ tool: { type: 'string', enum: ['scan_skill', 'scan_text', 'check_tool_call', 'audit_assets', 'get_stats'] },
228
+ args: { type: 'object', additionalProperties: true, default: {} },
229
+ },
230
+ required: ['tool'],
231
+ },
232
+ },
233
+ {
234
+ name: 'experimental.task_status',
235
+ description: '[Experimental] Get async task status by taskId.',
236
+ inputSchema: {
237
+ type: 'object',
238
+ properties: { taskId: { type: 'string' } },
239
+ required: ['taskId'],
240
+ },
241
+ },
242
+ {
243
+ name: 'experimental.task_result',
244
+ description: '[Experimental] Get async task final result by taskId.',
245
+ inputSchema: {
246
+ type: 'object',
247
+ properties: { taskId: { type: 'string' } },
248
+ required: ['taskId'],
249
+ },
250
+ },
251
+ {
252
+ name: 'experimental.task_cancel',
253
+ description: '[Experimental] Cancel async task by taskId (best-effort).',
254
+ inputSchema: {
255
+ type: 'object',
256
+ properties: { taskId: { type: 'string' }, reason: { type: 'string' } },
257
+ required: ['taskId'],
258
+ },
259
+ },
144
260
  ];
145
261
 
262
+ // NOTE: cron_glm5_config was removed in v14.0.0 — not guard-scanner's responsibility
263
+
146
264
  // ── Tool Handlers ──
147
265
 
148
266
  function handleScanSkill({ path: scanPath, verbose = false, strict = false }) {
@@ -227,10 +345,11 @@ function handleCheckToolCall({ tool, args, mode = 'enforce' }) {
227
345
  if (args === undefined) return errorResult('args is required');
228
346
 
229
347
  const result = scanToolCall(tool, args, { mode, auditLog: true });
348
+ const runtimeCheckCount = getCheckStats().total;
230
349
 
231
350
  if (result.detections.length === 0) {
232
351
  return successResult(
233
- `✅ Tool call "${tool}" passed all 26 runtime checks.\nMode: ${mode}`
352
+ `✅ Tool call "${tool}" passed all ${runtimeCheckCount} runtime checks.\nMode: ${mode}`
234
353
  );
235
354
  }
236
355
 
@@ -286,7 +405,7 @@ function handleGetStats() {
286
405
  return successResult(
287
406
  `🛡️ guard-scanner v${VERSION}\n\n` +
288
407
  `Static Analysis:\n` +
289
- ` • 166 threat patterns across 23 categories\n` +
408
+ ` • ${STATIC_SUMMARY}\n` +
290
409
  ` • Entropy-based secret detection\n` +
291
410
  ` • Data flow analysis (JS)\n` +
292
411
  ` • Cross-file reference checking\n` +
@@ -307,6 +426,66 @@ function handleGetStats() {
307
426
  );
308
427
  }
309
428
 
429
+ function handleRunAsync({ tool, args = {} }) {
430
+ if (!tool) return errorResult('tool is required');
431
+ const supported = new Set(['scan_skill', 'scan_text', 'check_tool_call', 'audit_assets', 'get_stats']);
432
+ if (!supported.has(tool)) return errorResult(`Unsupported async tool: ${tool}`);
433
+
434
+ const taskId = makeTaskId();
435
+ const tasks = loadTasks();
436
+ tasks[taskId] = {
437
+ taskId,
438
+ tool,
439
+ args,
440
+ state: 'queued',
441
+ createdAt: nowIso(),
442
+ updatedAt: nowIso(),
443
+ };
444
+ saveTasks(tasks);
445
+ runTaskAsync(taskId, tool, args);
446
+
447
+ return successResult(`accepted\ntaskId=${taskId}\nstate=queued\npollAfterMs=800`);
448
+ }
449
+
450
+ function handleTaskStatus({ taskId }) {
451
+ if (!taskId) return errorResult('taskId is required');
452
+ const tasks = loadTasks();
453
+ const t = tasks[taskId];
454
+ if (!t) return errorResult(`Task not found: ${taskId}`);
455
+ return successResult(`taskId=${taskId}\nstate=${t.state}\nupdatedAt=${t.updatedAt}`);
456
+ }
457
+
458
+ function handleTaskResult({ taskId }) {
459
+ if (!taskId) return errorResult('taskId is required');
460
+ const tasks = loadTasks();
461
+ const t = tasks[taskId];
462
+ if (!t) return errorResult(`Task not found: ${taskId}`);
463
+ if (t.state === 'queued' || t.state === 'running') {
464
+ return successResult(`taskId=${taskId}\nstate=${t.state}\nmessage=not ready`);
465
+ }
466
+ if (t.error) return errorResult(`taskId=${taskId}\nstate=${t.state}\nerror=${t.error}`);
467
+ if (!t.result) return errorResult(`taskId=${taskId}\nstate=${t.state}\nerror=no result`);
468
+ return t.result;
469
+ }
470
+
471
+ function handleTaskCancel({ taskId, reason = 'user cancel' }) {
472
+ if (!taskId) return errorResult('taskId is required');
473
+ const tasks = loadTasks();
474
+ const t = tasks[taskId];
475
+ if (!t) return errorResult(`Task not found: ${taskId}`);
476
+ if (t.state === 'succeeded' || t.state === 'failed' || t.state === 'cancelled') {
477
+ return successResult(`taskId=${taskId}\nstate=${t.state}\nmessage=already terminal`);
478
+ }
479
+ t.state = 'cancelled';
480
+ t.updatedAt = nowIso();
481
+ t.finishedAt = nowIso();
482
+ t.cancelReason = reason;
483
+ saveTasks(tasks);
484
+ return successResult(`taskId=${taskId}\nstate=cancelled`);
485
+ }
486
+
487
+ // handleCronGlm5Config removed in v14.0.0 — not guard-scanner's responsibility
488
+
310
489
  // ── Result helpers ──
311
490
 
312
491
  function successResult(text) {
@@ -444,6 +623,14 @@ class MCPServer {
444
623
  return await handleAuditAssets(args);
445
624
  case 'get_stats':
446
625
  return handleGetStats();
626
+ case 'experimental.run_async':
627
+ return handleRunAsync(args);
628
+ case 'experimental.task_status':
629
+ return handleTaskStatus(args);
630
+ case 'experimental.task_result':
631
+ return handleTaskResult(args);
632
+ case 'experimental.task_cancel':
633
+ return handleTaskCancel(args);
447
634
  default:
448
635
  return errorResult(`Unknown tool: ${name}`);
449
636
  }
@@ -0,0 +1,128 @@
1
+ const https = require('node:https');
2
+
3
+ function parseVersion(version) {
4
+ const [stable, prerelease = ''] = String(version).split('-', 2);
5
+ const parts = stable.split('.').map((value) => Number.parseInt(value, 10));
6
+ while (parts.length < 3) parts.push(0);
7
+
8
+ return {
9
+ raw: version,
10
+ parts,
11
+ prerelease,
12
+ };
13
+ }
14
+
15
+ function compareOpenClawVersions(left, right) {
16
+ const a = parseVersion(left);
17
+ const b = parseVersion(right);
18
+
19
+ for (let index = 0; index < 3; index += 1) {
20
+ if (a.parts[index] > b.parts[index]) return 1;
21
+ if (a.parts[index] < b.parts[index]) return -1;
22
+ }
23
+
24
+ if (!a.prerelease && b.prerelease) return 1;
25
+ if (a.prerelease && !b.prerelease) return -1;
26
+ if (a.prerelease > b.prerelease) return 1;
27
+ if (a.prerelease < b.prerelease) return -1;
28
+ return 0;
29
+ }
30
+
31
+ function evaluateOpenClawBaseline({ pinnedVersion, latestVersion, latestPublishedAt, source }) {
32
+ const comparison = compareOpenClawVersions(pinnedVersion, latestVersion);
33
+
34
+ return {
35
+ pinnedVersion,
36
+ latestVersion,
37
+ latestPublishedAt,
38
+ source,
39
+ upToDate: comparison === 0,
40
+ ahead: comparison > 0,
41
+ behind: comparison < 0,
42
+ };
43
+ }
44
+
45
+ function normalizeGitHubReleaseVersion(tagName) {
46
+ return String(tagName).replace(/^v/i, '');
47
+ }
48
+
49
+ function evaluateOpenClawSourceParity({ npmLatestVersion, githubLatestVersion }) {
50
+ const normalizedGitHubVersion = normalizeGitHubReleaseVersion(githubLatestVersion);
51
+ return {
52
+ npmLatestVersion,
53
+ githubLatestVersion: normalizedGitHubVersion,
54
+ inParity: compareOpenClawVersions(npmLatestVersion, normalizedGitHubVersion) === 0,
55
+ };
56
+ }
57
+
58
+ function httpGetJson(url) {
59
+ return new Promise((resolve, reject) => {
60
+ https
61
+ .get(
62
+ url,
63
+ {
64
+ headers: {
65
+ 'user-agent': 'guard-scanner-openclaw-upstream-check',
66
+ accept: 'application/json',
67
+ },
68
+ },
69
+ (response) => {
70
+ let body = '';
71
+ response.setEncoding('utf8');
72
+ response.on('data', (chunk) => {
73
+ body += chunk;
74
+ });
75
+ response.on('end', () => {
76
+ if (response.statusCode && response.statusCode >= 400) {
77
+ reject(new Error(`GET ${url} failed with status ${response.statusCode}`));
78
+ return;
79
+ }
80
+ try {
81
+ resolve(JSON.parse(body));
82
+ } catch (error) {
83
+ reject(error);
84
+ }
85
+ });
86
+ },
87
+ )
88
+ .on('error', reject);
89
+ });
90
+ }
91
+
92
+ async function fetchLatestOpenClawRelease(fetchJson = httpGetJson) {
93
+ const npmMeta = await fetchJson('https://registry.npmjs.org/openclaw');
94
+ const latestVersion = npmMeta['dist-tags']?.latest;
95
+ if (!latestVersion) {
96
+ throw new Error('npm registry metadata missing dist-tags.latest for openclaw');
97
+ }
98
+
99
+ const githubRelease = await fetchJson('https://api.github.com/repos/openclaw/openclaw/releases/latest');
100
+ const githubLatestVersion = normalizeGitHubReleaseVersion(githubRelease.tag_name || '');
101
+ if (!githubLatestVersion) {
102
+ throw new Error('GitHub releases/latest missing tag_name for openclaw/openclaw');
103
+ }
104
+
105
+ const parity = evaluateOpenClawSourceParity({
106
+ npmLatestVersion: latestVersion,
107
+ githubLatestVersion,
108
+ });
109
+
110
+ return {
111
+ latestVersion,
112
+ latestPublishedAt: npmMeta.time?.[latestVersion] ?? null,
113
+ source: 'npm',
114
+ registryModifiedAt: npmMeta.time?.modified ?? null,
115
+ githubLatestVersion,
116
+ githubPublishedAt: githubRelease.published_at ?? null,
117
+ githubUrl: githubRelease.html_url ?? null,
118
+ sourceParity: parity,
119
+ };
120
+ }
121
+
122
+ module.exports = {
123
+ compareOpenClawVersions,
124
+ evaluateOpenClawBaseline,
125
+ evaluateOpenClawSourceParity,
126
+ fetchLatestOpenClawRelease,
127
+ normalizeGitHubReleaseVersion,
128
+ };