feat: secrets verify — manifest↔store consistency + decrypt integrity (EGB-698)

Read-only integrity check, the safety net for the stage-2 store migration.

Default mode (current project) cross-checks $PWD/.secrets.json against the
store both ways — declared-but-missing blobs and orphaned blobs (no manifest
entry) — and decrypt-tests every dotenv + external blob with the current key,
streaming plaintext to /dev/null so nothing is ever written to disk. `verify
--all` decrypt-tests every blob in every project (integrity only; the store
carries no manifests, so consistency can't be checked store-wide). Both
recurse the whole project tree (find -type f), the same walk rekey/list use,
so nested manifest blobs are covered. Exits non-zero on any finding so it can
gate `migrate --finalize` and CI.

12 bats tests (clean, nested+external, missing blob, decrypt failure, orphan,
missing external, no-manifest die, symlink refusal, --all clean/corrupt/orphan,
nested decrypt failure). Full suite 205/205. bash 3.2 clean.
This commit is contained in:
Brian Majewski 2026-06-07 14:55:42 -07:00
parent fb71b956da
commit 52528f2e06
4 changed files with 291 additions and 1 deletions

View file

@ -731,3 +731,132 @@ m_nojq_path() {
[ "$status" -eq 0 ]
[[ "$output" == *"packages/web/.env.development"* ]] || false
}
# ─── K: secrets verify — manifest↔store consistency + decrypt integrity (EGB-698) ─
@test "verify: clean pushed project reports OK and exits 0" {
init_with_remote
create_project_dir verifyok
"$SECRETS_BIN" push >/dev/null 2>&1
run "$SECRETS_BIN" verify
[ "$status" -eq 0 ]
[[ "$output" == *"OK"* ]] || false
}
@test "verify: nested + external entries all pass" {
init_with_remote
m_gradle_src $'beaconClerkPkTest=pk_test_abc\n'
create_project_dir verifymix
mkdir -p packages/web
echo "N=nested" > packages/web/.env.development
"$SECRETS_BIN" add packages/web/.env.development >/dev/null
printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > .secrets-files
"$SECRETS_BIN" push >/dev/null 2>&1
run "$SECRETS_BIN" verify
[ "$status" -eq 0 ]
}
@test "verify: declared-but-missing blob is a finding (exit 1)" {
init_with_remote
create_project_dir verifymiss
"$SECRETS_BIN" push >/dev/null 2>&1
rm "$SECRETS_DIR/verifymiss/.env.staging.age"
run "$SECRETS_BIN" verify
[ "$status" -eq 1 ]
[[ "$output" == *".env.staging"* ]] || false
}
@test "verify: a blob that fails to decrypt is a finding (exit 1)" {
init_with_remote
create_project_dir verifycorrupt
"$SECRETS_BIN" push >/dev/null 2>&1
printf 'not-a-valid-age-blob' > "$SECRETS_DIR/verifycorrupt/.env.age"
run "$SECRETS_BIN" verify
[ "$status" -eq 1 ]
[[ "$output" == *".env"* ]] || false
}
@test "verify: orphan blob (no manifest entry) is a finding (exit 1)" {
init_with_remote
create_project_dir verifyorphan
"$SECRETS_BIN" push >/dev/null 2>&1
# A valid, decryptable blob with no manifest entry — pure consistency miss.
cp "$SECRETS_DIR/verifyorphan/.env.age" "$SECRETS_DIR/verifyorphan/.stray.age"
run "$SECRETS_BIN" verify
[ "$status" -eq 1 ]
[[ "$output" == *"stray"* ]] || false
}
@test "verify: missing external blob is a finding (exit 1)" {
init_with_remote
m_gradle_src $'beaconClerkPkTest=pk_test_abc\n'
create_project_dir verifyextmiss
printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > .secrets-files
"$SECRETS_BIN" push >/dev/null 2>&1
rm "$SECRETS_DIR/verifyextmiss/external/"*.age
run "$SECRETS_BIN" verify
[ "$status" -eq 1 ]
}
@test "verify: no manifest in cwd dies with a directed message" {
init_with_remote
local dir="$WORK_DIR/verifynomani"; mkdir -p "$dir"; cd "$dir"
run "$SECRETS_BIN" verify
[ "$status" -eq 1 ]
[[ "$output" == *".secrets.json"* ]] || false
[[ "$output" == *"--all"* ]] || false
}
@test "verify: symlinked manifest is refused" {
init_with_remote
create_project_dir verifysymlink
"$SECRETS_BIN" push >/dev/null 2>&1
rm .secrets.json
ln -s /etc/hosts .secrets.json
run "$SECRETS_BIN" verify
[ "$status" -eq 1 ]
[[ "$output" == *"symlink"* ]] || false
}
@test "verify --all: clean store passes (decrypt-only)" {
init_with_remote
create_project_dir verifyall1
"$SECRETS_BIN" push >/dev/null 2>&1
cd "$WORK_DIR"
run "$SECRETS_BIN" verify --all
[ "$status" -eq 0 ]
}
@test "verify --all: a corrupted blob anywhere fails (exit 1)" {
init_with_remote
create_project_dir verifyall2
"$SECRETS_BIN" push >/dev/null 2>&1
printf 'garbage' > "$SECRETS_DIR/verifyall2/.env.age"
cd "$WORK_DIR"
run "$SECRETS_BIN" verify --all
[ "$status" -eq 1 ]
}
@test "verify --all: an orphan that decrypts is NOT flagged (no consistency check)" {
init_with_remote
create_project_dir verifyall3
"$SECRETS_BIN" push >/dev/null 2>&1
# Orphan blob that decrypts fine — default mode flags it, --all does not.
cp "$SECRETS_DIR/verifyall3/.env.age" "$SECRETS_DIR/verifyall3/.stray.age"
cd "$WORK_DIR"
run "$SECRETS_BIN" verify --all
[ "$status" -eq 0 ]
}
@test "verify: nested blob that fails to decrypt is caught (rekey-orphan guard)" {
init_with_remote
create_project_dir verifynested
mkdir -p packages/web
echo "N=nested" > packages/web/.env.development
"$SECRETS_BIN" add packages/web/.env.development >/dev/null
"$SECRETS_BIN" push >/dev/null 2>&1
printf 'broken' > "$SECRETS_DIR/verifynested/packages/web/.env.development.age"
run "$SECRETS_BIN" verify
[ "$status" -eq 1 ]
[[ "$output" == *"packages/web/.env.development"* ]] || false
}