From 5dfbd07fef4f81bfc9633b64cd2eabf868f24da2 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 2 Apr 2026 14:57:45 -0700 Subject: [PATCH] [Infra] Add script to harden older remote branches Syncs CI/CD files from main and removes supply-chain attack surface files across all remote branches. Supports dry-run, filtering by date or branch pattern, and resume-from for large runs. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/harden_old_branches.sh | 320 +++++++++++++++++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100755 scripts/harden_old_branches.sh diff --git a/scripts/harden_old_branches.sh b/scripts/harden_old_branches.sh new file mode 100755 index 00000000000..75a5e207a15 --- /dev/null +++ b/scripts/harden_old_branches.sh @@ -0,0 +1,320 @@ +#!/usr/bin/env bash +# +# harden_old_branches.sh — Sync CI/CD and supply-chain-sensitive files from +# main to all remote branches, reducing attack surface on stale branches. +# +# What it does: +# 1. SYNCS files from main → overwrites the branch copy with main's version +# 2. DELETES files → removes files that should no longer exist +# 3. Commits & pushes → one commit per branch, only if there are changes +# +# Usage: +# ./scripts/harden_old_branches.sh [OPTIONS] +# +# Options: +# --dry-run Show what would be changed without pushing (default) +# --execute Actually push changes to remote branches +# --since YYYY-MM-DD Only process branches with commits after this date (default: all) +# --branch PATTERN Only process branches matching this grep pattern +# --exclude PATTERN Skip branches matching this grep pattern +# --max N Process at most N branches (useful for testing) +# --log FILE Append per-branch results to this file +# --resume-from NAME Skip branches until this one is found, then continue +# +# Prerequisites: +# - Run from repo root +# - git fetch origin must have been run recently +# - All CI/CD runs on non-main branches should be DISABLED before running with --execute +# +# Safety: +# - Uses a detached worktree so your working directory is never touched +# - Default mode is --dry-run +# - Skips branches where the commit would be empty (already up to date) +# - Skips protected branches (main, master, release/*) +# +set -euo pipefail + +# ── Configuration ───────────────────────────────────────────────────────── + +MAIN_REF="origin/main" + +# Files to sync from main (overwrite on branch with main's version) +SYNC_FILES=( + ".circleci/config.yml" + ".circleci/requirements.txt" + ".github/actions/helm-oci-chart-releaser/action.yml" + ".github/dependabot.yaml" + "docker/build_admin_ui.sh" + "ui/litellm-dashboard/build_ui.sh" + "ui/litellm-dashboard/build_ui_custom_path.sh" + "scripts/install.sh" +) + +# Directories to sync from main (entire directory replaced with main's version) +SYNC_DIRS=( + ".github/workflows" +) + +# Files to delete if they exist on the branch +DELETE_FILES=( + "ci_cd/publish-proxy-extras.sh" + ".pre-commit-config.yaml" + "ci_cd/security_scans.sh" + "ci_cd/.grype.yaml" + ".trivyignore" + "ui/litellm-dashboard/.trivyignore" + "docs/my-website/.trivyignore" +) + +COMMIT_MSG="[Infra] Harden branch: sync CI/CD files from main, remove attack surface + +Automated supply-chain hardening: +- Sync .circleci/, .github/, build scripts from main +- Remove dead/dangerous files (security_scans.sh, .pre-commit-config.yaml, etc.) +- SHA256-verified NVM installs, pip --only-binary" + +# Protected branch patterns (never touch these) +PROTECTED_PATTERNS="^(main|master|release/.*)$" + +# ── CLI Parsing ─────────────────────────────────────────────────────────── + +DRY_RUN=true +SINCE="" +BRANCH_FILTER="" +EXCLUDE_FILTER="" +MAX_BRANCHES=0 +LOG_FILE="" +RESUME_FROM="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=true; shift ;; + --execute) DRY_RUN=false; shift ;; + --since) SINCE="$2"; shift 2 ;; + --branch) BRANCH_FILTER="$2"; shift 2 ;; + --exclude) EXCLUDE_FILTER="$2"; shift 2 ;; + --max) MAX_BRANCHES="$2"; shift 2 ;; + --log) LOG_FILE="$2"; shift 2 ;; + --resume-from) RESUME_FROM="$2"; shift 2 ;; + -h|--help) + sed -n '2,/^$/p' "$0" | sed 's/^# \?//' + exit 0 + ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +# ── Preflight ───────────────────────────────────────────────────────────── + +if [[ ! -d .git && ! -f .git ]]; then + echo "ERROR: Run this from the repo root." >&2 + exit 1 +fi + +if $DRY_RUN; then + echo "=== DRY RUN MODE (use --execute to push) ===" + echo "" +fi + +# Fetch latest main +echo "Fetching latest main..." +git fetch origin main --quiet + +# ── Build branch list ───────────────────────────────────────────────────── + +echo "Building branch list..." + +if [[ -n "$SINCE" ]]; then + # Only branches with recent commits + BRANCHES=$(git for-each-ref --sort=-committerdate \ + --format='%(committerdate:short) %(refname:short)' refs/remotes/origin/ \ + | awk -v since="$SINCE" '$1 >= since { print $2 }') +else + BRANCHES=$(git for-each-ref --format='%(refname:short)' refs/remotes/origin/) +fi + +# Strip origin/ prefix, filter, dedupe +BRANCHES=$(echo "$BRANCHES" | sed 's|^origin/||' | sort -u) + +# Remove protected branches +BRANCHES=$(echo "$BRANCHES" | grep -vE "$PROTECTED_PATTERNS" || true) + +# Remove HEAD +BRANCHES=$(echo "$BRANCHES" | grep -v '^HEAD$' || true) + +# Apply user filters +if [[ -n "$BRANCH_FILTER" ]]; then + BRANCHES=$(echo "$BRANCHES" | grep "$BRANCH_FILTER" || true) +fi +if [[ -n "$EXCLUDE_FILTER" ]]; then + BRANCHES=$(echo "$BRANCHES" | grep -v "$EXCLUDE_FILTER" || true) +fi + +# Resume support +if [[ -n "$RESUME_FROM" ]]; then + BRANCHES=$(echo "$BRANCHES" | sed -n "/$RESUME_FROM/,\$p") +fi + +# Cap count +TOTAL=$(echo "$BRANCHES" | grep -c . || true) +if [[ "$MAX_BRANCHES" -gt 0 && "$TOTAL" -gt "$MAX_BRANCHES" ]]; then + BRANCHES=$(echo "$BRANCHES" | head -n "$MAX_BRANCHES") + TOTAL=$MAX_BRANCHES +fi + +echo "Found $TOTAL branches to process." +echo "" + +if [[ "$TOTAL" -eq 0 ]]; then + echo "Nothing to do." + exit 0 +fi + +# ── Setup worktree ──────────────────────────────────────────────────────── + +WORKTREE_DIR=$(mktemp -d "${TMPDIR:-/tmp}/harden-branches.XXXXXX") +trap 'echo "Cleaning up worktree..."; git worktree remove --force "$WORKTREE_DIR" 2>/dev/null || rm -rf "$WORKTREE_DIR"' EXIT + +# Create a detached worktree from main initially +git worktree add --detach "$WORKTREE_DIR" "$MAIN_REF" --quiet + +# ── Helper: log result ──────────────────────────────────────────────────── + +log_result() { + local branch="$1" status="$2" detail="$3" + local line="$(date -u +%Y-%m-%dT%H:%M:%SZ) | $status | $branch | $detail" + echo " [$status] $detail" + if [[ -n "$LOG_FILE" ]]; then + echo "$line" >> "$LOG_FILE" + fi +} + +# ── Process each branch ────────────────────────────────────────────────── + +COUNT=0 +CHANGED=0 +SKIPPED=0 +FAILED=0 + +for BRANCH in $BRANCHES; do + COUNT=$((COUNT + 1)) + echo "[$COUNT/$TOTAL] $BRANCH" + + # Try to check out the branch in the worktree + if ! git -C "$WORKTREE_DIR" checkout "origin/$BRANCH" --detach --quiet 2>/dev/null; then + log_result "$BRANCH" "SKIP" "could not checkout" + SKIPPED=$((SKIPPED + 1)) + continue + fi + + CHANGES_MADE=false + + # ── Sync individual files from main ── + for FILE in "${SYNC_FILES[@]}"; do + # Check if file exists on main + if ! git show "${MAIN_REF}:${FILE}" > /dev/null 2>&1; then + continue + fi + + # Check if file exists on branch AND is already identical to main + MAIN_HASH=$(git rev-parse "${MAIN_REF}:${FILE}" 2>/dev/null || true) + BRANCH_HASH=$(git rev-parse "origin/${BRANCH}:${FILE}" 2>/dev/null || true) + + if [[ "$MAIN_HASH" == "$BRANCH_HASH" ]]; then + continue # Already in sync + fi + + # File differs or doesn't exist on branch — sync it + mkdir -p "$WORKTREE_DIR/$(dirname "$FILE")" + git show "${MAIN_REF}:${FILE}" > "$WORKTREE_DIR/$FILE" + git -C "$WORKTREE_DIR" add "$FILE" + CHANGES_MADE=true + done + + # ── Sync directories from main ── + for DIR in "${SYNC_DIRS[@]}"; do + # Remove existing directory on branch (if any) so we get an exact copy + if [[ -d "$WORKTREE_DIR/$DIR" ]]; then + rm -rf "$WORKTREE_DIR/$DIR" + fi + mkdir -p "$WORKTREE_DIR/$DIR" + + # Extract all files from main for this directory + MAIN_DIR_FILES=$(git ls-tree -r --name-only "${MAIN_REF}" "$DIR" 2>/dev/null || true) + if [[ -z "$MAIN_DIR_FILES" ]]; then + continue + fi + + for FILE in $MAIN_DIR_FILES; do + mkdir -p "$WORKTREE_DIR/$(dirname "$FILE")" + git show "${MAIN_REF}:${FILE}" > "$WORKTREE_DIR/$FILE" + done + git -C "$WORKTREE_DIR" add "$DIR" + + # Check if anything actually changed + if ! git -C "$WORKTREE_DIR" diff --cached --quiet -- "$DIR"; then + CHANGES_MADE=true + fi + done + + # ── Delete files ── + for FILE in "${DELETE_FILES[@]}"; do + if [[ -f "$WORKTREE_DIR/$FILE" ]]; then + rm -f "$WORKTREE_DIR/$FILE" + git -C "$WORKTREE_DIR" add "$FILE" + CHANGES_MADE=true + fi + done + + # ── Commit & push ── + if ! $CHANGES_MADE; then + # Double-check with git + if git -C "$WORKTREE_DIR" diff --cached --quiet 2>/dev/null; then + log_result "$BRANCH" "SKIP" "already up to date" + SKIPPED=$((SKIPPED + 1)) + continue + fi + fi + + # Check if there are actually staged changes + if git -C "$WORKTREE_DIR" diff --cached --quiet 2>/dev/null; then + log_result "$BRANCH" "SKIP" "already up to date" + SKIPPED=$((SKIPPED + 1)) + continue + fi + + if $DRY_RUN; then + DIFF_STAT=$(git -C "$WORKTREE_DIR" diff --cached --stat | tail -1) + log_result "$BRANCH" "DRY-RUN" "would change: $DIFF_STAT" + # Reset for next branch + git -C "$WORKTREE_DIR" reset --quiet HEAD 2>/dev/null || true + CHANGED=$((CHANGED + 1)) + else + # Commit + git -C "$WORKTREE_DIR" commit --quiet -m "$COMMIT_MSG" --author="litellm-bot " + + # Push the detached HEAD to the remote branch + COMMIT_SHA=$(git -C "$WORKTREE_DIR" rev-parse HEAD) + if git push origin "$COMMIT_SHA:refs/heads/$BRANCH" --quiet 2>/dev/null; then + log_result "$BRANCH" "OK" "pushed" + CHANGED=$((CHANGED + 1)) + else + log_result "$BRANCH" "FAIL" "push rejected (branch may be protected)" + FAILED=$((FAILED + 1)) + fi + fi +done + +# ── Summary ─────────────────────────────────────────────────────────────── + +echo "" +echo "=== Summary ===" +echo " Processed: $COUNT" +echo " Changed: $CHANGED" +echo " Skipped: $SKIPPED" +echo " Failed: $FAILED" + +if $DRY_RUN && [[ "$CHANGED" -gt 0 ]]; then + echo "" + echo "Run with --execute to apply these changes." +fi