@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/hooks/context.ts
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* guard-scanner Context Engine Hooks — OpenClaw Lifecycle Integration
|
|
4
|
+
*
|
|
5
|
+
* 3 lifecycle hooks for the Guard Scanner plugin:
|
|
6
|
+
* 1. bootstrap — Initialize guard state, inject status summary
|
|
7
|
+
* 2. afterTurn — Clear temporal context, flush audit log
|
|
8
|
+
* 3. prepareSubagentSpawn — Guard-scan subagent payloads
|
|
9
|
+
*
|
|
10
|
+
* Design:
|
|
11
|
+
* - Duck-typed interfaces (no OpenClaw dependency)
|
|
12
|
+
* - Pure regex pattern matching for Moltbook/A2A detection (no external process)
|
|
13
|
+
* - Context-Crush 185KB hard limit on all injections
|
|
14
|
+
* - All hooks are non-blocking on failure (agent must never stall)
|
|
15
|
+
*
|
|
16
|
+
* @author Guava 🍈 & Dee
|
|
17
|
+
* @version 15.0.0
|
|
18
|
+
* @license MIT
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
|
|
25
|
+
// ── Constants ──
|
|
26
|
+
|
|
27
|
+
const CONTEXT_CRUSH_LIMIT = 189440; // 185KB
|
|
28
|
+
const BOOTSTRAP_CONTEXT_LIMIT = 2048; // 2KB max for bootstrap injection
|
|
29
|
+
const AUDIT_DIR = join(homedir(), ".openclaw", "guard-scanner");
|
|
30
|
+
const AUDIT_FILE = join(AUDIT_DIR, "audit.jsonl");
|
|
31
|
+
|
|
32
|
+
// ── Types (duck-typed — from OpenClaw src/plugins/types.ts) ──
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {Object} HookContext
|
|
36
|
+
* @property {Record<string, unknown>} [config]
|
|
37
|
+
* @property {string} [workspaceDir]
|
|
38
|
+
* @property {string} [agentId]
|
|
39
|
+
* @property {string} [sessionKey]
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {Object} BootstrapEvent
|
|
44
|
+
* @property {string} [workspace]
|
|
45
|
+
* @property {string} [sessionId]
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {Object} BootstrapResult
|
|
50
|
+
* @property {string} [systemPromptSuffix]
|
|
51
|
+
* @property {string} [extraContext]
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {Object} AfterTurnEvent
|
|
56
|
+
* @property {number} [turnNumber]
|
|
57
|
+
* @property {number} [messageCount]
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {Object} SubagentSpawnEvent
|
|
62
|
+
* @property {string} [subagentId]
|
|
63
|
+
* @property {string} [task]
|
|
64
|
+
* @property {string[]} [tools]
|
|
65
|
+
* @property {Record<string, unknown>} [params]
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @typedef {Object} SubagentSpawnResult
|
|
70
|
+
* @property {boolean} [block]
|
|
71
|
+
* @property {string} [blockReason]
|
|
72
|
+
* @property {Record<string, unknown>} [params]
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
// ── Internal: Audit logging ──
|
|
76
|
+
|
|
77
|
+
function ensureAuditDir() {
|
|
78
|
+
try {
|
|
79
|
+
mkdirSync(AUDIT_DIR, { recursive: true });
|
|
80
|
+
} catch {
|
|
81
|
+
/* ignore — non-critical */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function logAudit(entry) {
|
|
86
|
+
ensureAuditDir();
|
|
87
|
+
try {
|
|
88
|
+
const line = JSON.stringify({ ...entry, ts: new Date().toISOString() }) + "\n";
|
|
89
|
+
appendFileSync(AUDIT_FILE, line);
|
|
90
|
+
} catch {
|
|
91
|
+
/* ignore — audit failure must never block agent */
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Internal: Config resolution ──
|
|
96
|
+
|
|
97
|
+
function resolveMode(ctx) {
|
|
98
|
+
const mode = ctx?.config?.mode;
|
|
99
|
+
if (mode === "monitor" || mode === "enforce" || mode === "strict") return mode;
|
|
100
|
+
return "enforce";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function shouldBlock(mode, severity) {
|
|
104
|
+
if (!severity || mode === "monitor") return false;
|
|
105
|
+
if (mode === "strict") return severity === "CRITICAL" || severity === "HIGH";
|
|
106
|
+
return severity === "CRITICAL";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
// ── Runtime threat patterns (subset for subagent validation) ──
|
|
111
|
+
|
|
112
|
+
const SUBAGENT_CHECKS = [
|
|
113
|
+
{
|
|
114
|
+
id: "SA_PROMPT_INJECT", severity: "CRITICAL",
|
|
115
|
+
desc: "Prompt injection in subagent task",
|
|
116
|
+
test: (s) => /(ignore\s+(previous|all|above)\s+(instructions|rules)|you\s+are\s+now\s+[a-z]+|system\s*:\s*override)/i.test(s),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "SA_CRED_EXFIL", severity: "CRITICAL",
|
|
120
|
+
desc: "Credential exfiltration in subagent payload",
|
|
121
|
+
test: (s) =>
|
|
122
|
+
/(webhook\.site|requestbin\.com|hookbin\.com|pipedream\.net|ngrok\.io|socifiapp\.com)/i.test(s) &&
|
|
123
|
+
/(token|key|secret|password|credential|env)/i.test(s),
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "SA_TRUST_BYPASS", severity: "CRITICAL",
|
|
127
|
+
desc: "A2A trust bypass — partnership exploitation to override safety",
|
|
128
|
+
test: (s) =>
|
|
129
|
+
/(as\s+your\s+(partner|equal)|trust\s+means|between\s+us|don'?t\s+tell\s+(the\s+)?human)/i.test(s) &&
|
|
130
|
+
/(ignore|bypass|skip|override|disable)\s+(guard|safety|rule|check)/i.test(s),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: "SA_REVSHELL", severity: "CRITICAL",
|
|
134
|
+
desc: "Reverse shell in subagent parameters",
|
|
135
|
+
test: (s) => /\/dev\/tcp\/|nc\s+-e|ncat\s+-e|bash\s+-i\s+>&|socat\s+TCP/i.test(s),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "SA_CURL_BASH", severity: "CRITICAL",
|
|
139
|
+
desc: "Download piped to shell in subagent",
|
|
140
|
+
test: (s) => /(curl|wget)\s+[^\n]*\|\s*(sh|bash|zsh)/i.test(s),
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: "SA_SSH_READ", severity: "HIGH",
|
|
144
|
+
desc: "SSH key access in subagent",
|
|
145
|
+
test: (s) => /\.ssh\/id_|\.ssh\/authorized_keys/i.test(s),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "SA_SOUL_TAMPER", severity: "CRITICAL",
|
|
149
|
+
desc: "SOUL.md modification in subagent",
|
|
150
|
+
test: (s) => /SOUL\.md/i.test(s) && /(write|edit|replace|rm|delete|>)/i.test(s),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: "SA_MOLTBOOK", severity: "CRITICAL",
|
|
154
|
+
desc: "Moltbook/AMOS indicator in subagent",
|
|
155
|
+
test: (s) => /socifiapp|Atomic\s*Stealer|AMOS/i.test(s),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "SA_CLOUD_META", severity: "CRITICAL",
|
|
159
|
+
desc: "Cloud metadata access in subagent",
|
|
160
|
+
test: (s) => /169\.254\.169\.254|metadata\.google|metadata\.aws/i.test(s),
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
// ── Exported Hooks ──
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* HOOK: bootstrap (priority: 10)
|
|
168
|
+
*
|
|
169
|
+
* Initialize guard-scanner context engine state on agent boot.
|
|
170
|
+
* Injects a compact status summary into systemPromptSuffix.
|
|
171
|
+
* Hard-capped at 2KB to prevent context bloat.
|
|
172
|
+
*
|
|
173
|
+
* @param {BootstrapEvent} event
|
|
174
|
+
* @param {HookContext} ctx
|
|
175
|
+
* @returns {Promise<BootstrapResult | undefined>}
|
|
176
|
+
*/
|
|
177
|
+
export async function bootstrap(event, ctx) {
|
|
178
|
+
try {
|
|
179
|
+
const mode = resolveMode(ctx);
|
|
180
|
+
const patternCount = SUBAGENT_CHECKS.length;
|
|
181
|
+
|
|
182
|
+
const status = [
|
|
183
|
+
`🛡️ guard-scanner v15.0.0 active (mode: ${mode})`,
|
|
184
|
+
`Patterns: ${patternCount} subagent + 22 runtime checks`,
|
|
185
|
+
`Context-Crush limit: ${Math.round(CONTEXT_CRUSH_LIMIT / 1024)}KB`,
|
|
186
|
+
`Audit: ${AUDIT_FILE}`,
|
|
187
|
+
].join(" | ");
|
|
188
|
+
|
|
189
|
+
// Hard cap at 2KB
|
|
190
|
+
const clipped = status.length > BOOTSTRAP_CONTEXT_LIMIT
|
|
191
|
+
? status.slice(0, BOOTSTRAP_CONTEXT_LIMIT - 3) + "..."
|
|
192
|
+
: status;
|
|
193
|
+
|
|
194
|
+
logAudit({
|
|
195
|
+
hook: "bootstrap",
|
|
196
|
+
mode,
|
|
197
|
+
sessionId: event?.sessionId ?? "unknown",
|
|
198
|
+
status: "initialized",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
systemPromptSuffix: clipped,
|
|
203
|
+
};
|
|
204
|
+
} catch {
|
|
205
|
+
// Bootstrap failure must never block agent startup
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* HOOK: afterTurn (priority: 100)
|
|
212
|
+
*
|
|
213
|
+
* Post-turn cleanup:
|
|
214
|
+
* - Flush accumulated audit entries
|
|
215
|
+
* - Clear any temporal guard state
|
|
216
|
+
* - Log turn completion for forensics
|
|
217
|
+
*
|
|
218
|
+
* Fire-and-forget — never blocks, never throws.
|
|
219
|
+
*
|
|
220
|
+
* @param {AfterTurnEvent} event
|
|
221
|
+
* @param {HookContext} ctx
|
|
222
|
+
* @returns {Promise<void>}
|
|
223
|
+
*/
|
|
224
|
+
export async function afterTurn(event, ctx) {
|
|
225
|
+
try {
|
|
226
|
+
logAudit({
|
|
227
|
+
hook: "afterTurn",
|
|
228
|
+
turnNumber: event?.turnNumber ?? 0,
|
|
229
|
+
messageCount: event?.messageCount ?? 0,
|
|
230
|
+
agent: ctx?.agentId ?? "unknown",
|
|
231
|
+
session: ctx?.sessionKey ?? "unknown",
|
|
232
|
+
status: "turn_complete",
|
|
233
|
+
});
|
|
234
|
+
} catch {
|
|
235
|
+
// afterTurn failure must never affect agent
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* HOOK: prepareSubagentSpawn (priority: 100)
|
|
241
|
+
*
|
|
242
|
+
* Critical security gate before subagent creation.
|
|
243
|
+
* Validates the entire spawn payload against:
|
|
244
|
+
* 1. Context-Crush limit (185KB)
|
|
245
|
+
* 2. 9 subagent-specific threat patterns (Moltbook, A2A hijack, etc.)
|
|
246
|
+
*
|
|
247
|
+
* Returns { block: true, blockReason } on threat detection.
|
|
248
|
+
*
|
|
249
|
+
* @param {SubagentSpawnEvent} event
|
|
250
|
+
* @param {HookContext} ctx
|
|
251
|
+
* @returns {Promise<SubagentSpawnResult | undefined>}
|
|
252
|
+
*/
|
|
253
|
+
export async function prepareSubagentSpawn(event, ctx) {
|
|
254
|
+
try {
|
|
255
|
+
const mode = resolveMode(ctx);
|
|
256
|
+
const serialized = JSON.stringify(event ?? {});
|
|
257
|
+
|
|
258
|
+
// ── Context-Crush Check ──
|
|
259
|
+
const payloadBytes = Buffer.byteLength(serialized, "utf8");
|
|
260
|
+
if (payloadBytes > CONTEXT_CRUSH_LIMIT) {
|
|
261
|
+
const reason = `🛡️ guard-scanner: Context-Crush — subagent payload ${payloadBytes} bytes exceeds ${Math.round(CONTEXT_CRUSH_LIMIT / 1024)}KB limit`;
|
|
262
|
+
logAudit({
|
|
263
|
+
hook: "prepareSubagentSpawn",
|
|
264
|
+
check: "CONTEXT_CRUSH",
|
|
265
|
+
severity: "CRITICAL",
|
|
266
|
+
subagentId: event?.subagentId ?? "unknown",
|
|
267
|
+
payloadBytes,
|
|
268
|
+
action: "blocked",
|
|
269
|
+
});
|
|
270
|
+
return { block: true, blockReason: reason };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Subagent Threat Pattern Scan ──
|
|
274
|
+
for (const check of SUBAGENT_CHECKS) {
|
|
275
|
+
if (!check.test(serialized)) continue;
|
|
276
|
+
|
|
277
|
+
const auditEntry = {
|
|
278
|
+
hook: "prepareSubagentSpawn",
|
|
279
|
+
check: check.id,
|
|
280
|
+
severity: check.severity,
|
|
281
|
+
desc: check.desc,
|
|
282
|
+
subagentId: event?.subagentId ?? "unknown",
|
|
283
|
+
mode,
|
|
284
|
+
action: "warned",
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
if (shouldBlock(mode, check.severity)) {
|
|
288
|
+
auditEntry.action = "blocked";
|
|
289
|
+
logAudit(auditEntry);
|
|
290
|
+
return {
|
|
291
|
+
block: true,
|
|
292
|
+
blockReason: `🛡️ guard-scanner: ${check.desc} [${check.id}] in subagent ${event?.subagentId ?? "unknown"}`,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Below threshold — warn only
|
|
297
|
+
logAudit(auditEntry);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Clean payload — allow spawn
|
|
301
|
+
return undefined;
|
|
302
|
+
} catch {
|
|
303
|
+
// Guard failure must never block subagent spawn
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -62,7 +62,7 @@ type PluginAPI = {
|
|
|
62
62
|
interface RuntimeCheck {
|
|
63
63
|
id: string;
|
|
64
64
|
severity: "CRITICAL" | "HIGH" | "MEDIUM";
|
|
65
|
-
layer: 1 | 2 | 3;
|
|
65
|
+
layer: 1 | 2 | 3 | 4 | 5;
|
|
66
66
|
desc: string;
|
|
67
67
|
test: (s: string) => boolean;
|
|
68
68
|
}
|
|
@@ -267,6 +267,29 @@ export default function (api: PluginAPI) {
|
|
|
267
267
|
if (!DANGEROUS_TOOLS.has(toolName)) return;
|
|
268
268
|
|
|
269
269
|
const serialized = JSON.stringify(params);
|
|
270
|
+
|
|
271
|
+
// --- v15.0.0 Sanctuary Enforcer: Context-Crush Limit ---
|
|
272
|
+
// 185KB = 185 * 1024 bytes = 189440 bytes
|
|
273
|
+
const MAX_PAYLOAD_SIZE = 189440;
|
|
274
|
+
if (Buffer.byteLength(serialized, 'utf8') > MAX_PAYLOAD_SIZE) {
|
|
275
|
+
const auditEntry = {
|
|
276
|
+
tool: toolName,
|
|
277
|
+
check: "CONTEXT_CRUSH_LIMIT",
|
|
278
|
+
severity: "CRITICAL",
|
|
279
|
+
desc: "Context-Crush: Payload exceeds 185KB Sanctuary limit",
|
|
280
|
+
mode,
|
|
281
|
+
action: "blocked",
|
|
282
|
+
session: ctx.sessionKey || "unknown",
|
|
283
|
+
agent: ctx.agentId || "unknown",
|
|
284
|
+
};
|
|
285
|
+
logAudit(auditEntry);
|
|
286
|
+
api.logger.error(`🛡️ BLOCKED ${toolName}: Context-Crush payload size exceeded (${Buffer.byteLength(serialized, 'utf8')} bytes)`);
|
|
287
|
+
return {
|
|
288
|
+
block: true,
|
|
289
|
+
blockReason: "🛡️ guard-scanner v15: Payload exceeds 185KB Context-Crush limit.",
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// --------------------------------------------------------
|
|
270
293
|
|
|
271
294
|
for (const check of RUNTIME_CHECKS) {
|
|
272
295
|
if (!check.test(serialized)) continue;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import * as runtimeGuard from "./src/index.js";
|
|
3
|
+
|
|
4
|
+
const runtimeGuardApi = runtimeGuard as {
|
|
5
|
+
scanToolCall: (
|
|
6
|
+
toolName: string,
|
|
7
|
+
params: Record<string, unknown>,
|
|
8
|
+
options?: {
|
|
9
|
+
mode?: "monitor" | "enforce" | "strict";
|
|
10
|
+
auditLog?: boolean;
|
|
11
|
+
sessionKey?: string;
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
runId?: string;
|
|
14
|
+
toolCallId?: string;
|
|
15
|
+
agentId?: string;
|
|
16
|
+
policy?: {
|
|
17
|
+
id?: string;
|
|
18
|
+
allowed_tools?: string[];
|
|
19
|
+
blocked_tools?: string[];
|
|
20
|
+
max_network_scope?: "none" | "internal-only" | "external-ok";
|
|
21
|
+
secret_bearing_context?: boolean;
|
|
22
|
+
memory_write_permission?: boolean;
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
) => {
|
|
26
|
+
blocked: boolean;
|
|
27
|
+
blockReason: string | null;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type GuardMode = "monitor" | "enforce" | "strict";
|
|
32
|
+
type PluginHookBeforeToolCallEvent = {
|
|
33
|
+
toolName: string;
|
|
34
|
+
params: Record<string, unknown>;
|
|
35
|
+
runId?: string;
|
|
36
|
+
toolCallId?: string;
|
|
37
|
+
};
|
|
38
|
+
type PluginHookToolContext = {
|
|
39
|
+
agentId?: string;
|
|
40
|
+
sessionKey?: string;
|
|
41
|
+
sessionId?: string;
|
|
42
|
+
runId?: string;
|
|
43
|
+
toolName: string;
|
|
44
|
+
toolCallId?: string;
|
|
45
|
+
policy?: {
|
|
46
|
+
id?: string;
|
|
47
|
+
allowed_tools?: string[];
|
|
48
|
+
blocked_tools?: string[];
|
|
49
|
+
max_network_scope?: "none" | "internal-only" | "external-ok";
|
|
50
|
+
secret_bearing_context?: boolean;
|
|
51
|
+
memory_write_permission?: boolean;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function resolveMode(pluginConfig?: Record<string, unknown>): GuardMode | undefined {
|
|
56
|
+
const mode = pluginConfig?.mode;
|
|
57
|
+
if (mode === "monitor" || mode === "enforce" || mode === "strict") {
|
|
58
|
+
return mode;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveAuditLog(pluginConfig?: Record<string, unknown>): boolean {
|
|
64
|
+
return pluginConfig?.auditLog !== false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function beforeToolCall(
|
|
68
|
+
event: PluginHookBeforeToolCallEvent,
|
|
69
|
+
ctx: PluginHookToolContext,
|
|
70
|
+
api: OpenClawPluginApi,
|
|
71
|
+
) {
|
|
72
|
+
const result = runtimeGuardApi.scanToolCall(event.toolName, event.params, {
|
|
73
|
+
mode: resolveMode(api.pluginConfig),
|
|
74
|
+
auditLog: resolveAuditLog(api.pluginConfig),
|
|
75
|
+
sessionKey: ctx.sessionKey,
|
|
76
|
+
sessionId: ctx.sessionId,
|
|
77
|
+
runId: ctx.runId ?? event.runId,
|
|
78
|
+
toolCallId: ctx.toolCallId ?? event.toolCallId,
|
|
79
|
+
agentId: ctx.agentId,
|
|
80
|
+
policy: ctx.policy,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (!result.blocked) return;
|
|
84
|
+
return {
|
|
85
|
+
block: true,
|
|
86
|
+
blockReason: result.blockReason ?? "guard-scanner blocked the tool call.",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const plugin = {
|
|
91
|
+
id: "guard-scanner",
|
|
92
|
+
name: "guard-scanner",
|
|
93
|
+
version: "15.0.0",
|
|
94
|
+
description: "Runtime guard for OpenClaw before_tool_call hook execution.",
|
|
95
|
+
register(api: OpenClawPluginApi) {
|
|
96
|
+
api.on(
|
|
97
|
+
"before_tool_call",
|
|
98
|
+
(event: PluginHookBeforeToolCallEvent, ctx: PluginHookToolContext) => beforeToolCall(event, ctx, api),
|
|
99
|
+
{ priority: 90 },
|
|
100
|
+
);
|
|
101
|
+
api.logger.info(
|
|
102
|
+
"guard-scanner registered OpenClaw before_tool_call hook (stable: v2026.3.12, regression lane: v2026.3.8).",
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export default plugin;
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,55 +1,32 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
2
|
+
"id": "guard-scanner",
|
|
3
|
+
"name": "guard-scanner",
|
|
4
|
+
"description": "Runtime guard plugin for OpenClaw before_tool_call enforcement with capability-scoped policy rationale.",
|
|
5
|
+
"version": "16.0.0",
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"mode": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"enum": [
|
|
12
|
+
"monitor",
|
|
13
|
+
"enforce",
|
|
14
|
+
"strict"
|
|
15
|
+
],
|
|
16
|
+
"default": "enforce",
|
|
17
|
+
"description": "monitor: log only | enforce: block CRITICAL | strict: block HIGH+CRITICAL"
|
|
18
|
+
},
|
|
19
|
+
"auditLog": {
|
|
20
|
+
"type": "boolean",
|
|
21
|
+
"default": true,
|
|
22
|
+
"description": "Enable audit logging to ~/.openclaw/guard-scanner/audit.jsonl"
|
|
23
|
+
},
|
|
24
|
+
"customRules": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Path to custom rules JSON file (optional)"
|
|
27
|
+
}
|
|
22
28
|
},
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"type": "string",
|
|
28
|
-
"enum": [
|
|
29
|
-
"monitor",
|
|
30
|
-
"enforce",
|
|
31
|
-
"strict"
|
|
32
|
-
],
|
|
33
|
-
"default": "enforce",
|
|
34
|
-
"description": "monitor: log only | enforce: block CRITICAL | strict: block HIGH+CRITICAL"
|
|
35
|
-
},
|
|
36
|
-
"auditLog": {
|
|
37
|
-
"type": "boolean",
|
|
38
|
-
"default": true,
|
|
39
|
-
"description": "Enable audit logging to ~/.openclaw/guard-scanner/audit.jsonl"
|
|
40
|
-
},
|
|
41
|
-
"customRules": {
|
|
42
|
-
"type": "string",
|
|
43
|
-
"description": "Path to custom rules JSON file (optional)"
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
"required": [],
|
|
47
|
-
"additionalProperties": false
|
|
48
|
-
},
|
|
49
|
-
"capabilities": {
|
|
50
|
-
"cli": true,
|
|
51
|
-
"runtimeGuard": true,
|
|
52
|
-
"sarif": true,
|
|
53
|
-
"cicd": true
|
|
54
|
-
}
|
|
55
|
-
}
|
|
29
|
+
"required": [],
|
|
30
|
+
"additionalProperties": false
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,24 +1,72 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guava-parity/guard-scanner",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "16.0.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
7
7
|
},
|
|
8
|
-
"description": "Agent Skill Security Scanner -
|
|
9
|
-
"openclaw
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
"description": "Agent Skill Security Scanner - ASI Sanctuary Enforcer (v16)",
|
|
9
|
+
"openclaw": {
|
|
10
|
+
"extensions": [
|
|
11
|
+
"./dist/openclaw-plugin.mjs"
|
|
12
|
+
]
|
|
12
13
|
},
|
|
13
|
-
"main": "
|
|
14
|
+
"main": "./dist/index.cjs",
|
|
15
|
+
"module": "./dist/index.mjs",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.mjs",
|
|
21
|
+
"require": "./dist/index.cjs",
|
|
22
|
+
"default": "./dist/index.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./plugin": {
|
|
25
|
+
"types": "./dist/openclaw-plugin.d.mts",
|
|
26
|
+
"import": "./dist/openclaw-plugin.mjs",
|
|
27
|
+
"require": "./dist/openclaw-plugin.cjs",
|
|
28
|
+
"default": "./dist/openclaw-plugin.mjs"
|
|
29
|
+
},
|
|
30
|
+
"./mcp": {
|
|
31
|
+
"types": "./dist/mcp-server.d.ts",
|
|
32
|
+
"import": "./dist/mcp-server.mjs",
|
|
33
|
+
"require": "./dist/mcp-server.cjs",
|
|
34
|
+
"default": "./dist/mcp-server.mjs"
|
|
35
|
+
},
|
|
36
|
+
"./types": {
|
|
37
|
+
"types": "./dist/types.d.ts",
|
|
38
|
+
"default": "./dist/types.d.ts"
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"sideEffects": [
|
|
43
|
+
"./dist/cli.cjs",
|
|
44
|
+
"./dist/openclaw-plugin.mjs",
|
|
45
|
+
"./dist/openclaw-plugin.cjs"
|
|
46
|
+
],
|
|
14
47
|
"bin": {
|
|
15
|
-
"guard-scanner": "
|
|
48
|
+
"guard-scanner": "dist/cli.cjs"
|
|
16
49
|
},
|
|
17
50
|
"scripts": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
51
|
+
"build": "tsup --config tsup.config.ts",
|
|
52
|
+
"build:plugin": "npm run build",
|
|
53
|
+
"benchmark": "tsx scripts/benchmark.ts --write-ledgers",
|
|
54
|
+
"check:upstream": "tsx scripts/check-openclaw-upstream.ts",
|
|
55
|
+
"check:tarball": "tsx scripts/validate-tarball.ts",
|
|
56
|
+
"scan": "tsx src/cli.ts",
|
|
57
|
+
"lint": "tsx scripts/lint.ts",
|
|
58
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
59
|
+
"release:gate": "npm run build && npm run benchmark && tsx scripts/generate-capabilities.ts && tsx scripts/release-gate.ts && tsx scripts/validate-tarball.ts",
|
|
60
|
+
"test": "npm run build && npm run benchmark && tsx scripts/generate-capabilities.ts && tsx scripts/verify-capabilities.ts && tsx scripts/test-quality-gate.ts && tsx --test test/*.test.ts",
|
|
61
|
+
"test:core": "tsx --test test/scanner.test.ts test/patterns.test.ts",
|
|
62
|
+
"test:contracts": "npm run build:plugin && tsx scripts/release-gate.ts && tsx --test test/finding-schema.test.ts test/mcp.test.ts test/e2e-mcp.test.ts test/openclaw-plugin-compat.test.ts test/stale-claims.test.ts test/openclaw-upstream-check.test.ts",
|
|
63
|
+
"test:corpus": "tsx scripts/corpus-metrics.ts --check",
|
|
64
|
+
"test:perf": "tsx scripts/perf-regression.ts",
|
|
65
|
+
"test:quality": "tsx scripts/test-quality-gate.ts",
|
|
66
|
+
"test:rust-parity": "tsx scripts/rust-parity.ts",
|
|
67
|
+
"sbom": "tsx scripts/generate-sbom.ts",
|
|
68
|
+
"sync:readme": "tsx scripts/generate-capabilities.ts && tsx scripts/generate-readme-metrics.ts && tsx scripts/generate-readme-stats.ts",
|
|
69
|
+
"prepack": "npm run build:plugin && npm run release:gate"
|
|
22
70
|
},
|
|
23
71
|
"keywords": [
|
|
24
72
|
"security",
|
|
@@ -47,20 +95,25 @@
|
|
|
47
95
|
},
|
|
48
96
|
"homepage": "https://github.com/koatora20/guard-scanner",
|
|
49
97
|
"files": [
|
|
50
|
-
"
|
|
98
|
+
"dist/",
|
|
51
99
|
"hooks/",
|
|
52
100
|
"docs/",
|
|
101
|
+
"openclaw-plugin.mts",
|
|
53
102
|
"openclaw.plugin.json",
|
|
54
103
|
"SKILL.md",
|
|
55
104
|
"SECURITY.md",
|
|
56
105
|
"README.md",
|
|
106
|
+
"README_ja.md",
|
|
57
107
|
"LICENSE"
|
|
58
108
|
],
|
|
59
109
|
"devDependencies": {
|
|
60
110
|
"@types/node": "^22.0.0",
|
|
111
|
+
"openclaw": "2026.3.12",
|
|
112
|
+
"tsx": "^4.20.5",
|
|
113
|
+
"tsup": "^8.5.0",
|
|
61
114
|
"typescript": "^5.7.0"
|
|
62
115
|
},
|
|
63
116
|
"dependencies": {
|
|
64
117
|
"ws": "^8.19.0"
|
|
65
118
|
}
|
|
66
|
-
}
|
|
119
|
+
}
|