mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 00:14:34 +00:00
test(security): consolidate runtime guardrail scans
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { loadRuntimeSourceFilesForGuardrails } from "../test-utils/runtime-source-guardrail-scan.js";
|
||||
|
||||
const RUNTIME_ROOTS = ["src", "extensions"];
|
||||
const SKIP_PATTERNS = [
|
||||
/\.test\.tsx?$/,
|
||||
/\.test-helpers\.tsx?$/,
|
||||
@@ -11,7 +8,7 @@ const SKIP_PATTERNS = [
|
||||
/\.test-harness\.tsx?$/,
|
||||
/\.e2e\.tsx?$/,
|
||||
/\.d\.ts$/,
|
||||
/[\\/](?:__tests__|tests)[\\/]/,
|
||||
/[\\/](?:__tests__|tests|test-utils)[\\/]/,
|
||||
/[\\/][^\\/]*test-helpers(?:\.[^\\/]+)?\.ts$/,
|
||||
/[\\/][^\\/]*test-utils(?:\.[^\\/]+)?\.ts$/,
|
||||
/[\\/][^\\/]*test-harness(?:\.[^\\/]+)?\.ts$/,
|
||||
@@ -193,100 +190,6 @@ function hasDynamicTmpdirJoin(source: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async function listTsFiles(dir: string): Promise<string[]> {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
const out: string[] = [];
|
||||
for (const entry of entries) {
|
||||
if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) {
|
||||
continue;
|
||||
}
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
out.push(...(await listTsFiles(fullPath)));
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
if (fullPath.endsWith(".ts") || fullPath.endsWith(".tsx")) {
|
||||
out.push(fullPath);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parsePathList(stdout: string): Set<string> {
|
||||
const out = new Set<string>();
|
||||
for (const line of stdout.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
out.add(path.resolve(trimmed));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function prefilterLikelyTmpdirJoinFiles(roots: readonly string[]): Set<string> | null {
|
||||
const commonArgs = [
|
||||
"--files-with-matches",
|
||||
"--glob",
|
||||
"*.ts",
|
||||
"--glob",
|
||||
"*.tsx",
|
||||
"--glob",
|
||||
"!**/*.test.ts",
|
||||
"--glob",
|
||||
"!**/*.test.tsx",
|
||||
"--glob",
|
||||
"!**/*.e2e.ts",
|
||||
"--glob",
|
||||
"!**/*.e2e.tsx",
|
||||
"--glob",
|
||||
"!**/*.d.ts",
|
||||
"--glob",
|
||||
"!**/*test-helpers*.ts",
|
||||
"--glob",
|
||||
"!**/*test-helpers*.tsx",
|
||||
"--glob",
|
||||
"!**/*test-utils*.ts",
|
||||
"--glob",
|
||||
"!**/*test-utils*.tsx",
|
||||
"--glob",
|
||||
"!**/*test-harness*.ts",
|
||||
"--glob",
|
||||
"!**/*test-harness*.tsx",
|
||||
"--no-messages",
|
||||
];
|
||||
const strictDynamicCall = spawnSync(
|
||||
"rg",
|
||||
[
|
||||
...commonArgs,
|
||||
"-P",
|
||||
"-U",
|
||||
"(?s)path\\s*\\.\\s*join\\s*\\(\\s*os\\s*\\.\\s*tmpdir\\s*\\([^`]*`",
|
||||
...roots,
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
if (
|
||||
!strictDynamicCall.error &&
|
||||
(strictDynamicCall.status === 0 || strictDynamicCall.status === 1)
|
||||
) {
|
||||
return parsePathList(strictDynamicCall.stdout);
|
||||
}
|
||||
|
||||
const candidateCall = spawnSync(
|
||||
"rg",
|
||||
[...commonArgs, "path\\s*\\.\\s*join\\s*\\(\\s*os\\s*\\.\\s*tmpdir\\s*\\(", ...roots],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
if (candidateCall.error || (candidateCall.status !== 0 && candidateCall.status !== 1)) {
|
||||
return null;
|
||||
}
|
||||
return parsePathList(candidateCall.stdout);
|
||||
}
|
||||
|
||||
describe("temp path guard", () => {
|
||||
it("skips test helper filename variants", () => {
|
||||
expect(shouldSkip("src/commands/test-helpers.ts")).toBe(true);
|
||||
@@ -316,42 +219,33 @@ describe("temp path guard", () => {
|
||||
expect(hasDynamicTmpdirJoin(fixture)).toBe(false);
|
||||
}
|
||||
});
|
||||
it("blocks dynamic template path.join(os.tmpdir(), ...) in runtime source files", async () => {
|
||||
const repoRoot = process.cwd();
|
||||
const offenders: string[] = [];
|
||||
const scanRoots = RUNTIME_ROOTS.map((root) => path.join(repoRoot, root));
|
||||
const rgPrefiltered = prefilterLikelyTmpdirJoinFiles(scanRoots);
|
||||
const prefilteredByRoot = new Map<string, string[]>();
|
||||
if (rgPrefiltered) {
|
||||
for (const file of rgPrefiltered) {
|
||||
for (const absRoot of scanRoots) {
|
||||
if (file.startsWith(absRoot + path.sep)) {
|
||||
const bucket = prefilteredByRoot.get(absRoot) ?? [];
|
||||
bucket.push(file);
|
||||
prefilteredByRoot.set(absRoot, bucket);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const root of RUNTIME_ROOTS) {
|
||||
const absRoot = path.join(repoRoot, root);
|
||||
const files = rgPrefiltered
|
||||
? (prefilteredByRoot.get(absRoot) ?? [])
|
||||
: await listTsFiles(absRoot);
|
||||
for (const file of files) {
|
||||
const relativePath = path.relative(repoRoot, file);
|
||||
if (shouldSkip(relativePath)) {
|
||||
continue;
|
||||
}
|
||||
const source = await fs.readFile(file, "utf8");
|
||||
if (hasDynamicTmpdirJoin(source)) {
|
||||
offenders.push(relativePath);
|
||||
it("enforces runtime guardrails for tmpdir joins and weak randomness", async () => {
|
||||
const files = await loadRuntimeSourceFilesForGuardrails(process.cwd());
|
||||
const offenders: string[] = [];
|
||||
const weakRandomMatches: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const relativePath = file.relativePath;
|
||||
if (shouldSkip(relativePath)) {
|
||||
continue;
|
||||
}
|
||||
if (hasDynamicTmpdirJoin(file.source)) {
|
||||
offenders.push(relativePath);
|
||||
}
|
||||
if (file.source.includes("Date.now") && file.source.includes("Math.random")) {
|
||||
const lines = file.source.split(/\r?\n/);
|
||||
for (let idx = 0; idx < lines.length; idx += 1) {
|
||||
const line = lines[idx] ?? "";
|
||||
if (!line.includes("Date.now") || !line.includes("Math.random")) {
|
||||
continue;
|
||||
}
|
||||
weakRandomMatches.push(`${relativePath}:${idx + 1}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(offenders).toEqual([]);
|
||||
expect(weakRandomMatches).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user