mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 07:01:24 +00:00
security: add skill/plugin code safety scanner (#9806)
* security: add skill/plugin code safety scanner module * security: integrate skill scanner into security audit * security: add pre-install code safety scan for plugins * style: fix curly brace lint errors in skill-scanner.ts * docs: add changelog entry for skill code safety scanner * style: append ellipsis to truncated evidence strings * fix(security): harden plugin code safety scanning * fix: scan skills on install and report code-safety details * fix: dedupe audit-extra import * fix(security): make code safety scan failures observable * fix(test): stabilize smoke + gateway timeouts (#9806) (thanks @abdelsfane) --------- Co-authored-by: Darshil <ddhameliya@mail.sfsu.edu> Co-authored-by: Darshil <81693876+dvrshil@users.noreply.github.com> Co-authored-by: George Pickett <gpickett00@gmail.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveBrewExecutable } from "../infra/brew.js";
|
||||
import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { scanDirectoryWithSummary } from "../security/skill-scanner.js";
|
||||
import { CONFIG_DIR, ensureDir, resolveUserPath } from "../utils.js";
|
||||
import {
|
||||
hasBinary,
|
||||
@@ -32,6 +33,7 @@ export type SkillInstallResult = {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number | null;
|
||||
warnings?: string[];
|
||||
};
|
||||
|
||||
function isNodeReadableStream(value: unknown): value is NodeJS.ReadableStream {
|
||||
@@ -77,6 +79,57 @@ function formatInstallFailureMessage(result: {
|
||||
return `Install failed (${code}): ${summary}`;
|
||||
}
|
||||
|
||||
function withWarnings(result: SkillInstallResult, warnings: string[]): SkillInstallResult {
|
||||
if (warnings.length === 0) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
warnings: warnings.slice(),
|
||||
};
|
||||
}
|
||||
|
||||
function formatScanFindingDetail(
|
||||
rootDir: string,
|
||||
finding: { message: string; file: string; line: number },
|
||||
): string {
|
||||
const relativePath = path.relative(rootDir, finding.file);
|
||||
const filePath =
|
||||
relativePath && relativePath !== "." && !relativePath.startsWith("..")
|
||||
? relativePath
|
||||
: path.basename(finding.file);
|
||||
return `${finding.message} (${filePath}:${finding.line})`;
|
||||
}
|
||||
|
||||
async function collectSkillInstallScanWarnings(entry: SkillEntry): Promise<string[]> {
|
||||
const warnings: string[] = [];
|
||||
const skillName = entry.skill.name;
|
||||
const skillDir = path.resolve(entry.skill.baseDir);
|
||||
|
||||
try {
|
||||
const summary = await scanDirectoryWithSummary(skillDir);
|
||||
if (summary.critical > 0) {
|
||||
const criticalDetails = summary.findings
|
||||
.filter((finding) => finding.severity === "critical")
|
||||
.map((finding) => formatScanFindingDetail(skillDir, finding))
|
||||
.join("; ");
|
||||
warnings.push(
|
||||
`WARNING: Skill "${skillName}" contains dangerous code patterns: ${criticalDetails}`,
|
||||
);
|
||||
} else if (summary.warn > 0) {
|
||||
warnings.push(
|
||||
`Skill "${skillName}" has ${summary.warn} suspicious code pattern(s). Run "openclaw security audit --deep" for details.`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
warnings.push(
|
||||
`Skill "${skillName}" code safety scan failed (${String(err)}). Installation continues; run "openclaw security audit --deep" after install.`,
|
||||
);
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
function resolveInstallId(spec: SkillInstallSpec, index: number): string {
|
||||
return (spec.id ?? `${spec.kind}-${index}`).trim();
|
||||
}
|
||||
@@ -356,40 +409,51 @@ export async function installSkill(params: SkillInstallRequest): Promise<SkillIn
|
||||
}
|
||||
|
||||
const spec = findInstallSpec(entry, params.installId);
|
||||
const warnings = await collectSkillInstallScanWarnings(entry);
|
||||
if (!spec) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `Installer not found: ${params.installId}`,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
};
|
||||
return withWarnings(
|
||||
{
|
||||
ok: false,
|
||||
message: `Installer not found: ${params.installId}`,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
},
|
||||
warnings,
|
||||
);
|
||||
}
|
||||
if (spec.kind === "download") {
|
||||
return await installDownloadSpec({ entry, spec, timeoutMs });
|
||||
const downloadResult = await installDownloadSpec({ entry, spec, timeoutMs });
|
||||
return withWarnings(downloadResult, warnings);
|
||||
}
|
||||
|
||||
const prefs = resolveSkillsInstallPreferences(params.config);
|
||||
const command = buildInstallCommand(spec, prefs);
|
||||
if (command.error) {
|
||||
return {
|
||||
ok: false,
|
||||
message: command.error,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
};
|
||||
return withWarnings(
|
||||
{
|
||||
ok: false,
|
||||
message: command.error,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
},
|
||||
warnings,
|
||||
);
|
||||
}
|
||||
|
||||
const brewExe = hasBinary("brew") ? "brew" : resolveBrewExecutable();
|
||||
if (spec.kind === "brew" && !brewExe) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "brew not installed",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
};
|
||||
return withWarnings(
|
||||
{
|
||||
ok: false,
|
||||
message: "brew not installed",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
},
|
||||
warnings,
|
||||
);
|
||||
}
|
||||
if (spec.kind === "uv" && !hasBinary("uv")) {
|
||||
if (brewExe) {
|
||||
@@ -397,32 +461,41 @@ export async function installSkill(params: SkillInstallRequest): Promise<SkillIn
|
||||
timeoutMs,
|
||||
});
|
||||
if (brewResult.code !== 0) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "Failed to install uv (brew)",
|
||||
stdout: brewResult.stdout.trim(),
|
||||
stderr: brewResult.stderr.trim(),
|
||||
code: brewResult.code,
|
||||
};
|
||||
return withWarnings(
|
||||
{
|
||||
ok: false,
|
||||
message: "Failed to install uv (brew)",
|
||||
stdout: brewResult.stdout.trim(),
|
||||
stderr: brewResult.stderr.trim(),
|
||||
code: brewResult.code,
|
||||
},
|
||||
warnings,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
ok: false,
|
||||
message: "uv not installed (install via brew)",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
};
|
||||
return withWarnings(
|
||||
{
|
||||
ok: false,
|
||||
message: "uv not installed (install via brew)",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
},
|
||||
warnings,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!command.argv || command.argv.length === 0) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "invalid install command",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
};
|
||||
return withWarnings(
|
||||
{
|
||||
ok: false,
|
||||
message: "invalid install command",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
},
|
||||
warnings,
|
||||
);
|
||||
}
|
||||
|
||||
if (spec.kind === "brew" && brewExe && command.argv[0] === "brew") {
|
||||
@@ -435,22 +508,28 @@ export async function installSkill(params: SkillInstallRequest): Promise<SkillIn
|
||||
timeoutMs,
|
||||
});
|
||||
if (brewResult.code !== 0) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "Failed to install go (brew)",
|
||||
stdout: brewResult.stdout.trim(),
|
||||
stderr: brewResult.stderr.trim(),
|
||||
code: brewResult.code,
|
||||
};
|
||||
return withWarnings(
|
||||
{
|
||||
ok: false,
|
||||
message: "Failed to install go (brew)",
|
||||
stdout: brewResult.stdout.trim(),
|
||||
stderr: brewResult.stderr.trim(),
|
||||
code: brewResult.code,
|
||||
},
|
||||
warnings,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
ok: false,
|
||||
message: "go not installed (install via brew)",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
};
|
||||
return withWarnings(
|
||||
{
|
||||
ok: false,
|
||||
message: "go not installed (install via brew)",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
},
|
||||
warnings,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,11 +558,14 @@ export async function installSkill(params: SkillInstallRequest): Promise<SkillIn
|
||||
})();
|
||||
|
||||
const success = result.code === 0;
|
||||
return {
|
||||
ok: success,
|
||||
message: success ? "Installed" : formatInstallFailureMessage(result),
|
||||
stdout: result.stdout.trim(),
|
||||
stderr: result.stderr.trim(),
|
||||
code: result.code,
|
||||
};
|
||||
return withWarnings(
|
||||
{
|
||||
ok: success,
|
||||
message: success ? "Installed" : formatInstallFailureMessage(result),
|
||||
stdout: result.stdout.trim(),
|
||||
stderr: result.stderr.trim(),
|
||||
code: result.code,
|
||||
},
|
||||
warnings,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user