From 5b7845caede81f6c63ce70c82d03e27183f82fb7 Mon Sep 17 00:00:00 2001 From: johnson Date: Tue, 28 Jul 2026 15:07:11 +0000 Subject: [PATCH] fix: fail closed on hook inspection errors --- README.md | 2 +- hooks/encryption.template | 24 +++++++++++++-- hooks/obfuscation.template | 8 ++++- test/integration.bats | 58 +++++++++++++++++++++++++++++++++++++ test/obfuscation-hooks.bats | 32 ++++++++++++++++++++ 5 files changed, 119 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 03286fa..3875ff0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **Collective memory, encrypted.** -[![tests: 428](https://img.shields.io/badge/tests-428-brightgreen?style=flat)](test/) +[![tests: 431](https://img.shields.io/badge/tests-431-brightgreen?style=flat)](test/) ![lints: 8](https://img.shields.io/badge/lints-8-blue?style=flat) [![license: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE) diff --git a/hooks/encryption.template b/hooks/encryption.template index e62b464..fdc3534 100644 --- a/hooks/encryption.template +++ b/hooks/encryption.template @@ -15,20 +15,38 @@ if ! command -v git-crypt &>/dev/null; then exit 0 fi +staged_input=$(mktemp) || { + echo "ERROR: Failed to create encryption hook staged-path input" >&2 + exit 1 +} +attribute_input=$(mktemp) || { + rm -f "$staged_input" + echo "ERROR: Failed to create encryption hook attribute input" >&2 + exit 1 +} +trap 'rm -f "$staged_input" "$attribute_input"' EXIT + # Staged adds/copies/modifies/renames, NUL-delimited for path safety. +if ! git diff --cached --name-only --diff-filter=ACMR -z > "$staged_input"; then + echo "ERROR: Could not inspect staged paths for encryption" >&2 + exit 1 +fi staged=() -while IFS= read -r -d '' path; do staged+=("$path"); done \ - < <(git diff --cached --name-only --diff-filter=ACMR -z) +while IFS= read -r -d '' path; do staged+=("$path"); done < "$staged_input" [ "${#staged[@]}" -eq 0 ] && exit 0 # Which staged paths are under an encrypted (filter=git-crypt) pattern? # check-attr -z emits NUL-delimited triples. +if ! git check-attr --cached -z filter -- "${staged[@]}" > "$attribute_input"; then + echo "ERROR: Could not inspect staged encryption attributes" >&2 + exit 1 +fi encrypted_staged=() while IFS= read -r -d '' path \ && IFS= read -r -d '' _attr \ && IFS= read -r -d '' value; do [ "$value" = "git-crypt" ] && encrypted_staged+=("$path") -done < <(git check-attr --cached -z filter -- "${staged[@]}") +done < "$attribute_input" [ "${#encrypted_staged[@]}" -eq 0 ] && exit 0 verify_encrypted_paths "${encrypted_staged[@]}" diff --git a/hooks/obfuscation.template b/hooks/obfuscation.template index 03ae35d..06e05f6 100644 --- a/hooks/obfuscation.template +++ b/hooks/obfuscation.template @@ -20,15 +20,21 @@ run_notes() { build_staged_obfuscation_plan() { local candidates="$1" plan="$2" + local staged="$candidates.staged" local file relpath : > "$candidates" + if ! git diff --cached --name-only --diff-filter=ACMR > "$staged"; then + rm -f "$staged" + return 1 + fi while IFS= read -r file; do [[ "$file" != "$NOTES_ROOT/"* ]] && continue relpath="${file#"$NOTES_ROOT/"}" [[ "$relpath" == ".manifest" ]] && continue printf '%s\n' "$relpath" >> "$candidates" - done < <(git diff --cached --name-only --diff-filter=ACMR) + done < "$staged" + rm -f "$staged" classify_obfuscation_candidates \ "$NOTES_ROOT" "$NOTES_ROOT/.manifest" "$candidates" "$plan" diff --git a/test/integration.bats b/test/integration.bats index d8ecb8f..11525df 100644 --- a/test/integration.bats +++ b/test/integration.bats @@ -567,6 +567,64 @@ SH [ "$(wc -l < "$calls" | tr -d ' ')" -eq 1 ] } +@test "encryption hook fails closed when staged-path inspection fails (#49)" { + notes setup --yes + + printf '%s\n' "public" > "$TARGET_DIR/public.md" + git -C "$TARGET_DIR" add public.md + + local mock_bin="$BATS_TEST_TMPDIR/failing-encryption-diff-bin" + local real_git + real_git=$(command -v git) + mkdir -p "$mock_bin" + cat > "$mock_bin/git" <<'SH' +#!/usr/bin/env bash +if [ "${1:-}" = "diff" ]; then + printf '%s\n' "staged inspection failed" >&2 + exit 73 +fi +exec "$REAL_GIT" "$@" +SH + chmod +x "$mock_bin/git" + export PATH="$mock_bin:$PATH" + export REAL_GIT="$real_git" + + run_encryption_hook + [ "$status" -eq 1 ] + [[ "$output" == *"staged inspection failed"* ]] + [[ "$output" == *"Could not inspect staged paths for encryption"* ]] +} + +@test "encryption hook fails closed when staged-attribute inspection fails (#49)" { + notes setup --yes + + mkdir -p "$TARGET_DIR/notes" + printf '%s\n' "secret" > "$TARGET_DIR/notes/secret.md" + git -C "$TARGET_DIR" add notes/secret.md + + local mock_bin="$BATS_TEST_TMPDIR/failing-encryption-attr-bin" + local real_git + real_git=$(command -v git) + mkdir -p "$mock_bin" + cat > "$mock_bin/git" <<'SH' +#!/usr/bin/env bash +if [ "${1:-}" = "check-attr" ]; then + printf '%s\n' "attribute inspection failed" >&2 + exit 74 +fi +exec "$REAL_GIT" "$@" +SH + chmod +x "$mock_bin/git" + export PATH="$mock_bin:$PATH" + export REAL_GIT="$real_git" + + run_encryption_hook + [ "$status" -eq 1 ] + [[ "$output" == *"attribute inspection failed"* ]] + [[ "$output" == *"Could not inspect staged encryption attributes"* ]] +} + + @test "encryption hook falls back per path after a batched plaintext result (#49)" { notes setup --yes local fpr diff --git a/test/obfuscation-hooks.bats b/test/obfuscation-hooks.bats index c0319a9..d1d4ccf 100644 --- a/test/obfuscation-hooks.bats +++ b/test/obfuscation-hooks.bats @@ -198,6 +198,38 @@ SH } +@test "obfuscation hook fails closed when staged-path inspection fails" { + notes setup --yes + git -C "$NOTES_CALLER_PWD" add -A + git -C "$NOTES_CALLER_PWD" commit --no-verify -q -m "setup" + + printf '%s\n' "# Delta" > "$NOTES_CALLER_PWD/notes/delta.md" + git -C "$NOTES_CALLER_PWD" add notes/delta.md + + local mock_bin="$BATS_TEST_TMPDIR/failing-obfuscation-git-bin" + local real_git + real_git=$(command -v git) + mkdir -p "$mock_bin" + cat > "$mock_bin/git" <<'SH' +#!/usr/bin/env bash +if [ "${1:-}" = "diff" ]; then + printf '%s\n' "staged inspection failed" >&2 + exit 73 +fi +exec "$REAL_GIT" "$@" +SH + chmod +x "$mock_bin/git" + + run env PATH="$mock_bin:$PATH" REAL_GIT="$real_git" \ + bash -c 'cd "$1" && .git/hooks/pre-commit.d/obfuscation' \ + _ "$NOTES_CALLER_PWD" + + [ "$status" -eq 1 ] + [[ "$output" == *"staged inspection failed"* ]] + [[ "$output" == *"Could not classify staged note filenames"* ]] +} + + @test "auto hook fails closed when a staged readable file is missing from disk" { notes setup --yes git -C "$NOTES_CALLER_PWD" add -A