@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.
- package/README.md +170 -215
- package/README_ja.md +252 -0
- package/SECURITY.md +12 -4
- package/SKILL.md +148 -57
- package/dist/cli.cjs +5997 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +6003 -0
- package/dist/index.cjs +4825 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.mjs +4798 -0
- package/dist/mcp-server.cjs +4756 -0
- package/dist/mcp-server.d.mts +1 -0
- package/dist/mcp-server.d.ts +1 -0
- package/dist/mcp-server.mjs +4767 -0
- package/dist/openclaw-plugin.cjs +4863 -0
- package/dist/openclaw-plugin.d.mts +11 -0
- package/dist/openclaw-plugin.d.ts +11 -0
- package/dist/openclaw-plugin.mjs +4854 -0
- package/dist/types.cjs +18 -0
- package/dist/types.d.mts +215 -0
- package/dist/types.d.ts +215 -0
- package/dist/types.mjs +1 -0
- package/docs/EVIDENCE_DRIVEN.md +182 -0
- package/docs/banner.png +0 -0
- package/docs/data/benchmark-ledger.json +1428 -0
- package/docs/data/corpus-metrics.json +11 -0
- package/docs/data/fp-ledger.json +18 -0
- package/docs/data/latest.json +25837 -2481
- package/docs/data/quality-contract.json +36 -0
- package/docs/generated/npm-audit-20260312.json +96 -0
- package/docs/generated/openclaw-upstream-status.json +25 -0
- package/docs/glossary.md +46 -0
- package/docs/index.html +1085 -496
- package/docs/logo.png +0 -0
- package/docs/openclaw-compatibility-audit.md +45 -0
- package/docs/openclaw-continuous-compatibility-plan.md +37 -0
- package/docs/rules/a2a-contagion.md +68 -0
- package/docs/rules/advanced-exfil.md +52 -0
- package/docs/rules/agent-protocol.md +108 -0
- package/docs/rules/api-abuse.md +68 -0
- package/docs/rules/autonomous-risk.md +92 -0
- package/docs/rules/config-impact.md +132 -0
- package/docs/rules/credential-handling.md +100 -0
- package/docs/rules/cve-patterns.md +332 -0
- package/docs/rules/data-exposure.md +84 -0
- package/docs/rules/exfiltration.md +36 -0
- package/docs/rules/financial-access.md +84 -0
- package/docs/rules/identity-hijack.md +140 -0
- package/docs/rules/inference-manipulation.md +60 -0
- package/docs/rules/leaky-skills.md +52 -0
- package/docs/rules/malicious-code.md +108 -0
- package/docs/rules/mcp-security.md +148 -0
- package/docs/rules/memory-poisoning.md +84 -0
- package/docs/rules/model-poisoning.md +44 -0
- package/docs/rules/obfuscation.md +60 -0
- package/docs/rules/persistence.md +108 -0
- package/docs/rules/pii-exposure.md +116 -0
- package/docs/rules/prompt-injection.md +148 -0
- package/docs/rules/prompt-worm.md +44 -0
- package/docs/rules/safeguard-bypass.md +44 -0
- package/docs/rules/sandbox-escape.md +100 -0
- package/docs/rules/secret-detection.md +44 -0
- package/docs/rules/supply-chain-v2.md +92 -0
- package/docs/rules/suspicious-download.md +60 -0
- package/docs/rules/trust-boundary.md +76 -0
- package/docs/rules/trust-exploitation.md +92 -0
- package/docs/rules/unverifiable-deps.md +84 -0
- package/docs/rules/vdb-injection.md +84 -0
- package/docs/security-vulnerability-report-20260312.md +53 -0
- package/docs/spec/PRD_V2_ARCHITECTURE.md +55 -0
- package/docs/spec/capabilities.json +174 -0
- package/docs/spec/finding.schema.json +104 -0
- package/docs/spec/integration-manifest.md +39 -0
- package/docs/spec/plugin-trust.json +11 -0
- package/docs/spec/sbom.json +33 -0
- package/docs/threat-model.md +65 -0
- package/docs/v13-architecture-manifest.md +55 -0
- package/hooks/context.ts +306 -0
- package/hooks/guard-scanner/plugin.ts +24 -1
- package/openclaw-plugin.mts +107 -0
- package/openclaw.plugin.json +30 -53
- package/package.json +66 -13
- package/src/asset-auditor.js +0 -508
- package/src/ci-reporter.js +0 -135
- package/src/cli.js +0 -294
- package/src/html-template.js +0 -239
- package/src/ioc-db.js +0 -54
- package/src/mcp-server.js +0 -702
- package/src/patterns.js +0 -611
- package/src/quarantine.js +0 -41
- package/src/runtime-guard.js +0 -346
- package/src/scanner.js +0 -1157
- package/src/vt-client.js +0 -202
- package/src/watcher.js +0 -170
package/src/mcp-server.js
DELETED
|
@@ -1,702 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* guard-scanner MCP Server — Zero-dependency stdio JSON-RPC 2.0
|
|
4
|
-
*
|
|
5
|
-
* @security-manifest
|
|
6
|
-
* env-read: [VT_API_KEY (optional, for audit vt-scan)]
|
|
7
|
-
* env-write: []
|
|
8
|
-
* network: [npm registry, GitHub API, VirusTotal API — only for audit_assets]
|
|
9
|
-
* fs-read: [scan target directories, openclaw config]
|
|
10
|
-
* fs-write: [~/.openclaw/guard-scanner/audit.jsonl]
|
|
11
|
-
* exec: none
|
|
12
|
-
* purpose: MCP server exposing guard-scanner static/runtime analysis over stdio
|
|
13
|
-
*
|
|
14
|
-
* Protocol: MCP (Model Context Protocol) over stdio transport
|
|
15
|
-
* Implements: initialize, tools/list, tools/call, notifications
|
|
16
|
-
*
|
|
17
|
-
* Tools:
|
|
18
|
-
* scan_skill — Scan a directory for security threats (166 patterns)
|
|
19
|
-
* scan_text — Scan a code/text snippet inline
|
|
20
|
-
* check_tool_call — Runtime check before a tool call (26 checks, 5 layers)
|
|
21
|
-
* audit_assets — Audit npm/GitHub/ClawHub assets for exposure
|
|
22
|
-
* get_stats — Get scanner capabilities and statistics
|
|
23
|
-
*
|
|
24
|
-
* @author Guava 🍈 & Dee
|
|
25
|
-
* @license MIT
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
const { GuardScanner, VERSION, scanToolCall, getCheckStats, LAYER_NAMES } = require('./scanner.js');
|
|
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
|
-
|
|
34
|
-
// ── MCP Protocol Constants ──
|
|
35
|
-
|
|
36
|
-
const JSONRPC = '2.0';
|
|
37
|
-
const MCP_VERSION = '2025-11-05';
|
|
38
|
-
|
|
39
|
-
const SERVER_INFO = {
|
|
40
|
-
name: 'guard-scanner',
|
|
41
|
-
version: VERSION,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const SERVER_CAPABILITIES = {
|
|
45
|
-
tools: {},
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// ── Async Task Store (run_async / status / result / cancel) ──
|
|
49
|
-
|
|
50
|
-
const TASK_DIR = process.env.GUARD_SCANNER_TASK_DIR || path.join(os.homedir(), '.openclaw', 'guard-scanner', 'tasks');
|
|
51
|
-
const TASK_FILE = path.join(TASK_DIR, 'tasks.json');
|
|
52
|
-
|
|
53
|
-
function ensureTaskStore() {
|
|
54
|
-
fs.mkdirSync(TASK_DIR, { recursive: true });
|
|
55
|
-
if (!fs.existsSync(TASK_FILE)) fs.writeFileSync(TASK_FILE, '{}');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function loadTasks() {
|
|
59
|
-
ensureTaskStore();
|
|
60
|
-
try {
|
|
61
|
-
return JSON.parse(fs.readFileSync(TASK_FILE, 'utf8') || '{}');
|
|
62
|
-
} catch {
|
|
63
|
-
return {};
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function saveTasks(tasks) {
|
|
68
|
-
ensureTaskStore();
|
|
69
|
-
fs.writeFileSync(TASK_FILE, JSON.stringify(tasks, null, 2));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function makeTaskId() {
|
|
73
|
-
return `task_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function nowIso() {
|
|
77
|
-
return new Date().toISOString();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function runTaskAsync(taskId, toolName, toolArgs) {
|
|
81
|
-
setImmediate(async () => {
|
|
82
|
-
const tasks = loadTasks();
|
|
83
|
-
const t = tasks[taskId];
|
|
84
|
-
if (!t || t.state === 'cancelled') return;
|
|
85
|
-
|
|
86
|
-
t.state = 'running';
|
|
87
|
-
t.startedAt = nowIso();
|
|
88
|
-
t.updatedAt = nowIso();
|
|
89
|
-
saveTasks(tasks);
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
let result;
|
|
93
|
-
if (toolName === 'scan_skill') result = handleScanSkill(toolArgs);
|
|
94
|
-
else if (toolName === 'scan_text') result = handleScanText(toolArgs);
|
|
95
|
-
else if (toolName === 'check_tool_call') result = handleCheckToolCall(toolArgs);
|
|
96
|
-
else if (toolName === 'audit_assets') result = await handleAuditAssets(toolArgs);
|
|
97
|
-
else if (toolName === 'get_stats') result = handleGetStats(toolArgs);
|
|
98
|
-
else throw new Error(`Unsupported async tool: ${toolName}`);
|
|
99
|
-
|
|
100
|
-
const latest = loadTasks();
|
|
101
|
-
if (!latest[taskId] || latest[taskId].state === 'cancelled') return;
|
|
102
|
-
latest[taskId].state = result?.isError ? 'failed' : 'succeeded';
|
|
103
|
-
latest[taskId].updatedAt = nowIso();
|
|
104
|
-
latest[taskId].finishedAt = nowIso();
|
|
105
|
-
latest[taskId].result = result;
|
|
106
|
-
saveTasks(latest);
|
|
107
|
-
} catch (e) {
|
|
108
|
-
const latest = loadTasks();
|
|
109
|
-
if (!latest[taskId]) return;
|
|
110
|
-
latest[taskId].state = 'failed';
|
|
111
|
-
latest[taskId].updatedAt = nowIso();
|
|
112
|
-
latest[taskId].finishedAt = nowIso();
|
|
113
|
-
latest[taskId].error = e.message;
|
|
114
|
-
saveTasks(latest);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ── Tool Definitions ──
|
|
120
|
-
|
|
121
|
-
const TOOLS = [
|
|
122
|
-
{
|
|
123
|
-
name: 'scan_skill',
|
|
124
|
-
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.',
|
|
125
|
-
inputSchema: {
|
|
126
|
-
type: 'object',
|
|
127
|
-
properties: {
|
|
128
|
-
path: {
|
|
129
|
-
type: 'string',
|
|
130
|
-
description: 'Absolute path to the directory to scan',
|
|
131
|
-
},
|
|
132
|
-
verbose: {
|
|
133
|
-
type: 'boolean',
|
|
134
|
-
description: 'Include detailed finding samples',
|
|
135
|
-
default: false,
|
|
136
|
-
},
|
|
137
|
-
strict: {
|
|
138
|
-
type: 'boolean',
|
|
139
|
-
description: 'Lower detection thresholds (more sensitive)',
|
|
140
|
-
default: false,
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
required: ['path'],
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
name: 'scan_text',
|
|
148
|
-
description: 'Scan a code or text snippet for security threats inline. Useful for checking generated code, tool descriptions, or agent prompts before execution. Returns matched patterns with severity levels.',
|
|
149
|
-
inputSchema: {
|
|
150
|
-
type: 'object',
|
|
151
|
-
properties: {
|
|
152
|
-
text: {
|
|
153
|
-
type: 'string',
|
|
154
|
-
description: 'The code or text content to scan',
|
|
155
|
-
},
|
|
156
|
-
filename: {
|
|
157
|
-
type: 'string',
|
|
158
|
-
description: 'Optional filename hint for file-type detection (e.g. "script.js")',
|
|
159
|
-
default: 'snippet.txt',
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
required: ['text'],
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
name: 'check_tool_call',
|
|
167
|
-
description: 'Runtime security check for an agent tool call (before_tool_call equivalent). Checks 26 threat patterns across 5 layers: Threat Detection, Trust Defense, Safety Judge, Brain/Behavioral, Trust Exploitation. Returns whether the call should be blocked.',
|
|
168
|
-
inputSchema: {
|
|
169
|
-
type: 'object',
|
|
170
|
-
properties: {
|
|
171
|
-
tool: {
|
|
172
|
-
type: 'string',
|
|
173
|
-
description: 'Name of the tool being called (e.g. "exec", "write", "shell")',
|
|
174
|
-
},
|
|
175
|
-
args: {
|
|
176
|
-
type: 'object',
|
|
177
|
-
description: 'Arguments/parameters of the tool call',
|
|
178
|
-
additionalProperties: true,
|
|
179
|
-
},
|
|
180
|
-
mode: {
|
|
181
|
-
type: 'string',
|
|
182
|
-
enum: ['monitor', 'enforce', 'strict'],
|
|
183
|
-
description: 'Guard mode — monitor: log only, enforce: block CRITICAL (default), strict: block HIGH+CRITICAL',
|
|
184
|
-
default: 'enforce',
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
required: ['tool', 'args'],
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
name: 'audit_assets',
|
|
192
|
-
description: 'Audit npm packages or GitHub repositories for security exposure. Checks for accidental source leaks, overly permissive access, and supply chain risks.',
|
|
193
|
-
inputSchema: {
|
|
194
|
-
type: 'object',
|
|
195
|
-
properties: {
|
|
196
|
-
username: {
|
|
197
|
-
type: 'string',
|
|
198
|
-
description: 'npm or GitHub username to audit',
|
|
199
|
-
},
|
|
200
|
-
scope: {
|
|
201
|
-
type: 'string',
|
|
202
|
-
enum: ['npm', 'github', 'all'],
|
|
203
|
-
description: 'What to audit — npm packages, GitHub repos, or all',
|
|
204
|
-
default: 'all',
|
|
205
|
-
},
|
|
206
|
-
},
|
|
207
|
-
required: ['username'],
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
name: 'get_stats',
|
|
212
|
-
description: 'Get guard-scanner capabilities, version, and detection statistics. Returns pattern counts, runtime check counts by layer and severity, supported categories, and configuration.',
|
|
213
|
-
inputSchema: {
|
|
214
|
-
type: 'object',
|
|
215
|
-
properties: {},
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
name: 'run_async',
|
|
220
|
-
description: 'Run a supported guard-scanner tool asynchronously. Returns taskId immediately; use task_status/task_result to retrieve output.',
|
|
221
|
-
inputSchema: {
|
|
222
|
-
type: 'object',
|
|
223
|
-
properties: {
|
|
224
|
-
tool: { type: 'string', enum: ['scan_skill', 'scan_text', 'check_tool_call', 'audit_assets', 'get_stats'] },
|
|
225
|
-
args: { type: 'object', additionalProperties: true, default: {} },
|
|
226
|
-
},
|
|
227
|
-
required: ['tool'],
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
name: 'task_status',
|
|
232
|
-
description: 'Get async task status by taskId.',
|
|
233
|
-
inputSchema: {
|
|
234
|
-
type: 'object',
|
|
235
|
-
properties: { taskId: { type: 'string' } },
|
|
236
|
-
required: ['taskId'],
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
name: 'task_result',
|
|
241
|
-
description: 'Get async task final result by taskId.',
|
|
242
|
-
inputSchema: {
|
|
243
|
-
type: 'object',
|
|
244
|
-
properties: { taskId: { type: 'string' } },
|
|
245
|
-
required: ['taskId'],
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
name: 'task_cancel',
|
|
250
|
-
description: 'Cancel async task by taskId (best-effort).',
|
|
251
|
-
inputSchema: {
|
|
252
|
-
type: 'object',
|
|
253
|
-
properties: { taskId: { type: 'string' }, reason: { type: 'string' } },
|
|
254
|
-
required: ['taskId'],
|
|
255
|
-
},
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
name: 'cron_glm5_config',
|
|
259
|
-
description: 'Build a safe OpenClaw cron config and CLI command using model zai/glm-5 for MCP/cron automation.',
|
|
260
|
-
inputSchema: {
|
|
261
|
-
type: 'object',
|
|
262
|
-
properties: {
|
|
263
|
-
name: { type: 'string' },
|
|
264
|
-
cron: { type: 'string', description: '5-field cron expr (e.g. 0 7 * * *)' },
|
|
265
|
-
tz: { type: 'string', default: 'Asia/Tokyo' },
|
|
266
|
-
message: { type: 'string' },
|
|
267
|
-
channel: { type: 'string', default: 'last' },
|
|
268
|
-
to: { type: 'string' },
|
|
269
|
-
wake: { type: 'string', enum: ['now', 'next-heartbeat'], default: 'next-heartbeat' }
|
|
270
|
-
},
|
|
271
|
-
required: ['name', 'cron', 'message'],
|
|
272
|
-
},
|
|
273
|
-
},
|
|
274
|
-
];
|
|
275
|
-
|
|
276
|
-
// ── Tool Handlers ──
|
|
277
|
-
|
|
278
|
-
function handleScanSkill({ path: scanPath, verbose = false, strict = false }) {
|
|
279
|
-
if (!scanPath) return errorResult('path is required');
|
|
280
|
-
|
|
281
|
-
const fs = require('fs');
|
|
282
|
-
if (!fs.existsSync(scanPath)) {
|
|
283
|
-
return errorResult(`Directory not found: ${scanPath}`);
|
|
284
|
-
}
|
|
285
|
-
if (!fs.statSync(scanPath).isDirectory()) {
|
|
286
|
-
return errorResult(`Not a directory: ${scanPath}`);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const scanner = new GuardScanner({ verbose, strict, quiet: true });
|
|
290
|
-
scanner.scanDirectory(scanPath);
|
|
291
|
-
const report = scanner.toJSON();
|
|
292
|
-
|
|
293
|
-
// Count findings by severity
|
|
294
|
-
let critical = 0, high = 0, medium = 0, low = 0, total = 0;
|
|
295
|
-
for (const skill of report.findings) {
|
|
296
|
-
for (const f of skill.findings) {
|
|
297
|
-
total++;
|
|
298
|
-
if (f.severity === 'CRITICAL') critical++;
|
|
299
|
-
else if (f.severity === 'HIGH') high++;
|
|
300
|
-
else if (f.severity === 'MEDIUM') medium++;
|
|
301
|
-
else low++;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const verdict = report.stats.malicious > 0 ? '🔴 MALICIOUS'
|
|
306
|
-
: report.stats.suspicious > 0 ? '🟡 SUSPICIOUS'
|
|
307
|
-
: '🟢 SAFE';
|
|
308
|
-
|
|
309
|
-
return successResult(
|
|
310
|
-
`🛡️ Scan: ${verdict}\n` +
|
|
311
|
-
`Files: ${report.stats.scanned}, Skills: ${report.findings.length}\n` +
|
|
312
|
-
`Clean: ${report.stats.clean}, Suspicious: ${report.stats.suspicious}, Malicious: ${report.stats.malicious}\n` +
|
|
313
|
-
`Findings: ${total} (Critical: ${critical}, High: ${high}, Medium: ${medium}, Low: ${low})\n` +
|
|
314
|
-
(total > 0
|
|
315
|
-
? '\nTop findings:\n' + report.findings.flatMap(s =>
|
|
316
|
-
s.findings.slice(0, 5).map(f =>
|
|
317
|
-
` [${f.severity}] ${f.id}: ${f.desc} — ${s.skill}/${f.file}`
|
|
318
|
-
)
|
|
319
|
-
).slice(0, 10).join('\n')
|
|
320
|
-
: '\n✅ No threats detected.')
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function handleScanText({ text, filename = 'snippet.txt' }) {
|
|
325
|
-
if (!text) return errorResult('text is required');
|
|
326
|
-
|
|
327
|
-
const os = require('os');
|
|
328
|
-
const fs = require('fs');
|
|
329
|
-
const path = require('path');
|
|
330
|
-
|
|
331
|
-
// Write text to temp file, scan, cleanup
|
|
332
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gs-'));
|
|
333
|
-
const tmpFile = path.join(tmpDir, filename);
|
|
334
|
-
fs.writeFileSync(tmpFile, text);
|
|
335
|
-
|
|
336
|
-
const scanner = new GuardScanner({ quiet: true });
|
|
337
|
-
scanner.scanDirectory(tmpDir);
|
|
338
|
-
const report = scanner.toJSON();
|
|
339
|
-
|
|
340
|
-
// Cleanup
|
|
341
|
-
try { fs.unlinkSync(tmpFile); fs.rmdirSync(tmpDir); } catch { /* ok */ }
|
|
342
|
-
|
|
343
|
-
return successResult(
|
|
344
|
-
`🛡️ Text Scan: ${report.verdict}\n` +
|
|
345
|
-
`Score: ${report.risk.score} (${report.risk.level})\n` +
|
|
346
|
-
`Findings: ${report.summary.totalFindings}\n` +
|
|
347
|
-
(report.findings.length > 0
|
|
348
|
-
? '\nDetected:\n' + report.findings.map(f =>
|
|
349
|
-
` [${f.severity}] ${f.id}: ${f.desc}`
|
|
350
|
-
).join('\n')
|
|
351
|
-
: '\n✅ No threats detected.')
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function handleCheckToolCall({ tool, args, mode = 'enforce' }) {
|
|
356
|
-
if (!tool) return errorResult('tool is required');
|
|
357
|
-
if (args === undefined) return errorResult('args is required');
|
|
358
|
-
|
|
359
|
-
const result = scanToolCall(tool, args, { mode, auditLog: true });
|
|
360
|
-
|
|
361
|
-
if (result.detections.length === 0) {
|
|
362
|
-
return successResult(
|
|
363
|
-
`✅ Tool call "${tool}" passed all 26 runtime checks.\nMode: ${mode}`
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const lines = result.detections.map(d =>
|
|
368
|
-
` [${d.action.toUpperCase()}] ${d.id} (${d.severity}, L${d.layer}): ${d.desc}`
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
return successResult(
|
|
372
|
-
`🛡️ Runtime Check: ${result.blocked ? '🚫 BLOCKED' : '⚠️ WARNINGS'}\n` +
|
|
373
|
-
`Tool: ${tool} | Mode: ${mode}\n` +
|
|
374
|
-
`Detections: ${result.detections.length}\n\n` +
|
|
375
|
-
lines.join('\n') +
|
|
376
|
-
(result.blocked ? `\n\n❌ Blocked: ${result.blockReason}` : '')
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
async function handleAuditAssets({ username, scope = 'all' }) {
|
|
381
|
-
if (!username) return errorResult('username is required');
|
|
382
|
-
|
|
383
|
-
const auditor = new AssetAuditor({ quiet: true });
|
|
384
|
-
|
|
385
|
-
try {
|
|
386
|
-
if (scope === 'npm' || scope === 'all') {
|
|
387
|
-
await auditor.auditNpm(username);
|
|
388
|
-
}
|
|
389
|
-
if (scope === 'github' || scope === 'all') {
|
|
390
|
-
await auditor.auditGithub(username);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const report = auditor.toJSON();
|
|
394
|
-
const verdict = auditor.getVerdict();
|
|
395
|
-
|
|
396
|
-
return successResult(
|
|
397
|
-
`🛡️ Asset Audit: ${verdict.label}\n` +
|
|
398
|
-
`User: ${username} | Scope: ${scope}\n` +
|
|
399
|
-
`Alerts: ${report.summary.totalAlerts} ` +
|
|
400
|
-
`(Critical: ${report.summary.critical}, High: ${report.summary.high}, ` +
|
|
401
|
-
`Medium: ${report.summary.medium}, Low: ${report.summary.low})\n` +
|
|
402
|
-
(report.alerts.length > 0
|
|
403
|
-
? '\nAlerts:\n' + report.alerts.slice(0, 10).map(a =>
|
|
404
|
-
` [${a.severity}] ${a.source}: ${a.message}`
|
|
405
|
-
).join('\n')
|
|
406
|
-
: '\n✅ No exposure detected.')
|
|
407
|
-
);
|
|
408
|
-
} catch (e) {
|
|
409
|
-
return errorResult(`Audit failed: ${e.message}`);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function handleGetStats() {
|
|
414
|
-
const runtimeStats = getCheckStats();
|
|
415
|
-
|
|
416
|
-
return successResult(
|
|
417
|
-
`🛡️ guard-scanner v${VERSION}\n\n` +
|
|
418
|
-
`Static Analysis:\n` +
|
|
419
|
-
` • 166 threat patterns across 23 categories\n` +
|
|
420
|
-
` • Entropy-based secret detection\n` +
|
|
421
|
-
` • Data flow analysis (JS)\n` +
|
|
422
|
-
` • Cross-file reference checking\n` +
|
|
423
|
-
` • SKILL.md manifest validation\n` +
|
|
424
|
-
` • Dependency chain scanning\n` +
|
|
425
|
-
` • SARIF, JSON, HTML reporting\n\n` +
|
|
426
|
-
`Runtime Guard:\n` +
|
|
427
|
-
` • ${runtimeStats.total} checks across ${Object.keys(runtimeStats.byLayer).length} layers\n` +
|
|
428
|
-
Object.entries(runtimeStats.byLayer).map(([l, c]) =>
|
|
429
|
-
` • Layer ${l} (${LAYER_NAMES[l] || 'Unknown'}): ${c} checks`
|
|
430
|
-
).join('\n') + '\n\n' +
|
|
431
|
-
`Asset Audit: v${AUDIT_VERSION}\n` +
|
|
432
|
-
` • npm package exposure detection\n` +
|
|
433
|
-
` • GitHub repository scanning\n` +
|
|
434
|
-
` • ClawHub skill auditing\n\n` +
|
|
435
|
-
`Performance: 0.016ms/scan average\n` +
|
|
436
|
-
`Dependencies: 0 external (node:fs, node:path, node:https only)`
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function handleRunAsync({ tool, args = {} }) {
|
|
441
|
-
if (!tool) return errorResult('tool is required');
|
|
442
|
-
const supported = new Set(['scan_skill', 'scan_text', 'check_tool_call', 'audit_assets', 'get_stats']);
|
|
443
|
-
if (!supported.has(tool)) return errorResult(`Unsupported async tool: ${tool}`);
|
|
444
|
-
|
|
445
|
-
const taskId = makeTaskId();
|
|
446
|
-
const tasks = loadTasks();
|
|
447
|
-
tasks[taskId] = {
|
|
448
|
-
taskId,
|
|
449
|
-
tool,
|
|
450
|
-
args,
|
|
451
|
-
state: 'queued',
|
|
452
|
-
createdAt: nowIso(),
|
|
453
|
-
updatedAt: nowIso(),
|
|
454
|
-
};
|
|
455
|
-
saveTasks(tasks);
|
|
456
|
-
runTaskAsync(taskId, tool, args);
|
|
457
|
-
|
|
458
|
-
return successResult(`accepted\ntaskId=${taskId}\nstate=queued\npollAfterMs=800`);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function handleTaskStatus({ taskId }) {
|
|
462
|
-
if (!taskId) return errorResult('taskId is required');
|
|
463
|
-
const tasks = loadTasks();
|
|
464
|
-
const t = tasks[taskId];
|
|
465
|
-
if (!t) return errorResult(`Task not found: ${taskId}`);
|
|
466
|
-
return successResult(`taskId=${taskId}\nstate=${t.state}\nupdatedAt=${t.updatedAt}`);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function handleTaskResult({ taskId }) {
|
|
470
|
-
if (!taskId) return errorResult('taskId is required');
|
|
471
|
-
const tasks = loadTasks();
|
|
472
|
-
const t = tasks[taskId];
|
|
473
|
-
if (!t) return errorResult(`Task not found: ${taskId}`);
|
|
474
|
-
if (t.state === 'queued' || t.state === 'running') {
|
|
475
|
-
return successResult(`taskId=${taskId}\nstate=${t.state}\nmessage=not ready`);
|
|
476
|
-
}
|
|
477
|
-
if (t.error) return errorResult(`taskId=${taskId}\nstate=${t.state}\nerror=${t.error}`);
|
|
478
|
-
if (!t.result) return errorResult(`taskId=${taskId}\nstate=${t.state}\nerror=no result`);
|
|
479
|
-
return t.result;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function handleTaskCancel({ taskId, reason = 'user cancel' }) {
|
|
483
|
-
if (!taskId) return errorResult('taskId is required');
|
|
484
|
-
const tasks = loadTasks();
|
|
485
|
-
const t = tasks[taskId];
|
|
486
|
-
if (!t) return errorResult(`Task not found: ${taskId}`);
|
|
487
|
-
if (t.state === 'succeeded' || t.state === 'failed' || t.state === 'cancelled') {
|
|
488
|
-
return successResult(`taskId=${taskId}\nstate=${t.state}\nmessage=already terminal`);
|
|
489
|
-
}
|
|
490
|
-
t.state = 'cancelled';
|
|
491
|
-
t.updatedAt = nowIso();
|
|
492
|
-
t.finishedAt = nowIso();
|
|
493
|
-
t.cancelReason = reason;
|
|
494
|
-
saveTasks(tasks);
|
|
495
|
-
return successResult(`taskId=${taskId}\nstate=cancelled`);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function handleCronGlm5Config({ name, cron, tz = 'Asia/Tokyo', message, channel = 'last', to, wake = 'next-heartbeat' }) {
|
|
499
|
-
if (!name || !cron || !message) return errorResult('name, cron, message are required');
|
|
500
|
-
const parts = String(cron).trim().split(/\s+/);
|
|
501
|
-
if (parts.length !== 5) return errorResult('cron must be 5 fields (minute hour day month weekday)');
|
|
502
|
-
|
|
503
|
-
const payload = {
|
|
504
|
-
name,
|
|
505
|
-
schedule: { kind: 'cron', expr: cron, tz },
|
|
506
|
-
sessionTarget: 'isolated',
|
|
507
|
-
wakeMode: wake,
|
|
508
|
-
payload: { kind: 'agentTurn', message, model: 'zai/glm-5' },
|
|
509
|
-
delivery: {
|
|
510
|
-
mode: 'announce',
|
|
511
|
-
channel,
|
|
512
|
-
...(to ? { to } : {}),
|
|
513
|
-
bestEffort: true,
|
|
514
|
-
},
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
const cli = [
|
|
518
|
-
'openclaw cron add',
|
|
519
|
-
`--name ${JSON.stringify(name)}`,
|
|
520
|
-
`--cron ${JSON.stringify(cron)}`,
|
|
521
|
-
`--tz ${JSON.stringify(tz)}`,
|
|
522
|
-
'--session isolated',
|
|
523
|
-
`--message ${JSON.stringify(message)}`,
|
|
524
|
-
'--model "zai/glm-5"',
|
|
525
|
-
`--wake ${wake}`,
|
|
526
|
-
'--announce',
|
|
527
|
-
`--channel ${channel}`,
|
|
528
|
-
...(to ? [`--to ${JSON.stringify(to)}`] : []),
|
|
529
|
-
].join(' \\\n ');
|
|
530
|
-
|
|
531
|
-
return successResult(
|
|
532
|
-
`cron_glm5_config_ready\n\nCLI:\n${cli}\n\nJSON:\n${JSON.stringify(payload, null, 2)}`
|
|
533
|
-
);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// ── Result helpers ──
|
|
537
|
-
|
|
538
|
-
function successResult(text) {
|
|
539
|
-
return { content: [{ type: 'text', text }], isError: false };
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function errorResult(text) {
|
|
543
|
-
return { content: [{ type: 'text', text: `❌ ${text}` }], isError: true };
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// ── MCP JSON-RPC over stdio ──
|
|
547
|
-
|
|
548
|
-
class MCPServer {
|
|
549
|
-
constructor() {
|
|
550
|
-
this._initialized = false;
|
|
551
|
-
this._buffer = '';
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
start() {
|
|
555
|
-
process.stdin.setEncoding('utf8');
|
|
556
|
-
process.stdin.on('data', (chunk) => this._onData(chunk));
|
|
557
|
-
process.stdin.on('end', () => process.exit(0));
|
|
558
|
-
process.stderr.write(`🛡️ guard-scanner MCP server v${VERSION} started\n`);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
_onData(chunk) {
|
|
562
|
-
this._buffer += chunk;
|
|
563
|
-
|
|
564
|
-
// Parse newline-delimited JSON-RPC messages
|
|
565
|
-
let newlineIdx;
|
|
566
|
-
while ((newlineIdx = this._buffer.indexOf('\n')) !== -1) {
|
|
567
|
-
const line = this._buffer.slice(0, newlineIdx).trim();
|
|
568
|
-
this._buffer = this._buffer.slice(newlineIdx + 1);
|
|
569
|
-
|
|
570
|
-
if (line.length === 0) continue;
|
|
571
|
-
|
|
572
|
-
try {
|
|
573
|
-
const msg = JSON.parse(line);
|
|
574
|
-
this._handleMessage(msg);
|
|
575
|
-
} catch (e) {
|
|
576
|
-
// If parse fails, try Content-Length header protocol
|
|
577
|
-
if (line.startsWith('Content-Length:')) {
|
|
578
|
-
this._handleContentLength(line);
|
|
579
|
-
}
|
|
580
|
-
// else ignore malformed input
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Handle Content-Length based protocol (MCP stdio standard)
|
|
585
|
-
this._tryParseContentLength();
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
_handleContentLength(headerLine) {
|
|
589
|
-
// Already handled in _tryParseContentLength
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
_tryParseContentLength() {
|
|
593
|
-
// MCP stdio: "Content-Length: N\r\n\r\n{json}"
|
|
594
|
-
const clMatch = this._buffer.match(/Content-Length:\s*(\d+)\r?\n\r?\n/);
|
|
595
|
-
if (!clMatch) return;
|
|
596
|
-
|
|
597
|
-
const contentLength = parseInt(clMatch[1], 10);
|
|
598
|
-
const headerEnd = clMatch.index + clMatch[0].length;
|
|
599
|
-
const available = this._buffer.length - headerEnd;
|
|
600
|
-
|
|
601
|
-
if (available < contentLength) return; // wait for more data
|
|
602
|
-
|
|
603
|
-
const body = this._buffer.slice(headerEnd, headerEnd + contentLength);
|
|
604
|
-
this._buffer = this._buffer.slice(headerEnd + contentLength);
|
|
605
|
-
|
|
606
|
-
try {
|
|
607
|
-
const msg = JSON.parse(body);
|
|
608
|
-
this._handleMessage(msg);
|
|
609
|
-
} catch { /* ignore */ }
|
|
610
|
-
|
|
611
|
-
// Recurse for more messages
|
|
612
|
-
this._tryParseContentLength();
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
async _handleMessage(msg) {
|
|
616
|
-
if (!msg.method) return; // Not a request or notification
|
|
617
|
-
|
|
618
|
-
// Notifications (no id) — acknowledge silently
|
|
619
|
-
if (msg.id === undefined || msg.id === null) {
|
|
620
|
-
// notifications/initialized, notifications/cancelled, etc.
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
let result;
|
|
625
|
-
try {
|
|
626
|
-
result = await this._dispatch(msg.method, msg.params || {});
|
|
627
|
-
this._send({ jsonrpc: JSONRPC, id: msg.id, result });
|
|
628
|
-
} catch (e) {
|
|
629
|
-
this._send({
|
|
630
|
-
jsonrpc: JSONRPC,
|
|
631
|
-
id: msg.id,
|
|
632
|
-
error: { code: e.code || -32603, message: e.message },
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
async _dispatch(method, params) {
|
|
638
|
-
switch (method) {
|
|
639
|
-
case 'initialize':
|
|
640
|
-
this._initialized = true;
|
|
641
|
-
return {
|
|
642
|
-
protocolVersion: MCP_VERSION,
|
|
643
|
-
capabilities: SERVER_CAPABILITIES,
|
|
644
|
-
serverInfo: SERVER_INFO,
|
|
645
|
-
};
|
|
646
|
-
|
|
647
|
-
case 'tools/list':
|
|
648
|
-
return { tools: TOOLS };
|
|
649
|
-
|
|
650
|
-
case 'tools/call':
|
|
651
|
-
return this._callTool(params.name, params.arguments || {});
|
|
652
|
-
|
|
653
|
-
case 'ping':
|
|
654
|
-
return {};
|
|
655
|
-
|
|
656
|
-
default:
|
|
657
|
-
throw Object.assign(new Error(`Method not found: ${method}`), { code: -32601 });
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
async _callTool(name, args) {
|
|
662
|
-
switch (name) {
|
|
663
|
-
case 'scan_skill':
|
|
664
|
-
return handleScanSkill(args);
|
|
665
|
-
case 'scan_text':
|
|
666
|
-
return handleScanText(args);
|
|
667
|
-
case 'check_tool_call':
|
|
668
|
-
return handleCheckToolCall(args);
|
|
669
|
-
case 'audit_assets':
|
|
670
|
-
return await handleAuditAssets(args);
|
|
671
|
-
case 'get_stats':
|
|
672
|
-
return handleGetStats();
|
|
673
|
-
case 'run_async':
|
|
674
|
-
return handleRunAsync(args);
|
|
675
|
-
case 'task_status':
|
|
676
|
-
return handleTaskStatus(args);
|
|
677
|
-
case 'task_result':
|
|
678
|
-
return handleTaskResult(args);
|
|
679
|
-
case 'task_cancel':
|
|
680
|
-
return handleTaskCancel(args);
|
|
681
|
-
case 'cron_glm5_config':
|
|
682
|
-
return handleCronGlm5Config(args);
|
|
683
|
-
default:
|
|
684
|
-
return errorResult(`Unknown tool: ${name}`);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
_send(msg) {
|
|
689
|
-
const body = JSON.stringify(msg);
|
|
690
|
-
const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`;
|
|
691
|
-
process.stdout.write(header + body);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
// ── Export for CLI integration ──
|
|
696
|
-
|
|
697
|
-
function startServer() {
|
|
698
|
-
const server = new MCPServer();
|
|
699
|
-
server.start();
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
module.exports = { MCPServer, startServer, TOOLS };
|