@exelerus/openclaw-vexscan 1.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/index.ts ADDED
@@ -0,0 +1,499 @@
1
+ import { Type, type Static } from "@sinclair/typebox";
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
+ import { execCommand, execVexscan, findVexscan, installVexscan } from "./src/cli-wrapper.js";
4
+ import type { ScanResult, VetResult } from "./src/types.js";
5
+
6
+ // --- Config schema (TypeBox) ---
7
+
8
+ const ConfigSchema = Type.Object({
9
+ enabled: Type.Boolean({ default: true }),
10
+ scanOnInstall: Type.Boolean({ default: true }),
11
+ minSeverity: Type.Union(
12
+ [
13
+ Type.Literal("info"),
14
+ Type.Literal("low"),
15
+ Type.Literal("medium"),
16
+ Type.Literal("high"),
17
+ Type.Literal("critical"),
18
+ ],
19
+ { default: "medium" },
20
+ ),
21
+ thirdPartyOnly: Type.Boolean({ default: true }),
22
+ skipDeps: Type.Boolean({ default: true }),
23
+ ast: Type.Boolean({ default: true }),
24
+ deps: Type.Boolean({ default: true }),
25
+ cliPath: Type.Optional(Type.String()),
26
+ });
27
+
28
+ type Config = Static<typeof ConfigSchema>;
29
+
30
+ // --- Tool parameter schema ---
31
+
32
+ const VexscanToolSchema = Type.Union([
33
+ Type.Object({
34
+ action: Type.Literal("scan"),
35
+ path: Type.Optional(Type.String({ description: "Path to scan (defaults to extensions dir)" })),
36
+ thirdPartyOnly: Type.Optional(Type.Boolean({ description: "Only scan third-party extensions" })),
37
+ }),
38
+ Type.Object({
39
+ action: Type.Literal("vet"),
40
+ source: Type.String({ description: "GitHub URL or local path to vet" }),
41
+ branch: Type.Optional(Type.String({ description: "Git branch to check" })),
42
+ }),
43
+ Type.Object({
44
+ action: Type.Literal("install"),
45
+ source: Type.String({ description: "npm spec, local path, or GitHub URL to vet and install" }),
46
+ branch: Type.Optional(Type.String({ description: "Git branch to check" })),
47
+ force: Type.Optional(Type.Boolean({ description: "Allow medium severity findings" })),
48
+ allowHigh: Type.Optional(Type.Boolean({ description: "Allow high severity findings (dangerous)" })),
49
+ link: Type.Optional(Type.Boolean({ description: "Symlink instead of copy (for development)" })),
50
+ }),
51
+ Type.Object({
52
+ action: Type.Literal("status"),
53
+ }),
54
+ ]);
55
+
56
+ // --- Helpers ---
57
+
58
+ function parseConfig(value: unknown): Config {
59
+ const raw = (value && typeof value === "object" ? value : {}) as Record<string, unknown>;
60
+ return {
61
+ enabled: raw.enabled !== false,
62
+ scanOnInstall: raw.scanOnInstall !== false,
63
+ minSeverity: (raw.minSeverity as Config["minSeverity"]) || "medium",
64
+ thirdPartyOnly: raw.thirdPartyOnly !== false,
65
+ skipDeps: raw.skipDeps !== false,
66
+ ast: raw.ast !== false,
67
+ deps: raw.deps !== false,
68
+ cliPath: typeof raw.cliPath === "string" ? raw.cliPath : undefined,
69
+ };
70
+ }
71
+
72
+ function getVerdictMessage(verdict: string, findings: number, maxSeverity: string | null): string {
73
+ switch (verdict) {
74
+ case "clean":
75
+ return "No security issues found";
76
+ case "warnings":
77
+ return `Found ${findings} issue(s) with max severity: ${maxSeverity}. Review recommended.`;
78
+ case "high_risk":
79
+ return `Found ${findings} HIGH severity issue(s). Review carefully before installing.`;
80
+ case "dangerous":
81
+ return `Found ${findings} CRITICAL issue(s). Do NOT install without thorough review.`;
82
+ default:
83
+ return `Found ${findings} issue(s)`;
84
+ }
85
+ }
86
+
87
+ const SEVERITY_RANK: Record<string, number> = {
88
+ info: 0,
89
+ low: 1,
90
+ medium: 2,
91
+ high: 3,
92
+ critical: 4,
93
+ };
94
+
95
+ function checkInstallGate(
96
+ maxSeverity: string | null | undefined,
97
+ force: boolean,
98
+ allowHigh: boolean,
99
+ ): { allowed: boolean; reason?: string } {
100
+ const rank = SEVERITY_RANK[maxSeverity ?? ""] ?? -1;
101
+
102
+ if (rank >= SEVERITY_RANK.critical) {
103
+ return { allowed: false, reason: "CRITICAL severity findings — installation blocked. Cannot override." };
104
+ }
105
+ if (rank >= SEVERITY_RANK.high && !allowHigh) {
106
+ return { allowed: false, reason: "HIGH severity findings — installation blocked. Use allowHigh/--allow-high to override." };
107
+ }
108
+ if (rank >= SEVERITY_RANK.medium && !force) {
109
+ return { allowed: false, reason: "MEDIUM severity findings — installation blocked. Use force/--force to override." };
110
+ }
111
+ return { allowed: true };
112
+ }
113
+
114
+ // --- Plugin ---
115
+
116
+ const vexscanPlugin = {
117
+ id: "openclaw-vexscan",
118
+ name: "Vexscan Security Scanner",
119
+ description: "Security scanner for OpenClaw extensions, skills, and configurations",
120
+ kind: "tool" as const,
121
+ configSchema: ConfigSchema,
122
+
123
+ register(api: OpenClawPluginApi) {
124
+ const config = parseConfig(api.pluginConfig);
125
+ let cliPath: string | null = null;
126
+
127
+ const ensureCli = async (): Promise<string> => {
128
+ if (cliPath) return cliPath;
129
+
130
+ if (config.cliPath) {
131
+ cliPath = config.cliPath;
132
+ return cliPath;
133
+ }
134
+
135
+ const found = await findVexscan();
136
+ if (found) {
137
+ cliPath = found;
138
+ return cliPath;
139
+ }
140
+
141
+ api.logger.info("[vexscan] CLI not found, attempting auto-install...");
142
+ const installed = await installVexscan();
143
+ if (installed) {
144
+ cliPath = installed;
145
+ api.logger.info(`[vexscan] CLI installed to ${cliPath}`);
146
+ return cliPath;
147
+ }
148
+
149
+ throw new Error(
150
+ "Vexscan CLI not found. Install with: curl -fsSL https://raw.githubusercontent.com/edimuj/vexscan/main/install.sh | bash",
151
+ );
152
+ };
153
+
154
+ // Register the security scanner tool
155
+ api.registerTool({
156
+ name: "vexscan",
157
+ description:
158
+ "Scan extensions and code for security threats including prompt injection, malicious code, obfuscation, and data exfiltration.",
159
+ parameters: VexscanToolSchema,
160
+
161
+ async execute(_toolCallId: string, params: Record<string, unknown>) {
162
+ const json = (payload: unknown) => ({
163
+ content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
164
+ details: payload,
165
+ });
166
+
167
+ try {
168
+ if (!config.enabled) {
169
+ return json({ ok: false, error: "Vexscan is disabled in plugin config" });
170
+ }
171
+
172
+ const cli = await ensureCli();
173
+
174
+ if (params.action === "status") {
175
+ return json({
176
+ ok: true,
177
+ enabled: config.enabled,
178
+ cliPath: cli,
179
+ config: {
180
+ minSeverity: config.minSeverity,
181
+ thirdPartyOnly: config.thirdPartyOnly,
182
+ skipDeps: config.skipDeps,
183
+ ast: config.ast,
184
+ deps: config.deps,
185
+ scanOnInstall: config.scanOnInstall,
186
+ },
187
+ });
188
+ }
189
+
190
+ if (params.action === "scan") {
191
+ const scanPath = (params.path as string) || "~/.openclaw/extensions";
192
+ const args = ["scan", scanPath, "-f", "json", "--min-severity", config.minSeverity];
193
+
194
+ if (config.ast) args.push("--ast");
195
+ if (config.deps) args.push("--deps");
196
+ if (config.skipDeps) args.push("--skip-deps");
197
+ if ((params.thirdPartyOnly as boolean | undefined) ?? config.thirdPartyOnly) {
198
+ args.push("--third-party-only");
199
+ }
200
+
201
+ const result = await execVexscan(cli, args);
202
+ const parsed = JSON.parse(result.stdout) as ScanResult;
203
+
204
+ return json({
205
+ ok: true,
206
+ findings: parsed.total_findings || 0,
207
+ maxSeverity: parsed.max_severity || null,
208
+ summary: parsed.findings_by_severity || {},
209
+ scanTime: parsed.total_time_ms,
210
+ });
211
+ }
212
+
213
+ if (params.action === "vet") {
214
+ const args = ["vet", params.source as string, "-f", "json"];
215
+ if (config.ast) args.push("--ast");
216
+ if (config.deps) args.push("--deps");
217
+ if (config.skipDeps) args.push("--skip-deps");
218
+ if (params.branch) {
219
+ args.push("--branch", params.branch as string);
220
+ }
221
+
222
+ const result = await execVexscan(cli, args);
223
+ const parsed = JSON.parse(result.stdout) as VetResult;
224
+
225
+ let verdict: string;
226
+ if (parsed.total_findings === 0) {
227
+ verdict = "clean";
228
+ } else if (parsed.max_severity === "critical") {
229
+ verdict = "dangerous";
230
+ } else if (parsed.max_severity === "high") {
231
+ verdict = "high_risk";
232
+ } else {
233
+ verdict = "warnings";
234
+ }
235
+
236
+ return json({
237
+ ok: true,
238
+ verdict,
239
+ findings: parsed.total_findings || 0,
240
+ maxSeverity: parsed.max_severity || null,
241
+ message: getVerdictMessage(verdict, parsed.total_findings || 0, parsed.max_severity ?? null),
242
+ });
243
+ }
244
+
245
+ if (params.action === "install") {
246
+ // Step 1: Vet the source
247
+ const vetArgs = ["vet", params.source as string, "-f", "json"];
248
+ if (config.ast) vetArgs.push("--ast");
249
+ if (config.deps) vetArgs.push("--deps");
250
+ if (config.skipDeps) vetArgs.push("--skip-deps");
251
+ if (params.branch) vetArgs.push("--branch", params.branch as string);
252
+
253
+ const vetResult = await execVexscan(cli, vetArgs);
254
+ const parsed = JSON.parse(vetResult.stdout) as VetResult;
255
+
256
+ // Step 2: Check severity gate
257
+ const gate = checkInstallGate(
258
+ parsed.max_severity,
259
+ (params.force as boolean) || false,
260
+ (params.allowHigh as boolean) || false,
261
+ );
262
+
263
+ if (!gate.allowed) {
264
+ let verdict = "warnings";
265
+ if (parsed.max_severity === "critical") verdict = "dangerous";
266
+ else if (parsed.max_severity === "high") verdict = "high_risk";
267
+
268
+ return json({
269
+ ok: false,
270
+ action: "install_blocked",
271
+ verdict,
272
+ findings: parsed.total_findings || 0,
273
+ maxSeverity: parsed.max_severity || null,
274
+ reason: gate.reason,
275
+ message: getVerdictMessage(verdict, parsed.total_findings || 0, parsed.max_severity ?? null),
276
+ });
277
+ }
278
+
279
+ // Step 3: Install via openclaw
280
+ const installArgs = ["plugins", "install"];
281
+ if (params.link) installArgs.push("-l");
282
+ installArgs.push(params.source as string);
283
+
284
+ const installResult = await execCommand("openclaw", installArgs);
285
+ if (installResult.exitCode !== 0) {
286
+ return json({
287
+ ok: false,
288
+ error: `Installation failed: ${installResult.stderr || installResult.stdout}`.trim(),
289
+ });
290
+ }
291
+
292
+ return json({
293
+ ok: true,
294
+ action: "installed",
295
+ source: params.source,
296
+ findings: parsed.total_findings || 0,
297
+ maxSeverity: parsed.max_severity || null,
298
+ message: parsed.total_findings
299
+ ? `Installed with ${parsed.total_findings} low-severity finding(s). Review recommended.`
300
+ : "Installed — no security issues found.",
301
+ installOutput: installResult.stdout.trim(),
302
+ });
303
+ }
304
+
305
+ return json({ ok: false, error: "Unknown action" });
306
+ } catch (err) {
307
+ return json({
308
+ ok: false,
309
+ error: err instanceof Error ? err.message : String(err),
310
+ });
311
+ }
312
+ },
313
+ });
314
+
315
+ // Register CLI commands
316
+ api.registerCli(
317
+ ({ program }) => {
318
+ const vexscan = program.command("vexscan").description("Security scanner for extensions");
319
+
320
+ vexscan
321
+ .command("scan [path]")
322
+ .description("Scan extensions for security issues")
323
+ .option("-f, --format <format>", "Output format (cli, json, sarif, markdown)", "cli")
324
+ .option("--third-party-only", "Only scan third-party extensions")
325
+ .option("--min-severity <level>", "Minimum severity to report", config.minSeverity)
326
+ .action(async (path: string | undefined, opts: any) => {
327
+ try {
328
+ const cli = await ensureCli();
329
+ const scanPath = path || "~/.openclaw/extensions";
330
+ const args = ["scan", scanPath, "-f", opts.format, "--min-severity", opts.minSeverity];
331
+ if (config.ast) args.push("--ast");
332
+ if (config.deps) args.push("--deps");
333
+ if (config.skipDeps) args.push("--skip-deps");
334
+ if (opts.thirdPartyOnly) args.push("--third-party-only");
335
+
336
+ const result = await execVexscan(cli, args);
337
+ console.log(result.stdout);
338
+ if (result.stderr) console.error(result.stderr);
339
+ } catch (err) {
340
+ console.error("Error:", err instanceof Error ? err.message : err);
341
+ process.exit(1);
342
+ }
343
+ });
344
+
345
+ vexscan
346
+ .command("vet <source>")
347
+ .description("Vet an extension before installing")
348
+ .option("-f, --format <format>", "Output format (cli, json)", "cli")
349
+ .option("--branch <branch>", "Git branch to check")
350
+ .option("--keep", "Keep cloned repo after scan")
351
+ .action(async (source: string, opts: any) => {
352
+ try {
353
+ const cli = await ensureCli();
354
+ const args = ["vet", source, "-f", opts.format];
355
+ if (config.ast) args.push("--ast");
356
+ if (config.deps) args.push("--deps");
357
+ if (config.skipDeps) args.push("--skip-deps");
358
+ if (opts.branch) args.push("--branch", opts.branch);
359
+ if (opts.keep) args.push("--keep");
360
+
361
+ const result = await execVexscan(cli, args);
362
+ console.log(result.stdout);
363
+ if (result.stderr) console.error(result.stderr);
364
+ } catch (err) {
365
+ console.error("Error:", err instanceof Error ? err.message : err);
366
+ process.exit(1);
367
+ }
368
+ });
369
+
370
+ vexscan
371
+ .command("install <source>")
372
+ .description("Vet an extension and install if it passes")
373
+ .option("-f, --format <format>", "Output format for vet report (cli, json)", "cli")
374
+ .option("--branch <branch>", "Git branch to check")
375
+ .option("-l, --link", "Symlink instead of copy (for development)")
376
+ .option("--force", "Allow medium severity findings")
377
+ .option("--allow-high", "Allow high severity findings (dangerous)")
378
+ .option("--dry-run", "Vet only, show what would be installed")
379
+ .action(async (source: string, opts: any) => {
380
+ try {
381
+ const cli = await ensureCli();
382
+
383
+ // Step 1: Vet
384
+ console.log(`Vetting ${source}...`);
385
+ const vetArgs = ["vet", source, "-f", "json"];
386
+ if (config.ast) vetArgs.push("--ast");
387
+ if (config.deps) vetArgs.push("--deps");
388
+ if (config.skipDeps) vetArgs.push("--skip-deps");
389
+ if (opts.branch) vetArgs.push("--branch", opts.branch);
390
+
391
+ const vetResult = await execVexscan(cli, vetArgs);
392
+ const parsed = JSON.parse(vetResult.stdout) as VetResult;
393
+
394
+ // Show vet report in requested format
395
+ if (opts.format !== "json") {
396
+ const reportArgs = ["vet", source, "-f", opts.format];
397
+ if (config.ast) reportArgs.push("--ast");
398
+ if (config.deps) reportArgs.push("--deps");
399
+ if (config.skipDeps) reportArgs.push("--skip-deps");
400
+ if (opts.branch) reportArgs.push("--branch", opts.branch);
401
+ const report = await execVexscan(cli, reportArgs);
402
+ console.log(report.stdout);
403
+ } else {
404
+ console.log(vetResult.stdout);
405
+ }
406
+
407
+ // Step 2: Check severity gate
408
+ const gate = checkInstallGate(parsed.max_severity, opts.force, opts.allowHigh);
409
+ if (!gate.allowed) {
410
+ console.error(`\nInstallation blocked: ${gate.reason}`);
411
+ process.exit(1);
412
+ }
413
+
414
+ if (opts.dryRun) {
415
+ console.log("\n[dry-run] Would install:", source);
416
+ return;
417
+ }
418
+
419
+ // Step 3: Install
420
+ console.log("\nSecurity check passed. Installing...");
421
+ const installArgs = ["plugins", "install"];
422
+ if (opts.link) installArgs.push("-l");
423
+ installArgs.push(source);
424
+
425
+ const installResult = await execCommand("openclaw", installArgs);
426
+ if (installResult.exitCode !== 0) {
427
+ console.error("Installation failed:", installResult.stderr || installResult.stdout);
428
+ process.exit(1);
429
+ }
430
+ console.log(installResult.stdout.trim());
431
+ } catch (err) {
432
+ console.error("Error:", err instanceof Error ? err.message : err);
433
+ process.exit(1);
434
+ }
435
+ });
436
+
437
+ vexscan
438
+ .command("rules")
439
+ .description("List detection rules")
440
+ .option("--json", "Output as JSON")
441
+ .option("--rule <id>", "Show specific rule")
442
+ .action(async (opts: any) => {
443
+ try {
444
+ const cli = await ensureCli();
445
+ const args = ["rules"];
446
+ if (opts.json) args.push("--json");
447
+ if (opts.rule) args.push("--rule", opts.rule);
448
+
449
+ const result = await execVexscan(cli, args);
450
+ console.log(result.stdout);
451
+ } catch (err) {
452
+ console.error("Error:", err instanceof Error ? err.message : err);
453
+ process.exit(1);
454
+ }
455
+ });
456
+ },
457
+ { commands: ["vexscan"] },
458
+ );
459
+
460
+ // Register startup service for initial scan
461
+ if (config.scanOnInstall) {
462
+ api.registerService({
463
+ id: "vexscan-startup",
464
+ start: async () => {
465
+ if (!config.enabled) return;
466
+
467
+ try {
468
+ const cli = await ensureCli();
469
+ api.logger.info("[vexscan] Running startup security scan...");
470
+
471
+ const args = ["scan", "~/.openclaw/extensions", "-f", "json", "--min-severity", "high"];
472
+ if (config.ast) args.push("--ast");
473
+ if (config.deps) args.push("--deps");
474
+ if (config.skipDeps) args.push("--skip-deps");
475
+ if (config.thirdPartyOnly) args.push("--third-party-only");
476
+
477
+ const result = await execVexscan(cli, args);
478
+ const parsed = JSON.parse(result.stdout) as ScanResult;
479
+
480
+ if (parsed.total_findings && parsed.total_findings > 0) {
481
+ api.logger.warn(
482
+ `[vexscan] Security scan found ${parsed.total_findings} issue(s) with max severity: ${parsed.max_severity}`,
483
+ );
484
+ } else {
485
+ api.logger.info("[vexscan] Security scan complete - no issues found");
486
+ }
487
+ } catch (err) {
488
+ api.logger.error(
489
+ `[vexscan] Startup scan failed: ${err instanceof Error ? err.message : err}`,
490
+ );
491
+ }
492
+ },
493
+ stop: async () => {},
494
+ });
495
+ }
496
+ },
497
+ };
498
+
499
+ export default vexscanPlugin;
@@ -0,0 +1,52 @@
1
+ {
2
+ "id": "openclaw-vexscan",
3
+ "name": "Vexscan Security Scanner",
4
+ "kind": "tool",
5
+ "description": "Security scanner for OpenClaw extensions, skills, and configurations. Detects prompt injection, malicious code, obfuscation, and data exfiltration attempts.",
6
+ "configSchema": {
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "enabled": {
11
+ "type": "boolean",
12
+ "default": true,
13
+ "description": "Enable automatic security scanning"
14
+ },
15
+ "scanOnInstall": {
16
+ "type": "boolean",
17
+ "default": true,
18
+ "description": "Scan new extensions when installed"
19
+ },
20
+ "minSeverity": {
21
+ "type": "string",
22
+ "enum": ["info", "low", "medium", "high", "critical"],
23
+ "default": "medium",
24
+ "description": "Minimum severity to report"
25
+ },
26
+ "thirdPartyOnly": {
27
+ "type": "boolean",
28
+ "default": true,
29
+ "description": "Only scan third-party (non-official) extensions"
30
+ },
31
+ "skipDeps": {
32
+ "type": "boolean",
33
+ "default": true,
34
+ "description": "Skip node_modules to reduce false positives"
35
+ },
36
+ "ast": {
37
+ "type": "boolean",
38
+ "default": true,
39
+ "description": "Enable AST analysis for obfuscation detection"
40
+ },
41
+ "deps": {
42
+ "type": "boolean",
43
+ "default": true,
44
+ "description": "Enable dependency scanning for supply chain attacks"
45
+ },
46
+ "cliPath": {
47
+ "type": "string",
48
+ "description": "Path to vexscan CLI binary (auto-detected if not set)"
49
+ }
50
+ }
51
+ }
52
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@exelerus/openclaw-vexscan",
3
+ "version": "1.0.0",
4
+ "description": "Vexscan security scanner plugin for OpenClaw",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "openclaw": {
9
+ "extensions": ["./index.ts"],
10
+ "slots": ["tool"]
11
+ },
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "watch": "tsc --watch",
15
+ "clean": "rm -rf dist"
16
+ },
17
+ "keywords": [
18
+ "openclaw",
19
+ "plugin",
20
+ "security",
21
+ "scanner",
22
+ "vexscan"
23
+ ],
24
+ "author": "Vexscan Contributors",
25
+ "license": "Apache-2.0",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/edimuj/vexscan.git",
29
+ "directory": "plugins/openclaw"
30
+ },
31
+ "devDependencies": {
32
+ "@sinclair/typebox": "^0.32.0",
33
+ "@types/node": "^25.2.0",
34
+ "typescript": "^5.0.0"
35
+ },
36
+ "peerDependencies": {
37
+ "openclaw": ">=2026.1.26"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "openclaw": {
41
+ "optional": true
42
+ }
43
+ }
44
+ }