Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions hooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,55 @@ if [[ -z "$expected_sha" || ! "$expected_sha" =~ ^[0-9a-f]{40}$ ]]; then
exit 2
fi

# Git exports GIT_DIR (and friends) into hook processes. In a linked worktree
# that value is an ABSOLUTE path, so a plain `git -C "$shared_dir" ...` still
# resolves against the pushing repository and reports ITS HEAD, origin and
# status instead of the pinned policy checkout's — the hook then refuses a
# perfectly good checkout ("Vana scanner checkout is at <repo HEAD>"). In a
# normal checkout GIT_DIR is the relative ".git", which happens to resolve
# correctly under -C, which is why this only bites worktrees. Scrub the
# inherited repository environment for commands that must target the checkout;
# commands that scan the pushing repository keep it.
# The scrub list comes from git itself (`--local-env-vars` covers GIT_DIR,
# GIT_WORK_TREE, GIT_INDEX_FILE, the object-directory pair, and crucially
# GIT_CONFIG_PARAMETERS / GIT_CONFIG_COUNT, which `git -c foo=bar push` exports
# into hooks and which would otherwise let the caller's environment satisfy the
# origin check below instead of the checkout's real config). The GIT_CONFIG_*
# file overrides are not in that list, so they are added explicitly, and a
# hardcoded fallback covers a git too old to answer.
_shared_git_scrub=()
while IFS= read -r _v; do
[[ -n "$_v" ]] && _shared_git_scrub+=(-u "$_v")
done < <(git rev-parse --local-env-vars 2>/dev/null || printf '%s\n' \
GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_OBJECT_DIRECTORY \
GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_COMMON_DIR \
GIT_CONFIG GIT_CONFIG_PARAMETERS GIT_CONFIG_COUNT)
for _v in GIT_CONFIG_GLOBAL GIT_CONFIG_SYSTEM GIT_CONFIG_NOSYSTEM; do
_shared_git_scrub+=(-u "$_v")
done
unset _v

shared_git() {
env "${_shared_git_scrub[@]}" git "$@"
}

scanner="$shared_dir/scripts/scan-commit-range.sh"
verifier="$shared_dir/scripts/verify-gitleaks.sh"
[[ -x "$scanner" && -x "$verifier" ]] || {
printf 'VANA_SECRET_SCAN_HOME is not a valid shared scanner checkout.\n' >&2
exit 2
}
actual_sha=$(git -C "$shared_dir" rev-parse HEAD)
actual_sha=$(shared_git -C "$shared_dir" rev-parse HEAD)
if [[ "$actual_sha" != "$expected_sha" ]]; then
printf 'Vana scanner checkout is at %s, expected %s.\n' "$actual_sha" "$expected_sha" >&2
exit 2
fi
origin_url=$(git -C "$shared_dir" config --get remote.origin.url || true)
origin_url=$(shared_git -C "$shared_dir" config --get remote.origin.url || true)
case "$origin_url" in
git@github.com:vana-com/.github|git@github.com:vana-com/.github.git|https://github.com/vana-com/.github|https://github.com/vana-com/.github.git) ;;
*) printf 'Vana scanner checkout origin is not vana-com/.github: %s\n' "${origin_url:-<unset>}" >&2; exit 2 ;;
esac
git -C "$shared_dir" diff --quiet -- . && git -C "$shared_dir" diff --cached --quiet -- . || {
shared_git -C "$shared_dir" diff --quiet -- . && shared_git -C "$shared_dir" diff --cached --quiet -- . || {
printf 'Vana scanner checkout has local changes: %s\n' "$shared_dir" >&2
exit 2
}
Expand Down
13 changes: 10 additions & 3 deletions scripts/install-gitleaks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,17 @@ trap 'rm -rf "$workdir"' EXIT
archive="$workdir/$asset"
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
"$RELEASE_BASE/$asset" --output "$archive"
if command -v sha256sum >/dev/null 2>&1; then
printf '%s %s\n' "$sha256" "$archive" | sha256sum --check --status
else
# macOS ships its own /sbin/sha256sum ("sha256sum (Darwin) 1.0") which does NOT
# accept the GNU long options below, so `command -v sha256sum` succeeds there
# and this verification fails for every download regardless of authenticity —
# the shasum fallback was never reachable on the platform that needed it.
# `shasum -a 256` accepts --check/--status on both macOS and Linux (and still
# exits non-zero on a checksum mismatch), so prefer it, keeping sha256sum for
# minimal images that ship coreutils but no perl.
if command -v shasum >/dev/null 2>&1; then
printf '%s %s\n' "$sha256" "$archive" | shasum -a 256 --check --status
else
printf '%s %s\n' "$sha256" "$archive" | sha256sum --check --status
fi
tar -xzf "$archive" -C "$workdir" gitleaks
install -m 0755 "$workdir/gitleaks" "$destination/gitleaks"
Expand Down
40 changes: 36 additions & 4 deletions scripts/install-pre-push.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,57 @@ esac
exit 2
}

git -C "$shared_dir" rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
# Git exports GIT_DIR (and friends) into hook processes. In a linked worktree
# that value is an ABSOLUTE path, so a plain `git -C "$shared_dir" ...` still
# resolves against the pushing repository and reports ITS HEAD, origin and
# status instead of the pinned policy checkout's — the hook then refuses a
# perfectly good checkout ("Vana scanner checkout is at <repo HEAD>"). In a
# normal checkout GIT_DIR is the relative ".git", which happens to resolve
# correctly under -C, which is why this only bites worktrees. Scrub the
# inherited repository environment for commands that must target the checkout;
# commands that scan the pushing repository keep it.
# The scrub list comes from git itself (`--local-env-vars` covers GIT_DIR,
# GIT_WORK_TREE, GIT_INDEX_FILE, the object-directory pair, and crucially
# GIT_CONFIG_PARAMETERS / GIT_CONFIG_COUNT, which `git -c foo=bar push` exports
# into hooks and which would otherwise let the caller's environment satisfy the
# origin check below instead of the checkout's real config). The GIT_CONFIG_*
# file overrides are not in that list, so they are added explicitly, and a
# hardcoded fallback covers a git too old to answer.
_shared_git_scrub=()
while IFS= read -r _v; do
[[ -n "$_v" ]] && _shared_git_scrub+=(-u "$_v")
done < <(git rev-parse --local-env-vars 2>/dev/null || printf '%s\n' \
GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_OBJECT_DIRECTORY \
GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_COMMON_DIR \
GIT_CONFIG GIT_CONFIG_PARAMETERS GIT_CONFIG_COUNT)
for _v in GIT_CONFIG_GLOBAL GIT_CONFIG_SYSTEM GIT_CONFIG_NOSYSTEM; do
_shared_git_scrub+=(-u "$_v")
done
unset _v

shared_git() {
env "${_shared_git_scrub[@]}" git "$@"
}

shared_git -C "$shared_dir" rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
printf 'Policy checkout is not a Git work tree: %s\n' "$shared_dir" >&2
exit 2
}
[[ -x "$shared_dir/hooks/pre-push" ]] || { printf 'No hook at: %s/hooks/pre-push\n' "$shared_dir" >&2; exit 2; }
[[ -x "$shared_dir/scripts/install-gitleaks.sh" ]] || { printf 'No installer at: %s/scripts/install-gitleaks.sh\n' "$shared_dir" >&2; exit 2; }
[[ -x "$shared_dir/scripts/verify-gitleaks.sh" ]] || { printf 'No verifier at: %s/scripts/verify-gitleaks.sh\n' "$shared_dir" >&2; exit 2; }
shared_dir=$(cd "$shared_dir" && pwd -P)
actual_sha=$(git -C "$shared_dir" rev-parse HEAD)
actual_sha=$(shared_git -C "$shared_dir" rev-parse HEAD)
[[ "$actual_sha" == "$expected_sha" ]] || {
printf 'Policy checkout is at %s, expected %s.\n' "$actual_sha" "$expected_sha" >&2
exit 2
}
origin_url=$(git -C "$shared_dir" config --get remote.origin.url || true)
origin_url=$(shared_git -C "$shared_dir" config --get remote.origin.url || true)
case "$origin_url" in
git@github.com:vana-com/.github|git@github.com:vana-com/.github.git|https://github.com/vana-com/.github|https://github.com/vana-com/.github.git) ;;
*) printf 'Policy checkout origin is not vana-com/.github: %s\n' "${origin_url:-<unset>}" >&2; exit 2 ;;
esac
if [[ -n "$(git -C "$shared_dir" status --porcelain --untracked-files=all -- ':!/.tools')" ]]; then
if [[ -n "$(shared_git -C "$shared_dir" status --porcelain --untracked-files=all -- ':!/.tools')" ]]; then
printf 'Policy checkout has local changes: %s\n' "$shared_dir" >&2
exit 2
fi
Expand Down
80 changes: 80 additions & 0 deletions tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,86 @@ if [[ -e "$test_root/external-tools/gitleaks/gitleaks" ]]; then
exit 1
fi

# Git exports the repository environment into hook processes. A command that
# must target the policy checkout has to ignore it: GIT_DIR is absolute in a
# linked worktree, so a bare `git -C "$policy" ...` reads the PUSHING repo's
# HEAD and origin instead, and GIT_CONFIG_PARAMETERS (exported by
# `git -c foo=bar push`) lets the caller's environment answer the origin check.
# Both are accepted inputs from an untrusted caller, so both are tested here.
env_policy="$test_root/env-policy"
git clone -q "$root" "$env_policy"
git -C "$env_policy" remote set-url origin https://github.com/vana-com/.github.git
git -C "$env_policy" checkout -q "$policy_sha"
"$env_policy/scripts/install-pre-push.sh" prepare --shared-dir "$env_policy" --ref "$policy_sha"

# A decoy repository standing in for the one being pushed: a different HEAD and
# a non-Vana origin, so leaking into it fails the SHA and origin checks.
decoy="$test_root/env-decoy"
git init -q -b main "$decoy"
git -C "$decoy" config user.name test
git -C "$decoy" config user.email test@example.invalid
git -C "$decoy" remote add origin https://example.invalid/pushing-repo.git
git -C "$decoy" commit --allow-empty -q -m decoy
decoy_git_dir=$(git -C "$decoy" rev-parse --absolute-git-dir)

# `prepare` only inspects the policy checkout, so it isolates the scrub from
# the target-repository resolution that legitimately reads the environment.
if ! GIT_DIR="$decoy_git_dir" \
"$env_policy/scripts/install-pre-push.sh" prepare --shared-dir "$env_policy" --ref "$policy_sha" >/dev/null 2>&1; then
printf 'expected prepare to resolve the policy checkout with GIT_DIR inherited\n' >&2
exit 1
fi

# The pre-push hook performs the same checks, so it must scrub the same way.
worktree_repo="$test_root/worktree-repo"
git init -q -b main "$worktree_repo"
git -C "$worktree_repo" config user.name test
git -C "$worktree_repo" config user.email test@example.invalid
git -C "$worktree_repo" commit --allow-empty -q -m base
"$root/scripts/install-pre-push.sh" --shared-dir "$env_policy" --repo "$worktree_repo" --ref "$policy_sha" >/dev/null
git -C "$worktree_repo" worktree add -q "$test_root/worktree-linked" -b linked
linked_git_dir=$(git -C "$test_root/worktree-linked" rev-parse --absolute-git-dir)
worktree_head=$(git -C "$test_root/worktree-linked" rev-parse HEAD)
if ! (cd "$test_root/worktree-linked" &&
GIT_DIR="$linked_git_dir" printf 'refs/heads/linked %s refs/heads/linked %040d\n' "$worktree_head" 0 |
GIT_DIR="$linked_git_dir" "$worktree_repo/.git/hooks/pre-push" origin example.invalid) >/dev/null 2>&1; then
printf 'expected pre-push hook to run from a linked worktree\n' >&2
exit 1
fi

# The origin check must read the checkout's real config, not the caller's
# environment. Point the checkout at an attacker origin and claim otherwise.
git -C "$env_policy" remote set-url origin https://github.com/attacker/evil.git
if GIT_CONFIG_COUNT=1 \
GIT_CONFIG_KEY_0=remote.origin.url \
GIT_CONFIG_VALUE_0=https://github.com/vana-com/.github.git \
"$env_policy/scripts/install-pre-push.sh" prepare --shared-dir "$env_policy" --ref "$policy_sha" >/dev/null 2>&1; then
printf 'expected prepare to refuse an origin spoofed through the environment\n' >&2
exit 1
fi
if GIT_CONFIG_PARAMETERS="'remote.origin.url=https://github.com/vana-com/.github.git'" \
"$env_policy/scripts/install-pre-push.sh" prepare --shared-dir "$env_policy" --ref "$policy_sha" >/dev/null 2>&1; then
printf 'expected prepare to refuse an origin spoofed through GIT_CONFIG_PARAMETERS\n' >&2
exit 1
fi
git -C "$env_policy" remote set-url origin https://github.com/vana-com/.github.git

# The download checksum gate must reject a corrupted archive on this platform,
# whichever of shasum/sha256sum install-gitleaks.sh selects.
checksum_digest=$(printf 'gitleaks' | { command -v shasum >/dev/null 2>&1 && shasum -a 256 || sha256sum; } | cut -d' ' -f1)
checksum_file="$test_root/checksum-probe"
printf 'gitleaks' >"$checksum_file"
if ! printf '%s %s\n' "$checksum_digest" "$checksum_file" |
{ command -v shasum >/dev/null 2>&1 && shasum -a 256 --check --status || sha256sum --check --status; }; then
printf 'expected the checksum tool to accept a matching digest\n' >&2
exit 1
fi
if printf '%s %s\n' "$(printf '0%.0s' {1..64})" "$checksum_file" |
{ command -v shasum >/dev/null 2>&1 && shasum -a 256 --check --status || sha256sum --check --status; } 2>/dev/null; then
printf 'expected the checksum tool to reject a mismatched digest\n' >&2
exit 1
fi

key='4f3c8b1a9e6d2c7f0b5e1d8a6c3f9b2e''7d4a1c8f5b0e6d3a9c2f7b4e1d8a6c3f'
scan() { "$scanner" --repo "$repo" --range "$1" --config "$config" --gitleaks "$gitleaks"; }
expect_scan_status() {
Expand Down
Loading