import os from "node:os" ;
import path from "node:path" ;
import { afterEach, describe, expect, it, vi } from "vitest" ;
import type { OpenClawConfig } from "../config/config.js" ;
import {
collectSandboxDangerousConfigFindings,
collectSandboxDockerNoopFindings,
} from "./audit-extra.sync.js" ;
type FindingUnderTest = {
checkId: string;
severity: string;
};
function expectFindingSet(params: {
findings: FindingUnderTest[];
name: string;
expectedPresent?: readonly string[];
expectedAbsent?: readonly string[];
severity?: string;
}) {
const severity = params.severity ?? "warn" ;
for (const checkId of params.expectedPresent ?? []) {
expect(
params.findings.some(
(finding) => finding.checkId === checkId && finding.severity === severity,
),
`${params.name}:${checkId}`,
).toBe(true );
}
for (const checkId of params.expectedAbsent ?? []) {
expect(
params.findings.some((finding) => finding.checkId === checkId),
`${params.name}:${checkId}`,
).toBe(false );
}
}
describe("security audit sandbox docker config" , () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("evaluates sandbox docker config findings" , async () => {
const isolatedHome = path.join(os.tmpdir(), "openclaw-security-audit-home" );
const previousHome = process.env.HOME;
const previousUserProfile = process.env.USERPROFILE;
process.env.HOME = isolatedHome;
process.env.USERPROFILE = isolatedHome;
vi.spyOn(os, "homedir" ).mockReturnValue(isolatedHome);
try {
const cases = [
{
name: "mode off with docker config only" ,
cfg: {
agents: {
defaults: {
sandbox: {
mode: "off" ,
docker: { image: "ghcr.io/example/sandbox:latest" },
},
},
},
} as OpenClawConfig,
expectedFindings: [{ checkId: "sandbox.docker_config_mode_off" }],
},
{
name: "agent enables sandbox mode" ,
cfg: {
agents: {
defaults: {
sandbox: {
mode: "off" ,
docker: { image: "ghcr.io/example/sandbox:latest" },
},
},
list: [{ id: "ops" , sandbox: { mode: "all" } }],
},
} as OpenClawConfig,
expectedFindings: [],
expectedAbsent: ["sandbox.docker_config_mode_off" ],
},
{
name: "dangerous binds, host network, seccomp, and apparmor" ,
cfg: {
agents: {
defaults: {
sandbox: {
mode: "all" ,
docker: {
binds: ["/etc/passwd:/mnt/passwd:ro" , "/run:/run" ],
network: "host" ,
seccompProfile: "unconfined" ,
apparmorProfile: "unconfined" ,
},
},
},
},
} as OpenClawConfig,
expectedFindings: [
{ checkId: "sandbox.dangerous_bind_mount" , severity: "critical" },
{ checkId: "sandbox.dangerous_network_mode" , severity: "critical" },
{ checkId: "sandbox.dangerous_seccomp_profile" , severity: "critical" },
{ checkId: "sandbox.dangerous_apparmor_profile" , severity: "critical" },
],
},
{
name: "home credential bind is treated as dangerous" ,
cfg: {
agents: {
defaults: {
sandbox: {
mode: "all" ,
docker: {
binds: [path.join(isolatedHome, ".docker" , "config.json" ) + ":/mnt/docker:ro" ],
},
},
},
},
} as OpenClawConfig,
expectedFindings: [
{
checkId: "sandbox.dangerous_bind_mount" ,
severity: "critical" ,
title: "Dangerous bind mount in sandbox config" ,
},
],
},
{
name: "container namespace join network mode" ,
cfg: {
agents: {
defaults: {
sandbox: {
mode: "all" ,
docker: {
network: "container:peer" ,
},
},
},
},
} as OpenClawConfig,
expectedFindings: [
{
checkId: "sandbox.dangerous_network_mode" ,
severity: "critical" ,
title: "Dangerous network mode in sandbox config" ,
},
],
},
] as const ;
await Promise.all(
cases.map(async (testCase) => {
const findings = [
...collectSandboxDockerNoopFindings(testCase.cfg),
...collectSandboxDangerousConfigFindings(testCase.cfg),
];
if (testCase.expectedFindings.length > 0 ) {
expect(findings, testCase.name).toEqual(
expect.arrayContaining(
testCase.expectedFindings.map((finding) => expect.objectContaining(finding)),
),
);
}
expectFindingSet({
findings,
name: testCase.name,
expectedAbsent: "expectedAbsent" in testCase ? testCase.expectedAbsent : [],
});
}),
);
} finally {
if (previousHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = previousHome;
}
if (previousUserProfile === undefined) {
delete process.env.USERPROFILE;
} else {
process.env.USERPROFILE = previousUserProfile;
}
}
});
});
Messung V0.5 in Prozent C=98 H=98 G=97
¤ Dauer der Verarbeitung: 0.3 Sekunden
¤
*© Formatika GbR, Deutschland