mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-07 06:12:44 +02:00
91 lines
2.5 KiB
TypeScript
91 lines
2.5 KiB
TypeScript
|
|
import path from "path";
|
||
|
|
import fs from "fs";
|
||
|
|
import { WorkDir } from "./config.js";
|
||
|
|
|
||
|
|
export const SECURITY_CONFIG_PATH = path.join(WorkDir, "config", "security.json");
|
||
|
|
|
||
|
|
const DEFAULT_ALLOW_LIST = ["ls", "pwd", "cat", "echo", "whoami"];
|
||
|
|
|
||
|
|
let cachedAllowList: string[] | null = null;
|
||
|
|
let cachedMtimeMs: number | null = null;
|
||
|
|
|
||
|
|
function ensureSecurityConfig() {
|
||
|
|
if (!fs.existsSync(SECURITY_CONFIG_PATH)) {
|
||
|
|
fs.writeFileSync(
|
||
|
|
SECURITY_CONFIG_PATH,
|
||
|
|
JSON.stringify(DEFAULT_ALLOW_LIST, null, 2) + "\n",
|
||
|
|
"utf8",
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeList(commands: unknown[]): string[] {
|
||
|
|
const seen = new Set<string>();
|
||
|
|
for (const entry of commands) {
|
||
|
|
if (typeof entry !== "string") continue;
|
||
|
|
const normalized = entry.trim().toLowerCase();
|
||
|
|
if (!normalized) continue;
|
||
|
|
seen.add(normalized);
|
||
|
|
}
|
||
|
|
|
||
|
|
return Array.from(seen);
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseSecurityPayload(payload: unknown): string[] {
|
||
|
|
if (Array.isArray(payload)) {
|
||
|
|
return normalizeList(payload);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (payload && typeof payload === "object") {
|
||
|
|
const maybeObject = payload as Record<string, unknown>;
|
||
|
|
if (Array.isArray(maybeObject.allowedCommands)) {
|
||
|
|
return normalizeList(maybeObject.allowedCommands);
|
||
|
|
}
|
||
|
|
|
||
|
|
const dynamicList = Object.entries(maybeObject)
|
||
|
|
.filter(([, value]) => Boolean(value))
|
||
|
|
.map(([key]) => key);
|
||
|
|
|
||
|
|
return normalizeList(dynamicList);
|
||
|
|
}
|
||
|
|
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
function readAllowList(): string[] {
|
||
|
|
ensureSecurityConfig();
|
||
|
|
|
||
|
|
try {
|
||
|
|
const configContent = fs.readFileSync(SECURITY_CONFIG_PATH, "utf8");
|
||
|
|
const parsed = JSON.parse(configContent);
|
||
|
|
return parseSecurityPayload(parsed);
|
||
|
|
} catch (error) {
|
||
|
|
console.warn(`Failed to read security config at ${SECURITY_CONFIG_PATH}: ${error instanceof Error ? error.message : error}`);
|
||
|
|
return DEFAULT_ALLOW_LIST;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getSecurityAllowList(): string[] {
|
||
|
|
ensureSecurityConfig();
|
||
|
|
try {
|
||
|
|
const stats = fs.statSync(SECURITY_CONFIG_PATH);
|
||
|
|
if (cachedAllowList && cachedMtimeMs === stats.mtimeMs) {
|
||
|
|
return cachedAllowList;
|
||
|
|
}
|
||
|
|
|
||
|
|
const allowList = readAllowList();
|
||
|
|
cachedAllowList = allowList;
|
||
|
|
cachedMtimeMs = stats.mtimeMs;
|
||
|
|
return allowList;
|
||
|
|
} catch {
|
||
|
|
cachedAllowList = null;
|
||
|
|
cachedMtimeMs = null;
|
||
|
|
return readAllowList();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function resetSecurityAllowListCache() {
|
||
|
|
cachedAllowList = null;
|
||
|
|
cachedMtimeMs = null;
|
||
|
|
}
|