import { parse } from "@aliou/sh"; import { maybePathLike } from "../../src/core/paths"; import { walkCommands, wordToString } from "../../src/core/shell"; import { expandGlob, hasGlobChars } from "../../src/shared/glob"; import { translateMsysPath } from "../../src/shared/paths"; import type { CompiledPolicy } from "./rules"; import { normalizeTarget } from "./rules"; async function expandCandidate(candidate: string): Promise { if (!hasGlobChars(candidate)) return [candidate]; const matches = await expandGlob(candidate); return matches.length > 0 ? matches : [candidate]; } export async function extractTargets( event: { toolName: string; input: Record }, cwd: string, policies: CompiledPolicy[], ): Promise { if ( ["read", "write", "edit", "grep", "find", "ls"].includes(event.toolName) ) { const target = String( event.input.file_path ?? event.input.path ?? "", ).trim(); return target ? [target] : []; } if (event.toolName !== "bash") return []; const command = String(event.input.command ?? ""); const targets = new Set(); const maybeAdd = async (candidate: string, sourced = false) => { if (!candidate || candidate.startsWith("-")) return; // The file argument of the shell `source` / `.` builtin is exempt from // policies that opt in via allowSourcing: the values become env vars // without the contents entering context. const applicable = sourced ? policies.filter((policy) => !policy.allowSourcing) : policies; for (const file of await expandCandidate(candidate)) { // Git Bash on Windows emits MSYS forms (/c/Users/..., /tmp/...); // translate before normalizing so policies check the real file, and // record the translated form so downstream rule checks resolve it too. const translated = translateMsysPath(file); const normalized = normalizeTarget(translated, cwd); if ( applicable.some((policy) => policy.patterns.some((pattern) => pattern.test(normalized)), ) ) { targets.add(translated); } } }; try { const { ast } = parse(command); const pending: Promise[] = []; walkCommands(ast, (cmd) => { const words = (cmd.words ?? []).map(wordToString); const args = words.slice(1); const sourcedFile = words[0] === "source" || words[0] === "." ? args.find((word) => word && !word.startsWith("-")) : undefined; for (const word of args) { pending.push(maybeAdd(word, word === sourcedFile)); } for (const redir of cmd.redirects ?? []) { pending.push(maybeAdd(wordToString(redir.target))); } return false; }); await Promise.all(pending); } catch { const tokenRegex = /"([^"]+)"|'([^']+)'|`([^`]+)`|([^\s"'`<>|;&]+)/g; for (const match of command.matchAll(tokenRegex)) { const token = match[1] ?? match[2] ?? match[3] ?? match[4] ?? ""; if (maybePathLike(token)) await maybeAdd(token); } } return [...targets]; }