Route git push and gh pr through the sandbox supervisor broker so project computers never receive GITHUB_TOKEN/GH_TOKEN. Remove NOPASSWD sudo from the computer image. Localize awareness strip copy via Lingui and wire computer-image-tools.test.sh into the validate-computer-image job. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
export type GitShellPolicyInput = {
|
|
command: string;
|
|
defaultBranch: string | null | undefined;
|
|
gitPushApprovedThisRun: boolean;
|
|
};
|
|
|
|
export type GitShellPolicyDecision =
|
|
| { action: "allow" }
|
|
| { action: "block"; reason: string }
|
|
| { action: "require_approval"; reason: string };
|
|
|
|
const GIT_PUSH_RE = /\bgit\b[^\n;&|]*\bpush\b/i;
|
|
const GIT_FORCE_PUSH_RE = /(?:--force-with-lease|--force(?:\s|$)|(?:^|\s)-f(?:\s|$))/i;
|
|
const GH_PR_RE = /\bgh\b(?:(?:\s+[^;&|]+)*\s+pr\b|\s+pr\b)(?:\s+create\b|\s|$|[;&|])/i;
|
|
|
|
export function evaluateGitShellPolicy(input: GitShellPolicyInput): GitShellPolicyDecision {
|
|
const command = input.command.trim();
|
|
if (!command) return { action: "allow" };
|
|
|
|
if (GIT_PUSH_RE.test(command)) {
|
|
if (GIT_FORCE_PUSH_RE.test(command)) {
|
|
return {
|
|
action: "block",
|
|
reason: "Force-push is not allowed. Push to a new branch without force flags.",
|
|
};
|
|
}
|
|
const targetBranch = parseGitPushRef(command);
|
|
const protectedBranch = normalizeBranchName(input.defaultBranch);
|
|
if (protectedBranch && targetBranch && targetBranch === protectedBranch) {
|
|
return {
|
|
action: "block",
|
|
reason: `Push to the default branch (${protectedBranch}) is not allowed. Create a feature branch and push that instead.`,
|
|
};
|
|
}
|
|
if (!input.gitPushApprovedThisRun) {
|
|
return {
|
|
action: "require_approval",
|
|
reason:
|
|
"This run would push commits to a remote. Approve once to allow git push in this run.",
|
|
};
|
|
}
|
|
}
|
|
|
|
if (GH_PR_RE.test(command) && !input.gitPushApprovedThisRun) {
|
|
return {
|
|
action: "require_approval",
|
|
reason:
|
|
"Opening a pull request is a consequential git action. Approve once to allow gh pr in this run.",
|
|
};
|
|
}
|
|
|
|
return { action: "allow" };
|
|
}
|
|
|
|
function normalizeBranchName(value: string | null | undefined): string | null {
|
|
const trimmed = value?.trim();
|
|
if (!trimmed || trimmed === "HEAD") return null;
|
|
return trimmed.replace(/^origin\//, "");
|
|
}
|
|
|
|
/** Best-effort parse of the ref git push would update on the remote. */
|
|
export function parseGitPushRef(command: string): string | null {
|
|
const tokens = command
|
|
.replace(/&&/g, ";")
|
|
.replace(/\|\|/g, ";")
|
|
.split(/[;&|]/)
|
|
.flatMap((segment) => segment.trim().split(/\s+/))
|
|
.filter(Boolean);
|
|
const pushIndex = tokens.indexOf("push");
|
|
if (pushIndex < 0 || !tokens.slice(0, pushIndex).includes("git")) return null;
|
|
const args = tokens.slice(pushIndex + 1).filter((token) => !token.startsWith("-"));
|
|
if (args.length === 0) return null;
|
|
const last = args.at(-1);
|
|
if (!last) return null;
|
|
if (last.includes(":")) {
|
|
const [, dest] = last.split(":");
|
|
return normalizeBranchName(dest) ?? null;
|
|
}
|
|
if (args.length >= 2) return normalizeBranchName(last);
|
|
return normalizeBranchName(last);
|
|
}
|
|
|
|
export function gitNetworkArgv(argv: string[]): boolean {
|
|
if (argv.length === 0 || argv[0] !== "git") return false;
|
|
const sub = argv[1];
|
|
return (
|
|
sub === "push" ||
|
|
sub === "fetch" ||
|
|
sub === "pull" ||
|
|
sub === "clone" ||
|
|
sub === "ls-remote"
|
|
);
|
|
}
|
|
|
|
export function ghBrokerArgv(argv: string[]): boolean {
|
|
if (argv.length === 0 || argv[0] !== "gh") return false;
|
|
const joined = argv.join(" ");
|
|
return GH_PR_RE.test(joined);
|
|
}
|