mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 11:01:24 +00:00
refactor(exec): centralize safe-bin policy checks
This commit is contained in:
@@ -192,7 +192,44 @@ function normalizeSafeBinProfileName(raw: string): string | null {
|
||||
return name.length > 0 ? name : null;
|
||||
}
|
||||
|
||||
function normalizeSafeBinProfileFixtures(
|
||||
function normalizeFixtureLimit(raw: number | undefined): number | undefined {
|
||||
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
const next = Math.trunc(raw);
|
||||
return next >= 0 ? next : undefined;
|
||||
}
|
||||
|
||||
function normalizeFixtureFlags(
|
||||
flags: readonly string[] | undefined,
|
||||
): readonly string[] | undefined {
|
||||
if (!Array.isArray(flags) || flags.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = Array.from(
|
||||
new Set(flags.map((flag) => flag.trim()).filter((flag) => flag.length > 0)),
|
||||
).toSorted((a, b) => a.localeCompare(b));
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function normalizeSafeBinProfileFixture(fixture: SafeBinProfileFixture): SafeBinProfileFixture {
|
||||
const minPositional = normalizeFixtureLimit(fixture.minPositional);
|
||||
const maxPositionalRaw = normalizeFixtureLimit(fixture.maxPositional);
|
||||
const maxPositional =
|
||||
minPositional !== undefined &&
|
||||
maxPositionalRaw !== undefined &&
|
||||
maxPositionalRaw < minPositional
|
||||
? minPositional
|
||||
: maxPositionalRaw;
|
||||
return {
|
||||
minPositional,
|
||||
maxPositional,
|
||||
allowedValueFlags: normalizeFixtureFlags(fixture.allowedValueFlags),
|
||||
deniedFlags: normalizeFixtureFlags(fixture.deniedFlags),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeSafeBinProfileFixtures(
|
||||
fixtures?: SafeBinProfileFixtures | null,
|
||||
): Record<string, SafeBinProfileFixture> {
|
||||
const normalized: Record<string, SafeBinProfileFixture> = {};
|
||||
@@ -204,12 +241,7 @@ function normalizeSafeBinProfileFixtures(
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
normalized[name] = {
|
||||
minPositional: fixture.minPositional,
|
||||
maxPositional: fixture.maxPositional,
|
||||
allowedValueFlags: fixture.allowedValueFlags,
|
||||
deniedFlags: fixture.deniedFlags,
|
||||
};
|
||||
normalized[name] = normalizeSafeBinProfileFixture(fixture);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
73
src/infra/exec-safe-bin-runtime-policy.test.ts
Normal file
73
src/infra/exec-safe-bin-runtime-policy.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
isInterpreterLikeSafeBin,
|
||||
listInterpreterLikeSafeBins,
|
||||
resolveExecSafeBinRuntimePolicy,
|
||||
resolveMergedSafeBinProfileFixtures,
|
||||
} from "./exec-safe-bin-runtime-policy.js";
|
||||
|
||||
describe("exec safe-bin runtime policy", () => {
|
||||
const interpreterCases: Array<{ bin: string; expected: boolean }> = [
|
||||
{ bin: "python3", expected: true },
|
||||
{ bin: "python3.12", expected: true },
|
||||
{ bin: "node", expected: true },
|
||||
{ bin: "node20", expected: true },
|
||||
{ bin: "ruby3.2", expected: true },
|
||||
{ bin: "bash", expected: true },
|
||||
{ bin: "myfilter", expected: false },
|
||||
{ bin: "jq", expected: false },
|
||||
];
|
||||
|
||||
for (const testCase of interpreterCases) {
|
||||
it(`classifies interpreter-like safe bin '${testCase.bin}'`, () => {
|
||||
expect(isInterpreterLikeSafeBin(testCase.bin)).toBe(testCase.expected);
|
||||
});
|
||||
}
|
||||
|
||||
it("lists interpreter-like bins from a mixed set", () => {
|
||||
expect(listInterpreterLikeSafeBins(["jq", "python3", "myfilter", "node"])).toEqual([
|
||||
"node",
|
||||
"python3",
|
||||
]);
|
||||
});
|
||||
|
||||
it("merges and normalizes safe-bin profile fixtures", () => {
|
||||
const merged = resolveMergedSafeBinProfileFixtures({
|
||||
global: {
|
||||
safeBinProfiles: {
|
||||
" MyFilter ": {
|
||||
deniedFlags: ["--file", " --file ", ""],
|
||||
},
|
||||
},
|
||||
},
|
||||
local: {
|
||||
safeBinProfiles: {
|
||||
myfilter: {
|
||||
maxPositional: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(merged).toEqual({
|
||||
myfilter: {
|
||||
maxPositional: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("computes unprofiled interpreter entries separately from custom profiled bins", () => {
|
||||
const policy = resolveExecSafeBinRuntimePolicy({
|
||||
local: {
|
||||
safeBins: ["python3", "myfilter"],
|
||||
safeBinProfiles: {
|
||||
myfilter: { maxPositional: 0 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(policy.safeBins.has("python3")).toBe(true);
|
||||
expect(policy.safeBins.has("myfilter")).toBe(true);
|
||||
expect(policy.unprofiledSafeBins).toEqual(["python3"]);
|
||||
expect(policy.unprofiledInterpreterSafeBins).toEqual(["python3"]);
|
||||
});
|
||||
});
|
||||
127
src/infra/exec-safe-bin-runtime-policy.ts
Normal file
127
src/infra/exec-safe-bin-runtime-policy.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { resolveSafeBins } from "./exec-approvals-allowlist.js";
|
||||
import {
|
||||
normalizeSafeBinProfileFixtures,
|
||||
resolveSafeBinProfiles,
|
||||
type SafeBinProfile,
|
||||
type SafeBinProfileFixture,
|
||||
type SafeBinProfileFixtures,
|
||||
} from "./exec-safe-bin-policy.js";
|
||||
import { getTrustedSafeBinDirs } from "./exec-safe-bin-trust.js";
|
||||
|
||||
export type ExecSafeBinConfigScope = {
|
||||
safeBins?: string[] | null;
|
||||
safeBinProfiles?: SafeBinProfileFixtures | null;
|
||||
};
|
||||
|
||||
const INTERPRETER_LIKE_SAFE_BINS = new Set([
|
||||
"ash",
|
||||
"bash",
|
||||
"bun",
|
||||
"cmd",
|
||||
"cmd.exe",
|
||||
"cscript",
|
||||
"dash",
|
||||
"deno",
|
||||
"fish",
|
||||
"ksh",
|
||||
"lua",
|
||||
"node",
|
||||
"nodejs",
|
||||
"perl",
|
||||
"php",
|
||||
"powershell",
|
||||
"powershell.exe",
|
||||
"pypy",
|
||||
"pwsh",
|
||||
"pwsh.exe",
|
||||
"python",
|
||||
"python2",
|
||||
"python3",
|
||||
"ruby",
|
||||
"sh",
|
||||
"wscript",
|
||||
"zsh",
|
||||
]);
|
||||
|
||||
const INTERPRETER_LIKE_PATTERNS = [
|
||||
/^python\d+(?:\.\d+)?$/,
|
||||
/^ruby\d+(?:\.\d+)?$/,
|
||||
/^perl\d+(?:\.\d+)?$/,
|
||||
/^php\d+(?:\.\d+)?$/,
|
||||
/^node\d+(?:\.\d+)?$/,
|
||||
];
|
||||
|
||||
function normalizeSafeBinName(raw: string): string {
|
||||
const trimmed = raw.trim().toLowerCase();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
const tail = trimmed.split(/[\\/]/).at(-1);
|
||||
return tail ?? trimmed;
|
||||
}
|
||||
|
||||
export function isInterpreterLikeSafeBin(raw: string): boolean {
|
||||
const normalized = normalizeSafeBinName(raw);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (INTERPRETER_LIKE_SAFE_BINS.has(normalized)) {
|
||||
return true;
|
||||
}
|
||||
return INTERPRETER_LIKE_PATTERNS.some((pattern) => pattern.test(normalized));
|
||||
}
|
||||
|
||||
export function listInterpreterLikeSafeBins(entries: Iterable<string>): string[] {
|
||||
return Array.from(entries)
|
||||
.map((entry) => normalizeSafeBinName(entry))
|
||||
.filter((entry) => entry.length > 0 && isInterpreterLikeSafeBin(entry))
|
||||
.toSorted();
|
||||
}
|
||||
|
||||
export function resolveMergedSafeBinProfileFixtures(params: {
|
||||
global?: ExecSafeBinConfigScope | null;
|
||||
local?: ExecSafeBinConfigScope | null;
|
||||
}): Record<string, SafeBinProfileFixture> | undefined {
|
||||
const global = normalizeSafeBinProfileFixtures(params.global?.safeBinProfiles);
|
||||
const local = normalizeSafeBinProfileFixtures(params.local?.safeBinProfiles);
|
||||
if (Object.keys(global).length === 0 && Object.keys(local).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...global,
|
||||
...local,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveExecSafeBinRuntimePolicy(params: {
|
||||
global?: ExecSafeBinConfigScope | null;
|
||||
local?: ExecSafeBinConfigScope | null;
|
||||
pathEnv?: string | null;
|
||||
}): {
|
||||
safeBins: Set<string>;
|
||||
safeBinProfiles: Readonly<Record<string, SafeBinProfile>>;
|
||||
trustedSafeBinDirs: ReadonlySet<string>;
|
||||
unprofiledSafeBins: string[];
|
||||
unprofiledInterpreterSafeBins: string[];
|
||||
} {
|
||||
const safeBins = resolveSafeBins(params.local?.safeBins ?? params.global?.safeBins);
|
||||
const safeBinProfiles = resolveSafeBinProfiles(
|
||||
resolveMergedSafeBinProfileFixtures({
|
||||
global: params.global,
|
||||
local: params.local,
|
||||
}),
|
||||
);
|
||||
const unprofiledSafeBins = Array.from(safeBins)
|
||||
.filter((entry) => !safeBinProfiles[entry])
|
||||
.toSorted();
|
||||
const trustedSafeBinDirs = params.pathEnv
|
||||
? getTrustedSafeBinDirs({ pathEnv: params.pathEnv })
|
||||
: getTrustedSafeBinDirs();
|
||||
return {
|
||||
safeBins,
|
||||
safeBinProfiles,
|
||||
trustedSafeBinDirs,
|
||||
unprofiledSafeBins,
|
||||
unprofiledInterpreterSafeBins: listInterpreterLikeSafeBins(unprofiledSafeBins),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user