Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  pi-tools.safe-bins.test.ts

  Sprache: JAVA
 

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import type { ExecApprovalsResolved } from "../infra/exec-approvals.js";
import type { SafeBinProfileFixture } from "../infra/exec-safe-bin-policy.js";
import { withEnvAsync } from "../test-utils/env.js";
import { resetProcessRegistryForTests } from "./bash-process-registry.js";

let createOpenClawCodingTools: typeof import("./pi-tools.js").createOpenClawCodingTools;

const { mockExecApprovals, supervisorSpawnMock } = vi.hoisted(() => {
  const execApprovals = {
    path: "/tmp/exec-approvals.json",
    socketPath: "/tmp/exec-approvals.sock",
    token: "token",
    defaults: {
      security: "allowlist",
      ask: "off",
      askFallback: "deny",
      autoAllowSkills: false,
    },
    agent: {
      security: "allowlist",
      ask: "off",
      askFallback: "deny",
      autoAllowSkills: false,
    },
    agentSources: {
      security: "defaults.security",
      ask: "defaults.ask",
      askFallback: "defaults.askFallback",
    },
    allowlist: [],
    file: {
      version: 1,
      socket: { path: "/tmp/exec-approvals.sock", token: "token" },
      defaults: {
        security: "allowlist",
        ask: "off",
        askFallback: "deny",
        autoAllowSkills: false,
      },
      agents: {},
    },
  };
  return {
    mockExecApprovals: execApprovals,
    supervisorSpawnMock: vi.fn(
      async (input: { argv?: string[]; onStdout?: (chunk: string) => void }) => {
        input.onStdout?.(`${input.argv?.join(" ") ?? ""}\n`);
        return {
          runId: "safe-bins-test-run",
          pid: 1234,
          startedAtMs: Date.now(),
          stdin: undefined,
          wait: async () => ({
            reason: "exit" as const,
            exitCode: 0,
            exitSignal: null,
            durationMs: 1,
            stdout: "",
            stderr: "",
            timedOut: false,
            noOutputTimedOut: false,
          }),
          cancel: vi.fn(),
        };
      },
    ),
  };
});

beforeAll(async () => {
  await withEnvAsync(
    {
      OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(os.tmpdir(), "openclaw-test-no-bundled-extensions"),
    },
    async () => {
      ({ createOpenClawCodingTools } = await import("./pi-tools.js"));
    },
  );
});

beforeEach(() => {
  supervisorSpawnMock.mockClear();
});

vi.mock("../infra/shell-env.js", async () => {
  const mod =
    await vi.importActual<typeof import("../infra/shell-env.js")>("../infra/shell-env.js");
  return {
    ...mod,
    getShellPathFromLoginShell: vi.fn(() => null),
    resolveShellEnvFallbackTimeoutMs: vi.fn(() => 50),
  };
});

vi.mock("../process/supervisor/index.js", () => ({
  getProcessSupervisor: () => ({
    spawn: supervisorSpawnMock,
    cancel: vi.fn(),
    cancelScope: vi.fn(),
    reconcileOrphans: vi.fn(),
    getRecord: vi.fn(),
  }),
}));

vi.mock("./channel-tools.js", () => ({
  copyChannelAgentToolMeta: vi.fn((_from, to) => to),
  listChannelAgentTools: () => [],
}));

vi.mock("./openclaw-tools.js", () => ({
  createOpenClawTools: () => [],
}));

vi.mock("./bash-tools.exec-host-shared.js", async () => {
  const mod = await vi.importActual<typeof import("./bash-tools.exec-host-shared.js")>(
    "./bash-tools.exec-host-shared.js",
  );
  return {
    ...mod,
    resolveExecHostApprovalContext: () => ({
      approvals: mockExecApprovals,
      hostSecurity: "allowlist",
      hostAsk: "off",
      askFallback: "deny",
    }),
  };
});

vi.mock("../plugins/tools.js", () => ({
  copyPluginToolMeta: vi.fn((_from, to) => to),
  resolvePluginTools: () => [],
  getPluginToolMeta: () => undefined,
}));

vi.mock("@mariozechner/pi-coding-agent", () => ({
  AuthStorage: vi.fn(),
  CURRENT_SESSION_VERSION: 1,
  ModelRegistry: vi.fn(),
  SessionManager: vi.fn(),
  SettingsManager: vi.fn(),
  createCodingTools: vi.fn(() => []),
  createEditTool: vi.fn(),
  createReadTool: vi.fn(),
  createWriteTool: vi.fn(),
  estimateTokens: vi.fn(() => 0),
  formatSkillsForPrompt: vi.fn(() => ""),
}));

vi.mock("../infra/exec-approvals.js", async () => {
  const mod = await vi.importActual<typeof import("../infra/exec-approvals.js")>(
    "../infra/exec-approvals.js",
  );
  const approvals = mockExecApprovals as ExecApprovalsResolved;
  return {
    ...mod,
    loadExecApprovals: () => approvals.file,
    resolveExecApprovals: () => approvals,
  };
});

type ExecToolResult = {
  content: Array<{ type: string; text?: string }>;
  details?: { status?: string };
};

type ExecTool = {
  execute(
    callId: string,
    params: {
      command: string;
      workdir: string;
      env?: Record<string, string>;
    },
  ): Promise<ExecToolResult>;
};

async function createSafeBinsExecTool(params: {
  tmpPrefix: string;
  safeBins: string[];
  safeBinProfiles?: Record<string, SafeBinProfileFixture>;
  files?: Array<{ name: string; contents: string }>;
}): Promise<{ tmpDir: string; execTool: ExecTool }> {
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), params.tmpPrefix));
  for (const file of params.files ?? []) {
    fs.writeFileSync(path.join(tmpDir, file.name), file.contents, "utf8");
  }

  const cfg: OpenClawConfig = {
    tools: {
      exec: {
        host: "gateway",
        security: "allowlist",
        ask: "off",
        safeBins: params.safeBins,
        safeBinProfiles: params.safeBinProfiles,
      },
    },
  };

  const tools = createOpenClawCodingTools({
    config: cfg,
    exec: {
      notifyOnExit: false,
    },
    sessionKey: "agent:main:main",
    workspaceDir: tmpDir,
    agentDir: path.join(tmpDir, "agent"),
  });
  const execTool = tools.find((tool) => tool.name === "exec");
  if (!execTool) {
    throw new Error("exec tool missing from coding tools");
  }
  return { tmpDir, execTool: execTool as ExecTool };
}

async function withSafeBinsExecTool(
  params: Parameters<typeof createSafeBinsExecTool>[0],
  run: (ctx: Awaited<ReturnType<typeof createSafeBinsExecTool>>) => Promise<void>,
) {
  if (process.platform === "win32") {
    return;
  }
  const ctx = await createSafeBinsExecTool(params);
  try {
    await withEnvAsync(
      {
        OPENCLAW_SHELL_ENV_TIMEOUT_MS: "1",
        SHELL: "/bin/sh",
      },
      async () => {
        await run(ctx);
      },
    );
  } finally {
    fs.rmSync(ctx.tmpDir, { recursive: true, force: true });
    resetProcessRegistryForTests();
  }
}

describe("createOpenClawCodingTools safeBins", () => {
  it("threads tools.exec.safeBins into exec allowlist checks", async () => {
    await withSafeBinsExecTool(
      {
        tmpPrefix: "openclaw-safe-bins-",
        safeBins: ["echo"],
        safeBinProfiles: {
          echo: { maxPositional: 1 },
        },
      },
      async ({ tmpDir, execTool }) => {
        const marker = `safe-bins-${Date.now()}`;
        const result = await execTool.execute("call1", {
          command: `echo ${marker}`,
          workdir: tmpDir,
        });
        const text = result.content.find((content) => content.type === "text")?.text ?? "";

        const resultDetails = result.details as { status?: string };
        expect(resultDetails.status).toBe("completed");
        expect(text).toContain(marker);
        expect(supervisorSpawnMock).toHaveBeenCalledOnce();
      },
    );
  });

  it("rejects unprofiled custom safe-bin entries", async () => {
    await withSafeBinsExecTool(
      {
        tmpPrefix: "openclaw-safe-bins-unprofiled-",
        safeBins: ["echo"],
      },
      async ({ tmpDir, execTool }) => {
        await expect(
          execTool.execute("call1", {
            command: "echo hello",
            workdir: tmpDir,
          }),
        ).rejects.toThrow("exec denied: allowlist miss");
      },
    );
  });

  it("does not allow env var expansion to smuggle file args via safeBins", async () => {
    await withSafeBinsExecTool(
      {
        tmpPrefix: "openclaw-safe-bins-expand-",
        safeBins: ["head""wc"],
        files: [{ name: "secret.txt", contents: "TOP_SECRET\n" }],
      },
      async ({ tmpDir, execTool }) => {
        await expect(
          execTool.execute("call1", {
            command: "head $FOO ; wc -l",
            workdir: tmpDir,
            env: { FOO: "secret.txt" },
          }),
        ).rejects.toThrow("exec denied: allowlist miss");
      },
    );
  });

  it("blocks sort output/compress bypass attempts in safeBins mode", async () => {
    await withSafeBinsExecTool(
      {
        tmpPrefix: "openclaw-safe-bins-sort-",
        safeBins: ["sort"],
        files: [{ name: "existing.txt", contents: "x\n" }],
      },
      async ({ tmpDir, execTool }) => {
        const run = async (command: string) => {
          try {
            const result = await execTool.execute("call-oracle", { command, workdir: tmpDir });
            const text = result.content.find((content) => content.type === "text")?.text ?? "";
            const resultDetails = result.details as { status?: string };
            return { kind: "result" as const, status: resultDetails.status, text };
          } catch (err) {
            return { kind: "error" as const, message: String(err) };
          }
        };

        const existing = await run("sort -o existing.txt");
        const missing = await run("sort -o missing.txt");
        expect(existing).toEqual(missing);

        const outputFlagCases = [
          { command: "sort -oblocked-short.txt", target: "blocked-short.txt" },
          { command: "sort --output=blocked-long.txt", target: "blocked-long.txt" },
        ] as const;
        for (const [index, testCase] of outputFlagCases.entries()) {
          await expect(
            execTool.execute(`call-output-${index + 1}`, {
              command: testCase.command,
              workdir: tmpDir,
            }),
          ).rejects.toThrow("exec denied: allowlist miss");
          expect(fs.existsSync(path.join(tmpDir, testCase.target))).toBe(false);
        }

        await expect(
          execTool.execute("call1", {
            command: "sort --compress-program=sh",
            workdir: tmpDir,
          }),
        ).rejects.toThrow("exec denied: allowlist miss");
      },
    );
  });

  it("blocks shell redirection metacharacters in safeBins mode", async () => {
    await withSafeBinsExecTool(
      {
        tmpPrefix: "openclaw-safe-bins-redirect-",
        safeBins: ["head"],
        files: [{ name: "source.txt", contents: "line1\nline2\n" }],
      },
      async ({ tmpDir, execTool }) => {
        await expect(
          execTool.execute("call1", {
            command: "head -n 1 source.txt > blocked-redirect.txt",
            workdir: tmpDir,
          }),
        ).rejects.toThrow("exec denied: allowlist miss");
        expect(fs.existsSync(path.join(tmpDir, "blocked-redirect.txt"))).toBe(false);
      },
    );
  });

  it("blocks grep recursive flags from reading cwd via safeBins", async () => {
    await withSafeBinsExecTool(
      {
        tmpPrefix: "openclaw-safe-bins-grep-",
        safeBins: ["grep"],
        files: [{ name: "secret.txt", contents: "SAFE_BINS_RECURSIVE_SHOULD_NOT_LEAK\n" }],
      },
      async ({ tmpDir, execTool }) => {
        await expect(
          execTool.execute("call1", {
            command: "grep -R SAFE_BINS_RECURSIVE_SHOULD_NOT_LEAK",
            workdir: tmpDir,
          }),
        ).rejects.toThrow("exec denied: allowlist miss");
      },
    );
  });
});

Messung V0.5 in Prozent
C=99 H=96 G=97

¤ Dauer der Verarbeitung: 0.11 Sekunden  (vorverarbeitet am  2026-05-26) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge