mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
fix(security): remove VirusTotal from ClawScan workers (#3156)
This commit is contained in:
@@ -86,7 +86,7 @@ jobs:
|
||||
- name: Install ClawScan CLI
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm install -g @openclaw/clawscan@0.1.5
|
||||
npm install -g @openclaw/clawscan@0.1.6
|
||||
clawscan --version
|
||||
|
||||
- name: Run pre-publication publish worker
|
||||
@@ -94,6 +94,5 @@ jobs:
|
||||
CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
SECURITY_SCAN_WORKER_TOKEN: ${{ secrets.SECURITY_SCAN_WORKER_TOKEN }}
|
||||
VIRUSTOTAL_API_KEY: ${{ secrets.VT_API_KEY }}
|
||||
run: |
|
||||
bun run publish:prepublication-worker
|
||||
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
- name: Install ClawScan CLI
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm install -g @openclaw/clawscan@0.1.5
|
||||
npm install -g @openclaw/clawscan@0.1.6
|
||||
clawscan --version
|
||||
|
||||
- name: Install SkillSpector
|
||||
|
||||
@@ -126,7 +126,7 @@ describe("pre-publication publish worker workflow", () => {
|
||||
expect(runStep?.run).not.toContain("--version");
|
||||
expect(runStep?.run).not.toContain("--max-jobs");
|
||||
expect(steps.find((step) => step.name === "Install ClawScan CLI")?.run).toContain(
|
||||
"npm install -g @openclaw/clawscan@0.1.5",
|
||||
"npm install -g @openclaw/clawscan@0.1.6",
|
||||
);
|
||||
expect(steps.find((step) => step.name === "Install Codex CLI")?.run).toContain(
|
||||
"npm install -g @openai/codex@0.142.3",
|
||||
@@ -138,7 +138,6 @@ describe("pre-publication publish worker workflow", () => {
|
||||
CODEX_API_KEY: "${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }}",
|
||||
OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}",
|
||||
SECURITY_SCAN_WORKER_TOKEN: "${{ secrets.SECURITY_SCAN_WORKER_TOKEN }}",
|
||||
VIRUSTOTAL_API_KEY: "${{ secrets.VT_API_KEY }}",
|
||||
});
|
||||
|
||||
for (const step of steps) {
|
||||
@@ -152,9 +151,7 @@ describe("pre-publication publish worker workflow", () => {
|
||||
expect(stepUsesSecret(step, "OPENAI_API_KEY"), stepName).toBe(
|
||||
stepName === "Run pre-publication publish worker",
|
||||
);
|
||||
expect(stepUsesSecret(step, "VT_API_KEY"), stepName).toBe(
|
||||
stepName === "Run pre-publication publish worker",
|
||||
);
|
||||
expect(stepUsesSecret(step, "VT_API_KEY"), stepName).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -161,14 +161,13 @@ function clawScanArtifactJson(options?: {
|
||||
completedAt?: string;
|
||||
includeCompletedAt?: boolean;
|
||||
judgeResult?: Record<string, unknown>;
|
||||
scannerStatuses?: Partial<Record<"clawscan-static" | "skillspector" | "virustotal", string>>;
|
||||
scannerStatuses?: Partial<Record<"clawscan-static" | "skillspector", string>>;
|
||||
verdict?: ClawScanVerdict;
|
||||
}) {
|
||||
const verdict = options?.verdict ?? "benign";
|
||||
const scannerStatuses = {
|
||||
"clawscan-static": "completed",
|
||||
skillspector: "completed",
|
||||
virustotal: "completed",
|
||||
...options?.scannerStatuses,
|
||||
};
|
||||
const artifact: Record<string, unknown> = {
|
||||
@@ -186,12 +185,6 @@ function clawScanArtifactJson(options?: {
|
||||
issues: [{ id: "SDI-1", severity: "HIGH", explanation: "test finding" }],
|
||||
},
|
||||
},
|
||||
virustotal: {
|
||||
status: scannerStatuses.virustotal,
|
||||
raw: {
|
||||
status: scannerStatuses.virustotal === "skipped" ? "skipped" : "clean",
|
||||
},
|
||||
},
|
||||
"clawscan-static": {
|
||||
status: scannerStatuses["clawscan-static"],
|
||||
raw: {
|
||||
@@ -277,7 +270,6 @@ JSON`,
|
||||
const workspace = await tempDir();
|
||||
const fakeClawScan = join(workspace, "fake-clawscan");
|
||||
const argsLog = join(workspace, "clawscan-args.log");
|
||||
const virusTotalLog = join(workspace, "virustotal-input.json");
|
||||
const artifactJson = clawScanArtifactJson({ verdict });
|
||||
await writeFakeClawScanCommand(
|
||||
fakeClawScan,
|
||||
@@ -287,13 +279,9 @@ JSON`,
|
||||
fi
|
||||
printf '%s\n' "$@" > ${JSON.stringify(argsLog)}
|
||||
out=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--scanner-result)
|
||||
cp "\${2#virustotal=}" ${JSON.stringify(virusTotalLog)}
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--output)
|
||||
out="$2"
|
||||
shift 2
|
||||
;;
|
||||
@@ -348,15 +336,7 @@ JSON`,
|
||||
expect(invocationArgs).toContain("--profile");
|
||||
expect(invocationArgs).toContain("clawhub");
|
||||
expect(invocationArgs).not.toContain("--context");
|
||||
expect(invocationArgs).toContain("--scanner-result");
|
||||
const invocationLines = invocationArgs.trim().split("\n");
|
||||
const scannerResultIndex = invocationLines.indexOf("--scanner-result");
|
||||
const scannerResult = invocationLines[scannerResultIndex + 1];
|
||||
expect(scannerResult).toMatch(/^virustotal=/);
|
||||
expect(JSON.parse(await readFile(virusTotalLog, "utf8"))).toEqual({
|
||||
status: "completed",
|
||||
source: "cached-skill-version",
|
||||
});
|
||||
expect(invocationArgs).not.toContain("--scanner-result");
|
||||
} finally {
|
||||
if (previousCommand === undefined) delete process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND;
|
||||
else process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND = previousCommand;
|
||||
@@ -422,20 +402,15 @@ JSON`,
|
||||
const fakeClawScan = join(workspace, "fake-clawscan");
|
||||
const argsLog = join(workspace, "clawscan-args.log");
|
||||
const filesLog = join(workspace, "clawscan-files.log");
|
||||
const virusTotalLog = join(workspace, "virustotal-input.json");
|
||||
await writeFakeClawScanCommand(
|
||||
fakeClawScan,
|
||||
`target="$1"
|
||||
printf '%s\n' "$@" > ${JSON.stringify(argsLog)}
|
||||
find "$target" -type f -print | sort > ${JSON.stringify(filesLog)}
|
||||
out=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--scanner-result)
|
||||
cp "\${2#virustotal=}" ${JSON.stringify(virusTotalLog)}
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--output)
|
||||
out="$2"
|
||||
shift 2
|
||||
;;
|
||||
@@ -495,14 +470,7 @@ JSON`,
|
||||
expect.arrayContaining(["--profile", "clawhub", "--output"]),
|
||||
);
|
||||
expect(invocationArgs).not.toContain("--context");
|
||||
const scannerResultIndex = invocationArgs.indexOf("--scanner-result");
|
||||
expect(scannerResultIndex).toBeGreaterThan(-1);
|
||||
const scannerResult = invocationArgs[scannerResultIndex + 1];
|
||||
expect(scannerResult).toMatch(/^virustotal=/);
|
||||
expect(JSON.parse(await readFile(virusTotalLog, "utf8"))).toEqual({
|
||||
status: "completed",
|
||||
source: `${targetKind}-${source}`,
|
||||
});
|
||||
expect(invocationArgs).not.toContain("--scanner-result");
|
||||
expect((await readFile(filesLog, "utf8")).trim().split("\n")).toContain(expectedFile);
|
||||
} finally {
|
||||
if (previousCommand === undefined) delete process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND;
|
||||
@@ -511,69 +479,6 @@ JSON`,
|
||||
},
|
||||
);
|
||||
|
||||
it("passes a completed null VirusTotal fixture when cached telemetry is unavailable", async () => {
|
||||
const workspace = await tempDir();
|
||||
const fakeClawScan = join(workspace, "fake-clawscan");
|
||||
const argsLog = join(workspace, "clawscan-args.log");
|
||||
const virusTotalLog = join(workspace, "virustotal-input.json");
|
||||
await writeFakeClawScanCommand(
|
||||
fakeClawScan,
|
||||
`printf '%s\n' "$@" > ${JSON.stringify(argsLog)}
|
||||
out=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--scanner-result)
|
||||
cp "\${2#virustotal=}" ${JSON.stringify(virusTotalLog)}
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
out="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
mkdir -p "$(dirname "$out")"
|
||||
cat > "$out" <<'JSON'
|
||||
${clawScanArtifactJson()}
|
||||
JSON`,
|
||||
);
|
||||
|
||||
const previousCommand = process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND;
|
||||
process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND = fakeClawScan;
|
||||
try {
|
||||
const client = {
|
||||
action: vi.fn(async (..._args: unknown[]) => ({})),
|
||||
};
|
||||
const result = await processJob(
|
||||
client,
|
||||
"worker-auth",
|
||||
claimedJob({
|
||||
jobId: "securityScanJobs:no-cached-vt",
|
||||
source: "manual",
|
||||
target: fileTarget("SKILL.md", "# Uploaded skill\n"),
|
||||
targetKind: "skillScanRequest",
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
completed: true,
|
||||
hardFailed: false,
|
||||
retryableFailed: false,
|
||||
});
|
||||
const invocationArgs = (await readFile(argsLog, "utf8")).trim().split("\n");
|
||||
const scannerResult = invocationArgs[invocationArgs.indexOf("--scanner-result") + 1];
|
||||
expect(scannerResult).toMatch(/^virustotal=/);
|
||||
expect(JSON.parse(await readFile(virusTotalLog, "utf8"))).toBeNull();
|
||||
} finally {
|
||||
if (previousCommand === undefined) delete process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND;
|
||||
else process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND = previousCommand;
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
source: "bulk-rescan",
|
||||
@@ -634,11 +539,11 @@ JSON`,
|
||||
},
|
||||
);
|
||||
|
||||
it("fails the job when VirusTotal scanner status is skipped", async () => {
|
||||
it("fails the job when SkillSpector scanner status is skipped", async () => {
|
||||
const workspace = await tempDir();
|
||||
const fakeClawScan = join(workspace, "fake-clawscan");
|
||||
const artifactJson = clawScanArtifactJson({
|
||||
scannerStatuses: { virustotal: "skipped" },
|
||||
scannerStatuses: { skillspector: "skipped" },
|
||||
});
|
||||
await writeFakeClawScanCommand(
|
||||
fakeClawScan,
|
||||
@@ -672,7 +577,7 @@ JSON`,
|
||||
const result = await processJob(
|
||||
client,
|
||||
"worker-auth",
|
||||
skillVersionJob("securityScanJobs:vt-skipped"),
|
||||
skillVersionJob("securityScanJobs:skillspector-skipped"),
|
||||
undefined,
|
||||
);
|
||||
|
||||
@@ -683,7 +588,7 @@ JSON`,
|
||||
});
|
||||
expect(client.action).toHaveBeenCalledTimes(1);
|
||||
expect(client.action.mock.calls[0]?.[1]).toMatchObject({
|
||||
error: "ClawScan scanner virustotal status was skipped",
|
||||
error: "ClawScan scanner skillspector status was skipped",
|
||||
});
|
||||
} finally {
|
||||
if (previousCommand === undefined) delete process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND;
|
||||
|
||||
@@ -105,7 +105,6 @@ type ClawScanCommandDiagnostic = {
|
||||
};
|
||||
scanners?: {
|
||||
skillspectorStatus?: string;
|
||||
virustotalStatus?: string;
|
||||
staticStatus?: string;
|
||||
};
|
||||
};
|
||||
@@ -135,7 +134,7 @@ type ProcessJobResult = {
|
||||
const DEFAULT_BATCH_LIMIT = 4;
|
||||
const DEFAULT_MAX_RUNTIME_MS = 40 * 60 * 1000;
|
||||
const DEFAULT_CLAWSCAN_TIMEOUT_MS = 20 * 60 * 1000;
|
||||
const REQUIRED_CLAWHUB_SCANNERS = ["clawscan-static", "skillspector", "virustotal"];
|
||||
const REQUIRED_CLAWHUB_SCANNERS = ["clawscan-static", "skillspector"];
|
||||
const MAX_DIAGNOSTIC_TEXT_CHARS = 20_000;
|
||||
const MAX_STORED_SKILLSPECTOR_ISSUES = 25;
|
||||
const MAX_STORED_SKILLSPECTOR_TEXT_CHARS = 2_000;
|
||||
@@ -361,7 +360,6 @@ const DIAGNOSTIC_PUBLIC_TEXT_PATHS = new Set([
|
||||
"clawscanmapping.judge.promptsha256",
|
||||
"clawscanmapping.judge.outputschemasha256",
|
||||
"clawscanmapping.scanners.skillspectorstatus",
|
||||
"clawscanmapping.scanners.virustotalstatus",
|
||||
"clawscanmapping.scanners.staticstatus",
|
||||
"llmanalysis.confidence",
|
||||
"llmanalysis.status",
|
||||
@@ -789,14 +787,6 @@ async function fileExists(path: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function cachedVirusTotalAnalysis(job: ClaimedJob) {
|
||||
return (
|
||||
(job.target.version as Record<string, unknown> | undefined)?.vtAnalysis ??
|
||||
(job.target.release as Record<string, unknown> | undefined)?.vtAnalysis ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function codexEnv() {
|
||||
const env = { ...process.env };
|
||||
const codexHome = resolveCodexWorkerHome(process.env, LOCAL_CODEX_HOME);
|
||||
@@ -1309,7 +1299,6 @@ function clawScanDiagnosticMapping(artifact: Record<string, unknown>) {
|
||||
scanners: {
|
||||
skillspectorStatus: scannerStatuses.skillspector,
|
||||
staticStatus: scannerStatuses["clawscan-static"],
|
||||
virustotalStatus: scannerStatuses.virustotal,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1328,7 +1317,6 @@ function validateClawScanArtifactForClawHubProfile(artifact: Record<string, unkn
|
||||
const allowedScannerStatuses: Record<string, Set<string>> = {
|
||||
"clawscan-static": new Set(["completed"]),
|
||||
skillspector: new Set(["completed"]),
|
||||
virustotal: new Set(["completed"]),
|
||||
};
|
||||
for (const [scanner, status] of Object.entries(scannerStatuses)) {
|
||||
const allowed = allowedScannerStatuses[scanner] ?? new Set(["completed"]);
|
||||
@@ -1377,22 +1365,8 @@ export async function runClawScan(
|
||||
) {
|
||||
const command = process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND ?? "clawscan";
|
||||
const artifactPath = join(workspace, "clawscan-artifact.json");
|
||||
const virusTotalResultPath = join(workspace, "clawscan-virustotal.json");
|
||||
await writeFile(
|
||||
virusTotalResultPath,
|
||||
`${JSON.stringify(cachedVirusTotalAnalysis(job), null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
const target = await resolveClawScanTarget(workspace, job);
|
||||
const args = [
|
||||
target,
|
||||
"--profile",
|
||||
"clawhub",
|
||||
"--scanner-result",
|
||||
`virustotal=${virusTotalResultPath}`,
|
||||
"--output",
|
||||
artifactPath,
|
||||
];
|
||||
const args = [target, "--profile", "clawhub", "--output", artifactPath];
|
||||
onDiagnostic({ args: [command, ...args], artifactPath });
|
||||
|
||||
const captureArtifact = async () => {
|
||||
|
||||
@@ -661,7 +661,7 @@ while [ "$#" -gt 0 ]; do
|
||||
shift
|
||||
done
|
||||
cat > "$output" <<'JSON'
|
||||
{"schemaVersion":"clawscan-run-v1","profile":"clawhub","scanners":{"clawscan-static":{"status":"completed"},"skillspector":{"status":"completed"},"virustotal":{"status":"completed"}},"judge":{"status":"failed","error":"Codex request was rate limited.","result":null}}
|
||||
{"schemaVersion":"clawscan-run-v1","profile":"clawhub","scanners":{"clawscan-static":{"status":"completed"},"skillspector":{"status":"completed"}},"judge":{"status":"failed","error":"Codex request was rate limited.","result":null}}
|
||||
JSON
|
||||
`,
|
||||
);
|
||||
|
||||
@@ -131,7 +131,7 @@ describe("security-scan-codex workflow", () => {
|
||||
const skillspectorInstall = steps.find((step) => step.name === "Install SkillSpector")?.run;
|
||||
expect(codexInstall).toContain("npm install -g @openai/codex@0.142.3");
|
||||
expect(codexInstall).not.toContain("@latest");
|
||||
expect(clawScanInstall).toContain("npm install -g @openclaw/clawscan@0.1.5");
|
||||
expect(clawScanInstall).toContain("npm install -g @openclaw/clawscan@0.1.6");
|
||||
expect(clawScanInstall).not.toContain("@latest");
|
||||
expect(skillspectorInstall).toContain("git+https://github.com/NVIDIA/skillspector.git@8f37cfa");
|
||||
expect(skillspectorInstall).not.toContain("git+https://github.com/NVIDIA/skillspector.git'");
|
||||
|
||||
@@ -271,15 +271,12 @@ See also: [acceptable-usage.md](./acceptable-usage.md) for the marketplace polic
|
||||
This is a product-facing model only; scanner storage, moderation decisions,
|
||||
and worker behavior remain separate internally.
|
||||
- ClawScan verdicts come from a GitHub Actions Codex worker, not a single
|
||||
hosted LLM call. Publishes enqueue a scan job that waits at most 10 minutes
|
||||
for VirusTotal telemetry, then Codex reviews the materialized artifact
|
||||
workspace with static and VT signals as context.
|
||||
hosted LLM call. Codex reviews the materialized artifact workspace with
|
||||
SkillSpector and static scan evidence as context.
|
||||
- Current skill and plugin scans are queued through `securityScanJobs` and
|
||||
completed by the external Codex worker.
|
||||
- ClawHub owns the single live VirusTotal scan for a published artifact. The
|
||||
worker passes the stored full `vtAnalysis` JSON into OSS ClawScan as the
|
||||
`virustotal` scanner result; it must not give ClawScan a VT credential or
|
||||
trigger a second live VT request.
|
||||
- VirusTotal telemetry remains a separate Security audit signal and is not an
|
||||
input to the production ClawScan profile or judge.
|
||||
- The worker's explicit artifact-only OSS ClawScan route accepts every claimed
|
||||
target kind and source through the same completion/failure contract. Skill
|
||||
versions and scan requests use the isolated `artifact` root; extracted
|
||||
|
||||
Reference in New Issue
Block a user