openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
514 lines (513 loc) • 15.7 kB
JavaScript
import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-1VuiLaaR.mjs";
import { v as sortUniqueStrings, y as uniqueStrings } from "./string-normalization-D6DKCpwE.mjs";
import { n as sameFsObject, r as swapAsciiCase } from "./path-case-BjTaHmBw.mjs";
import { o as parseExecArgvToken } from "./exec-command-resolution-B0fS-8og.mjs";
import fs from "node:fs";
import path from "node:path";
//#region src/infra/exec-safe-bin-policy-profiles.ts
const NO_FLAGS$1 = /* @__PURE__ */ new Set();
const DEFAULT_SAFE_BINS = [
"cut",
"uniq",
"head",
"tail",
"tr",
"wc"
];
const toFlagSet = (flags) => {
if (!flags || flags.length === 0) return NO_FLAGS$1;
return new Set(flags);
};
function collectKnownLongFlags(allowedValueFlags, deniedFlags, allowedBooleanFlags = NO_FLAGS$1) {
const known = /* @__PURE__ */ new Set();
for (const flags of [
allowedValueFlags,
allowedBooleanFlags,
deniedFlags
]) for (const flag of flags) if (flag.startsWith("--")) known.add(flag);
return Array.from(known);
}
function buildLongFlagPrefixMap(knownLongFlags) {
const prefixMap = /* @__PURE__ */ new Map();
for (const flag of knownLongFlags) {
if (!flag.startsWith("--") || flag.length <= 2) continue;
for (let length = 3; length <= flag.length; length += 1) {
const prefix = flag.slice(0, length);
const existing = prefixMap.get(prefix);
if (existing === void 0) {
prefixMap.set(prefix, flag);
continue;
}
if (existing !== flag) prefixMap.set(prefix, null);
}
}
return prefixMap;
}
function compileSafeBinProfile(fixture) {
const allowedValueFlags = toFlagSet(fixture.allowedValueFlags);
const allowedBooleanFlags = toFlagSet(fixture.allowedBooleanFlags);
const deniedFlags = toFlagSet(fixture.deniedFlags);
const knownLongFlags = collectKnownLongFlags(allowedValueFlags, deniedFlags, allowedBooleanFlags);
return {
minPositional: fixture.minPositional,
maxPositional: fixture.maxPositional,
allowedValueFlags,
allowedBooleanFlags,
deniedFlags,
knownLongFlags,
knownLongFlagsSet: new Set(knownLongFlags),
longFlagPrefixMap: buildLongFlagPrefixMap(knownLongFlags)
};
}
function compileSafeBinProfiles(fixtures) {
return Object.fromEntries(Object.entries(fixtures).map(([name, fixture]) => [name, compileSafeBinProfile(fixture)]));
}
const HEAD_TAIL_PROFILE = {
maxPositional: 0,
allowedBooleanFlags: [
"--quiet",
"--silent",
"--verbose",
"--zero-terminated",
"-q",
"-v",
"-z"
]
};
const SAFE_BIN_PROFILES = compileSafeBinProfiles({
jq: {
maxPositional: 1,
allowedValueFlags: [
"--arg",
"--argjson",
"--argstr"
],
deniedFlags: [
"--argfile",
"--rawfile",
"--slurpfile",
"--from-file",
"--library-path",
"-L",
"-f"
]
},
grep: {
maxPositional: 0,
allowedValueFlags: [
"--regexp",
"--max-count",
"--after-context",
"--before-context",
"--context",
"--devices",
"--binary-files",
"--exclude",
"--include",
"--label",
"-e",
"-m",
"-A",
"-B",
"-C",
"-D"
],
deniedFlags: [
"--file",
"--exclude-from",
"--dereference-recursive",
"--directories",
"--recursive",
"-f",
"-d",
"-r",
"-R"
]
},
cut: {
maxPositional: 0,
allowedValueFlags: [
"--bytes",
"--characters",
"--fields",
"--delimiter",
"--output-delimiter",
"-b",
"-c",
"-f",
"-d"
],
allowedBooleanFlags: [
"--complement",
"--only-delimited",
"--zero-terminated",
"-n",
"-s",
"-z"
]
},
sort: {
maxPositional: 0,
allowedValueFlags: [
"--key",
"--field-separator",
"--buffer-size",
"--parallel",
"--batch-size",
"-k",
"-t",
"-S"
],
deniedFlags: [
"--compress-program",
"--files0-from",
"--output",
"--random-source",
"--temporary-directory",
"-T",
"-o"
]
},
uniq: {
maxPositional: 0,
allowedValueFlags: [
"--skip-fields",
"--skip-chars",
"--check-chars",
"--group",
"-f",
"-s",
"-w"
],
allowedBooleanFlags: [
"--count",
"--repeated",
"--unique",
"--ignore-case",
"--zero-terminated",
"-c",
"-d",
"-u",
"-i",
"-z"
]
},
head: {
...HEAD_TAIL_PROFILE,
allowedValueFlags: [
"--lines",
"--bytes",
"-n",
"-c"
]
},
tail: {
...HEAD_TAIL_PROFILE,
allowedValueFlags: [
"--lines",
"--bytes",
"--sleep-interval",
"--max-unchanged-stats",
"--pid",
"-n",
"-c"
],
deniedFlags: [
"--follow",
"--retry",
"-F",
"-f"
]
},
tr: {
minPositional: 1,
maxPositional: 2,
allowedBooleanFlags: [
"--complement",
"--delete",
"--squeeze-repeats",
"--truncate-set1",
"-C",
"-c",
"-d",
"-s",
"-t"
]
},
wc: {
maxPositional: 0,
allowedBooleanFlags: [
"--bytes",
"--chars",
"--lines",
"--max-line-length",
"--words",
"-L",
"-c",
"-l",
"-m",
"-w"
],
deniedFlags: ["--files0-from"]
}
});
function normalizeSafeBinProfileName(raw) {
const name = normalizeLowercaseStringOrEmpty(raw);
return name.length > 0 ? name : null;
}
function normalizeFixtureLimit(raw) {
if (typeof raw !== "number" || !Number.isFinite(raw)) return;
const next = Math.trunc(raw);
return next >= 0 ? next : void 0;
}
function normalizeFixtureFlags(flags) {
if (!Array.isArray(flags) || flags.length === 0) return;
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 : void 0;
}
function normalizeSafeBinProfileFixture(fixture) {
const minPositional = normalizeFixtureLimit(fixture.minPositional);
const maxPositionalRaw = normalizeFixtureLimit(fixture.maxPositional);
return {
minPositional,
maxPositional: minPositional !== void 0 && maxPositionalRaw !== void 0 && maxPositionalRaw < minPositional ? minPositional : maxPositionalRaw,
allowedValueFlags: normalizeFixtureFlags(fixture.allowedValueFlags),
deniedFlags: normalizeFixtureFlags(fixture.deniedFlags)
};
}
function normalizeSafeBinProfileFixtures(fixtures) {
const normalized = {};
if (!fixtures) return normalized;
for (const [rawName, fixture] of Object.entries(fixtures)) {
const name = normalizeSafeBinProfileName(rawName);
if (!name) continue;
normalized[name] = normalizeSafeBinProfileFixture(fixture);
}
return normalized;
}
function resolveSafeBinProfiles(fixtures) {
const normalizedFixtures = normalizeSafeBinProfileFixtures(fixtures);
if (Object.keys(normalizedFixtures).length === 0) return SAFE_BIN_PROFILES;
return {
...SAFE_BIN_PROFILES,
...compileSafeBinProfiles(normalizedFixtures)
};
}
//#endregion
//#region src/infra/exec-safe-bin-semantics.ts
const UNSAFE_SAFE_BIN_WARNINGS = {
awk: "awk-family interpreters can execute commands, access ENVIRON, and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
jq: "jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
sed: "sed scripts can execute commands and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins."
};
const DENIED_SAFE_BIN_WARNINGS = /* @__PURE__ */ new Map([
["jq", UNSAFE_SAFE_BIN_WARNINGS.jq],
["awk", UNSAFE_SAFE_BIN_WARNINGS.awk],
["gawk", UNSAFE_SAFE_BIN_WARNINGS.awk],
["mawk", UNSAFE_SAFE_BIN_WARNINGS.awk],
["nawk", UNSAFE_SAFE_BIN_WARNINGS.awk],
["sed", UNSAFE_SAFE_BIN_WARNINGS.sed],
["gsed", UNSAFE_SAFE_BIN_WARNINGS.sed]
]);
/** Normalizes a configured safe-bin entry to its executable basename without Windows suffixes. */
function normalizeSafeBinName(raw) {
const trimmed = normalizeLowercaseStringOrEmpty(raw);
if (!trimmed) return "";
return (trimmed.split(/[\\/]/).at(-1) ?? trimmed).replace(/\.(?:exe|cmd|bat|com)$/i, "");
}
function getDeniedSafeBinWarning(binName) {
const normalized = typeof binName === "string" ? normalizeSafeBinName(binName) : "";
return DENIED_SAFE_BIN_WARNINGS.get(normalized);
}
/** Applies command-specific semantic gates for executables that are risky as broad safeBins. */
function validateSafeBinSemantics(params) {
return getDeniedSafeBinWarning(params.binName) === void 0;
}
/** Lists configured safeBins that need operator warnings because their semantics are broad. */
function listRiskyConfiguredSafeBins(entries) {
const hits = /* @__PURE__ */ new Map();
for (const entry of entries) {
const normalized = normalizeSafeBinName(entry);
if (!normalized || hits.has(normalized)) continue;
const warning = getDeniedSafeBinWarning(normalized);
if (!warning) continue;
hits.set(normalized, warning);
}
return Array.from(hits.entries()).map(([bin, warning]) => ({
bin,
warning
})).toSorted((a, b) => a.bin.localeCompare(b.bin));
}
//#endregion
//#region src/infra/exec-safe-bin-policy-validator.ts
function isPathLikeToken(value) {
const trimmed = value.trim();
return trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("~") || trimmed.startsWith("/") || /^[A-Za-z]:[\\/]/.test(trimmed);
}
function isSafeLiteralToken(value) {
return !/[*?[\]]/.test(value) && !/\$(?:[A-Za-z0-9_@*?!$#-]|\{|\(|\[)/.test(value) && !isPathLikeToken(value);
}
const NO_FLAGS = /* @__PURE__ */ new Set();
function validateSafeBinArgv(args, profile, options) {
const allowedValueFlags = profile.allowedValueFlags ?? NO_FLAGS;
const allowedBooleanFlags = profile.allowedBooleanFlags ?? NO_FLAGS;
const deniedFlags = profile.deniedFlags ?? NO_FLAGS;
const knownLongFlags = profile.knownLongFlags ?? collectKnownLongFlags(allowedValueFlags, deniedFlags, allowedBooleanFlags);
const knownLongFlagsSet = profile.knownLongFlagsSet ?? new Set(knownLongFlags);
const longFlagPrefixMap = profile.longFlagPrefixMap ?? buildLongFlagPrefixMap(knownLongFlags);
function consumeValue(index, inlineValue) {
if (inlineValue !== void 0) return isSafeLiteralToken(inlineValue) ? index + 1 : -1;
const value = args[index + 1];
return value && isSafeLiteralToken(value) ? index + 2 : -1;
}
const positional = [];
let optionsEnded = false;
let i = 0;
while (i < args.length) {
const token = parseExecArgvToken(args[i] ?? "");
if (token.kind === "empty" || token.kind === "stdin") {
i += 1;
continue;
}
if (optionsEnded || token.kind === "positional") {
if (!isSafeLiteralToken(token.raw)) return false;
positional.push(token.raw);
i += 1;
continue;
}
if (token.kind === "terminator") {
optionsEnded = true;
i += 1;
continue;
}
let nextIndex = i + 1;
if (token.style === "long") {
const flag = token.flag.length > 2 ? knownLongFlagsSet.has(token.flag) ? token.flag : longFlagPrefixMap.get(token.flag) : void 0;
if (!flag || deniedFlags.has(flag)) return false;
if (allowedValueFlags.has(flag)) nextIndex = consumeValue(i, token.inlineValue);
else if (token.inlineValue !== void 0) return false;
} else for (const [j, flag] of token.flags.entries()) {
if (deniedFlags.has(flag)) return false;
if (allowedValueFlags.has(flag)) {
nextIndex = consumeValue(i, token.cluster.slice(j + 1) || void 0);
break;
}
if (!allowedBooleanFlags.has(flag)) return false;
}
if (nextIndex < 0) return false;
i = nextIndex;
}
if (positional.length < (profile.minPositional ?? 0) || typeof profile.maxPositional === "number" && positional.length > profile.maxPositional) return false;
return validateSafeBinSemantics({
binName: options?.binName,
positional
});
}
//#endregion
//#region src/infra/exec-safe-bin-trust.ts
const DEFAULT_SAFE_BIN_TRUSTED_DIRS = ["/bin", "/usr/bin"];
function pathCaseInsensitive(value) {
let candidate = value;
for (;;) {
const swapped = swapAsciiCase(candidate);
if (swapped !== candidate) try {
const original = fs.statSync(candidate);
try {
const alternate = fs.statSync(swapped);
return sameFsObject(original, alternate);
} catch {
return false;
}
} catch {}
const parent = path.dirname(candidate);
if (parent === candidate) return process.platform === "win32";
candidate = parent;
}
}
function normalizeTrustComparisonPath(value) {
const resolved = path.resolve(value);
return pathCaseInsensitive(resolved) ? resolved.toLowerCase() : resolved;
}
function normalizeTrustedDir(value, forComparison = true) {
const trimmed = value.trim();
if (!trimmed) return null;
return forComparison ? normalizeTrustComparisonPath(trimmed) : path.resolve(trimmed);
}
function normalizeTrustedSafeBinDirs(entries) {
if (!Array.isArray(entries)) return [];
const normalized = entries.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
return uniqueStrings(normalized);
}
function resolveTrustedSafeBinDirs(entries, forComparison = true) {
const resolved = entries.map((entry) => normalizeTrustedDir(entry, forComparison)).filter((entry) => Boolean(entry));
return sortUniqueStrings(resolved);
}
function hasPathSelector(value) {
return value.includes("/") || value.includes("\\");
}
function isExecutableSafeBinFile(value) {
try {
if (!fs.statSync(value).isFile()) return false;
if (process.platform === "win32") return true;
fs.accessSync(value, fs.constants.X_OK);
return true;
} catch {
return false;
}
}
function resolveTrustedSafeBinTargetDirs(entries, safeBins, forComparison = true) {
const dirs = [];
const bins = Array.from(new Set(safeBins.map((entry) => entry.trim()).filter((entry) => entry && !hasPathSelector(entry)))).toSorted();
if (bins.length === 0) return dirs;
for (const entry of normalizeTrustedSafeBinDirs(entries)) {
const dir = path.resolve(entry);
for (const bin of bins) {
const candidate = path.join(dir, bin);
if (!isExecutableSafeBinFile(candidate)) continue;
try {
const normalized = normalizeTrustedDir(path.dirname(fs.realpathSync(candidate)), forComparison);
if (normalized) dirs.push(normalized);
} catch {}
}
}
return sortUniqueStrings(dirs);
}
function getTrustedSafeBinDirs(params = {}) {
const baseDirs = params.baseDirs ?? DEFAULT_SAFE_BIN_TRUSTED_DIRS;
const extraDirs = params.extraDirs ?? [];
const safeBins = params.safeBins ?? [];
const entries = [...normalizeTrustedSafeBinDirs(baseDirs), ...normalizeTrustedSafeBinDirs(extraDirs)];
const targetDirs = resolveTrustedSafeBinTargetDirs(entries, safeBins);
return /* @__PURE__ */ new Set([...resolveTrustedSafeBinDirs(entries), ...targetDirs]);
}
function isTrustedSafeBinPath(params) {
const trustedDirs = params.trustedDirs ?? getTrustedSafeBinDirs();
const resolvedDir = normalizeTrustComparisonPath(path.dirname(path.resolve(params.resolvedPath)));
return trustedDirs.has(resolvedDir);
}
function listWritableExplicitTrustedSafeBinDirs(entries) {
if (process.platform === "win32") return [];
const resolved = resolveTrustedSafeBinDirs(normalizeTrustedSafeBinDirs(entries), false);
const hits = [];
for (const dir of resolved) {
let stat;
try {
stat = fs.statSync(dir);
} catch {
continue;
}
if (!stat.isDirectory()) continue;
const mode = stat.mode & 511;
const groupWritable = (mode & 16) !== 0;
const worldWritable = (mode & 2) !== 0;
if (!groupWritable && !worldWritable) continue;
hits.push({
dir,
groupWritable,
worldWritable
});
}
return hits;
}
//#endregion
export { validateSafeBinArgv as a, DEFAULT_SAFE_BINS as c, resolveSafeBinProfiles as d, normalizeTrustedSafeBinDirs as i, SAFE_BIN_PROFILES as l, isTrustedSafeBinPath as n, listRiskyConfiguredSafeBins as o, listWritableExplicitTrustedSafeBinDirs as r, normalizeSafeBinName as s, getTrustedSafeBinDirs as t, normalizeSafeBinProfileFixtures as u };