@guava-parity/guard-scanner 13.0.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.
- package/README.md +42 -253
- package/SECURITY.md +12 -4
- package/SKILL.md +121 -59
- package/dist/openclaw-plugin.mjs +41 -0
- package/docs/EVIDENCE_DRIVEN.md +182 -0
- package/docs/banner.png +0 -0
- package/docs/data/corpus-metrics.json +11 -0
- package/docs/data/latest.json +25837 -2481
- 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 +44 -0
- package/docs/openclaw-continuous-compatibility-plan.md +36 -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 +42 -0
- package/docs/spec/finding.schema.json +104 -0
- package/docs/spec/integration-manifest.md +39 -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.js +305 -0
- package/hooks/guard-scanner/plugin.ts +24 -1
- package/openclaw-plugin.mts +91 -0
- package/openclaw.plugin.json +30 -53
- package/package.json +23 -8
- package/src/cli.js +174 -34
- package/src/core/content-loader.js +42 -0
- package/src/core/inventory.js +73 -0
- package/src/core/report-adapters.js +171 -0
- package/src/core/risk-engine.js +93 -0
- package/src/core/rule-registry.js +73 -0
- package/src/core/semantic-validators.js +85 -0
- package/src/finding-schema.js +191 -0
- package/src/hooks/context.ts +49 -0
- package/src/html-template.js +2 -2
- package/src/mcp-server.js +24 -73
- package/src/openclaw-upstream.js +128 -0
- package/src/patterns.js +371 -353
- package/src/policy-engine.js +32 -0
- package/src/runtime-guard.js +40 -2
- package/src/scanner.js +101 -216
- package/src/skill-crawler.js +254 -0
- package/src/threat-model.js +50 -0
- package/src/validation-layer.js +39 -0
package/src/html-template.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* guard-scanner — HTML Report Template
|
|
3
3
|
* Dark Glassmorphism + Conic-gradient Risk Gauges
|
|
4
|
-
*
|
|
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} —
|
|
232
|
+
guard-scanner v${version} — Lightweight runtime footprint (1 dependency: ws). 🛡️<br>
|
|
233
233
|
Built by <a href="https://github.com/koatora20">Guava 🍈 & 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 —
|
|
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
|
|
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
|
|
@@ -30,6 +30,7 @@ const { AssetAuditor, AUDIT_VERSION } = require('./asset-auditor.js');
|
|
|
30
30
|
const fs = require('fs');
|
|
31
31
|
const path = require('path');
|
|
32
32
|
const os = require('os');
|
|
33
|
+
const CAPABILITIES = require('../docs/spec/capabilities.json');
|
|
33
34
|
|
|
34
35
|
// ── MCP Protocol Constants ──
|
|
35
36
|
|
|
@@ -45,6 +46,8 @@ const SERVER_CAPABILITIES = {
|
|
|
45
46
|
tools: {},
|
|
46
47
|
};
|
|
47
48
|
|
|
49
|
+
const STATIC_SUMMARY = `${CAPABILITIES.static_pattern_count} threat patterns across ${CAPABILITIES.threat_category_count} categories`;
|
|
50
|
+
|
|
48
51
|
// ── Async Task Store (run_async / status / result / cancel) ──
|
|
49
52
|
|
|
50
53
|
const TASK_DIR = process.env.GUARD_SCANNER_TASK_DIR || path.join(os.homedir(), '.openclaw', 'guard-scanner', 'tasks');
|
|
@@ -121,7 +124,7 @@ function runTaskAsync(taskId, toolName, toolArgs) {
|
|
|
121
124
|
const TOOLS = [
|
|
122
125
|
{
|
|
123
126
|
name: 'scan_skill',
|
|
124
|
-
description:
|
|
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.`,
|
|
125
128
|
inputSchema: {
|
|
126
129
|
type: 'object',
|
|
127
130
|
properties: {
|
|
@@ -216,8 +219,8 @@ const TOOLS = [
|
|
|
216
219
|
},
|
|
217
220
|
},
|
|
218
221
|
{
|
|
219
|
-
name: 'run_async',
|
|
220
|
-
description: 'Run a supported guard-scanner tool asynchronously. Returns taskId immediately; use task_status/task_result to retrieve output.',
|
|
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.',
|
|
221
224
|
inputSchema: {
|
|
222
225
|
type: 'object',
|
|
223
226
|
properties: {
|
|
@@ -228,8 +231,8 @@ const TOOLS = [
|
|
|
228
231
|
},
|
|
229
232
|
},
|
|
230
233
|
{
|
|
231
|
-
name: 'task_status',
|
|
232
|
-
description: 'Get async task status by taskId.',
|
|
234
|
+
name: 'experimental.task_status',
|
|
235
|
+
description: '[Experimental] Get async task status by taskId.',
|
|
233
236
|
inputSchema: {
|
|
234
237
|
type: 'object',
|
|
235
238
|
properties: { taskId: { type: 'string' } },
|
|
@@ -237,8 +240,8 @@ const TOOLS = [
|
|
|
237
240
|
},
|
|
238
241
|
},
|
|
239
242
|
{
|
|
240
|
-
name: 'task_result',
|
|
241
|
-
description: 'Get async task final result by taskId.',
|
|
243
|
+
name: 'experimental.task_result',
|
|
244
|
+
description: '[Experimental] Get async task final result by taskId.',
|
|
242
245
|
inputSchema: {
|
|
243
246
|
type: 'object',
|
|
244
247
|
properties: { taskId: { type: 'string' } },
|
|
@@ -246,33 +249,18 @@ const TOOLS = [
|
|
|
246
249
|
},
|
|
247
250
|
},
|
|
248
251
|
{
|
|
249
|
-
name: 'task_cancel',
|
|
250
|
-
description: 'Cancel async task by taskId (best-effort).',
|
|
252
|
+
name: 'experimental.task_cancel',
|
|
253
|
+
description: '[Experimental] Cancel async task by taskId (best-effort).',
|
|
251
254
|
inputSchema: {
|
|
252
255
|
type: 'object',
|
|
253
256
|
properties: { taskId: { type: 'string' }, reason: { type: 'string' } },
|
|
254
257
|
required: ['taskId'],
|
|
255
258
|
},
|
|
256
259
|
},
|
|
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
260
|
];
|
|
275
261
|
|
|
262
|
+
// NOTE: cron_glm5_config was removed in v14.0.0 — not guard-scanner's responsibility
|
|
263
|
+
|
|
276
264
|
// ── Tool Handlers ──
|
|
277
265
|
|
|
278
266
|
function handleScanSkill({ path: scanPath, verbose = false, strict = false }) {
|
|
@@ -357,10 +345,11 @@ function handleCheckToolCall({ tool, args, mode = 'enforce' }) {
|
|
|
357
345
|
if (args === undefined) return errorResult('args is required');
|
|
358
346
|
|
|
359
347
|
const result = scanToolCall(tool, args, { mode, auditLog: true });
|
|
348
|
+
const runtimeCheckCount = getCheckStats().total;
|
|
360
349
|
|
|
361
350
|
if (result.detections.length === 0) {
|
|
362
351
|
return successResult(
|
|
363
|
-
`✅ Tool call "${tool}" passed all
|
|
352
|
+
`✅ Tool call "${tool}" passed all ${runtimeCheckCount} runtime checks.\nMode: ${mode}`
|
|
364
353
|
);
|
|
365
354
|
}
|
|
366
355
|
|
|
@@ -416,7 +405,7 @@ function handleGetStats() {
|
|
|
416
405
|
return successResult(
|
|
417
406
|
`🛡️ guard-scanner v${VERSION}\n\n` +
|
|
418
407
|
`Static Analysis:\n` +
|
|
419
|
-
` •
|
|
408
|
+
` • ${STATIC_SUMMARY}\n` +
|
|
420
409
|
` • Entropy-based secret detection\n` +
|
|
421
410
|
` • Data flow analysis (JS)\n` +
|
|
422
411
|
` • Cross-file reference checking\n` +
|
|
@@ -495,43 +484,7 @@ function handleTaskCancel({ taskId, reason = 'user cancel' }) {
|
|
|
495
484
|
return successResult(`taskId=${taskId}\nstate=cancelled`);
|
|
496
485
|
}
|
|
497
486
|
|
|
498
|
-
|
|
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
|
-
}
|
|
487
|
+
// handleCronGlm5Config removed in v14.0.0 — not guard-scanner's responsibility
|
|
535
488
|
|
|
536
489
|
// ── Result helpers ──
|
|
537
490
|
|
|
@@ -670,16 +623,14 @@ class MCPServer {
|
|
|
670
623
|
return await handleAuditAssets(args);
|
|
671
624
|
case 'get_stats':
|
|
672
625
|
return handleGetStats();
|
|
673
|
-
case 'run_async':
|
|
626
|
+
case 'experimental.run_async':
|
|
674
627
|
return handleRunAsync(args);
|
|
675
|
-
case 'task_status':
|
|
628
|
+
case 'experimental.task_status':
|
|
676
629
|
return handleTaskStatus(args);
|
|
677
|
-
case 'task_result':
|
|
630
|
+
case 'experimental.task_result':
|
|
678
631
|
return handleTaskResult(args);
|
|
679
|
-
case 'task_cancel':
|
|
632
|
+
case 'experimental.task_cancel':
|
|
680
633
|
return handleTaskCancel(args);
|
|
681
|
-
case 'cron_glm5_config':
|
|
682
|
-
return handleCronGlm5Config(args);
|
|
683
634
|
default:
|
|
684
635
|
return errorResult(`Unknown tool: ${name}`);
|
|
685
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
|
+
};
|