@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/CLAUDE.md +112 -0
- package/README.md +149 -0
- package/SKILL.md +129 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +466 -0
- package/dist/index.js.map +1 -0
- package/dist/src/cli-wrapper.d.ts +18 -0
- package/dist/src/cli-wrapper.d.ts.map +1 -0
- package/dist/src/cli-wrapper.js +151 -0
- package/dist/src/cli-wrapper.js.map +1 -0
- package/dist/src/types.d.ts +48 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/index.ts +499 -0
- package/openclaw.plugin.json +52 -0
- package/package.json +44 -0
- package/src/cli-wrapper.ts +177 -0
- package/src/openclaw-sdk.d.ts +84 -0
- package/src/types.ts +53 -0
- package/tsconfig.json +18 -0
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
|
+
}
|