#!/usr/bin/env bash
# ci-secret-scan — install gitleaks and scan for leaked secrets in CI or locally.
#
# Copy this file to .github/ci/secret-scan in consumer repositories. The body is
# the org-wide canonical implementation; sync via scripts/github-repo-lint.
# Bump ci_secret_scan_version when scripts/github-repo-lint reports an eligible
# newer gitleaks release (same release-age gate as dep-updater).
#
# Environment:
#   GITLEAKS_VERSION          Pin override (default: 8.30.1)
#   GITLEAKS_BASE             PR base SHA for diff scan (optional)
#   GITLEAKS_HEAD             PR head SHA for diff scan (optional)
#   CI_SECRET_SCAN_BIN_DIR    Install directory (default: /usr/local/bin; no sudo when set)

set -euo pipefail

ci_secret_scan_version='8.30.1'
readonly ci_secret_scan_version

ci_secret_scan_download_retries=3
readonly ci_secret_scan_download_retries

ci_secret_scan_gitleaks_leak_exit=1
readonly ci_secret_scan_gitleaks_leak_exit

ci_secret_scan_repo_dir() {
  local script_dir
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  cd "${script_dir}/../.." && pwd
}

ci_secret_scan_linux_arch() {
  local arch
  arch="$(uname -m)"
  case "$arch" in
    x86_64) printf '%s\n' 'x64' ;;
    aarch64 | arm64) printf '%s\n' 'arm64' ;;
    *)
      printf 'ERROR: unsupported architecture for gitleaks: %s\n' "$arch" >&2
      return 1
      ;;
  esac
}

ci_secret_scan_install_gitleaks() {
  local arch version url tmpdir tarball dest_dir attempt http_code
  arch="$(ci_secret_scan_linux_arch)"
  version="${GITLEAKS_VERSION:-$ci_secret_scan_version}"
  url="https://github.com/gitleaks/gitleaks/releases/download/v${version}/gitleaks_${version}_linux_${arch}.tar.gz"
  tmpdir="$(mktemp -d)"
  tarball="${tmpdir}/gitleaks.tar.gz"
  dest_dir="${CI_SECRET_SCAN_BIN_DIR:-/usr/local/bin}"

  for attempt in $(seq 1 "$ci_secret_scan_download_retries"); do
    http_code=''
    http_code="$(curl -fSL --retry 2 --retry-delay 1 -o "$tarball" -w '%{http_code}' "$url" 2>/dev/null || true)"
    if [[ "$http_code" == '200' ]] \
      && [[ -s "$tarball" ]] \
      && file -b "$tarball" | grep -qi 'gzip'; then
      if tar -tzf "$tarball" gitleaks >/dev/null 2>&1; then
        tar -xzf "$tarball" -C "$tmpdir" gitleaks
        if [[ "$dest_dir" == '/usr/local/bin' ]]; then
          sudo install -m 0755 "${tmpdir}/gitleaks" "${dest_dir}/gitleaks"
        else
          mkdir -p "$dest_dir"
          install -m 0755 "${tmpdir}/gitleaks" "${dest_dir}/gitleaks"
          PATH="${dest_dir}:${PATH}"
          export PATH
        fi
        rm -rf "$tmpdir"
        return 0
      fi
    fi
    rm -f "$tarball"
    if (( attempt < ci_secret_scan_download_retries )); then
      sleep $((attempt * 2))
    fi
  done

  rm -rf "$tmpdir"
  printf 'ERROR: gitleaks download failed after %s attempts (url=%s)\n' \
    "$ci_secret_scan_download_retries" "$url" >&2
  return 1
}

ci_secret_scan_resolve_repository() {
  local repo_dir="$1"
  local remote

  if [[ -n "${GITHUB_REPOSITORY:-}" ]]; then
    printf '%s\n' "$GITHUB_REPOSITORY"
    return 0
  fi
  remote="$(git -C "$repo_dir" remote get-url origin 2>/dev/null || true)"
  if [[ "$remote" =~ github\.com[:/]([^/]+/[^/.]+)(\.git)?$ ]]; then
    printf '%s\n' "${BASH_REMATCH[1]}"
    return 0
  fi
  return 1
}

ci_secret_scan_resolve_branch() {
  local repo_dir="$1"
  local branch

  if [[ -n "${GITHUB_HEAD_REF:-}" ]]; then
    printf '%s\n' "$GITHUB_HEAD_REF"
    return 0
  fi
  if [[ "${GITHUB_REF:-}" =~ ^refs/heads/(.+)$ ]]; then
    printf '%s\n' "${BASH_REMATCH[1]}"
    return 0
  fi
  branch="$(git -C "$repo_dir" branch --show-current 2>/dev/null || true)"
  if [[ -n "$branch" ]]; then
    printf '%s\n' "$branch"
    return 0
  fi
  return 1
}

# Prints remediation when gitleaks reports leaks (exit 1). CI scans run after push, so
# secrets may already exist in remote git objects; this is pre-merge triage, not prevention.
ci_secret_scan_print_mitigation() {
  local repo_dir="$1"
  local repository branch

  printf '\n' >&2
  printf 'ERROR: SECRET_SCAN_LEAK gitleaks reported one or more secrets\n' >&2
  printf '\n' >&2
  printf 'Workflow secret scanning runs after push. Leaked material may already be in\n' >&2
  printf 'remote git objects for this branch. Use this job for pre-merge triage and\n' >&2
  printf 'backfill detection — not as the primary prevention layer.\n' >&2
  printf '\n' >&2
  printf 'Mitigation (complete all steps):\n' >&2
  printf '  1. Revoke and rotate every credential gitleaks reported (assume compromise).\n' >&2
  printf '  2. Close the PR without merging. Do not merge until a clean branch replaces it.\n' >&2
  if repository="$(ci_secret_scan_resolve_repository "$repo_dir")" \
    && branch="$(ci_secret_scan_resolve_branch "$repo_dir")"; then
    printf '  3. Delete the remote branch:\n' >&2
    printf '       git push origin --delete %s\n' "$branch" >&2
    printf '     or:\n' >&2
    printf '       gh api -X DELETE repos/%s/git/refs/heads/%s\n' "$repository" "$branch" >&2
    printf '  4. Delete the local branch and return to the default branch:\n' >&2
    printf '       git switch main && git branch -D %s\n' "$branch" >&2
  else
    printf '  3. Delete the remote branch that introduced the secret:\n' >&2
    printf '       git push origin --delete <branch>\n' >&2
    printf '  4. Delete the local branch and return to the default branch:\n' >&2
    printf '       git switch main && git branch -D <branch>\n' >&2
  fi
  printf '  5. Remove the secret from your working tree, then open a new PR from a fresh\n' >&2
  printf '     branch (never recommit the same credential).\n' >&2
  printf '  6. If the secret reached main, treat it as an incident: rotate again, audit\n' >&2
  printf '     access logs, and coordinate history rewrite only with org owners.\n' >&2
  printf '\n' >&2
  printf 'Prevention before the next push:\n' >&2
  printf '  - Run scripts/dev/secret-scan (repository-helpers) or gitleaks locally\n' >&2
  printf '  - Add a pre-push hook / IDE scan where practical\n' >&2
  printf '  - Enable GitHub push protection and org secret scanning where available\n' >&2
  printf '\n' >&2
}

ci_secret_scan_run() {
  local repo_dir base head scan_rc
  repo_dir="$1"
  base="${GITLEAKS_BASE:-}"
  head="${GITLEAKS_HEAD:-}"
  scan_rc=0

  if [[ -n "$base" && -n "$head" ]]; then
    gitleaks detect --source "$repo_dir" --log-opts "${base}..${head}" || scan_rc=$?
  else
    gitleaks detect --source "$repo_dir" || scan_rc=$?
  fi

  if (( scan_rc == ci_secret_scan_gitleaks_leak_exit )); then
    ci_secret_scan_print_mitigation "$repo_dir"
  fi
  return "$scan_rc"
}

ci_secret_scan_main() {
  local repo_dir
  repo_dir="$(ci_secret_scan_repo_dir)"

  if ! command -v gitleaks >/dev/null 2>&1; then
    ci_secret_scan_install_gitleaks
  fi

  ci_secret_scan_run "$repo_dir"
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  ci_secret_scan_main
fi
