feat(backup): add audited backup listing and restoration
This commit is contained in:
+37
-4
@@ -5,11 +5,44 @@ command_name=${0##*/}
|
||||
printf '%q' "$command_name" >>"$TEST_MOCK_LOG"
|
||||
for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done
|
||||
printf '\n' >>"$TEST_MOCK_LOG"
|
||||
[[ $# == 2 && $1 == -n && $2 =~ ^[0-9]+$ ]] || exit 99
|
||||
if [[ -n ${MOCK_FLOCK_CHANGE_TO:-} && -z ${MOCK_FLOCK_CHANGED_MARKER:-} && -f $MOCK_FLOCK_CHANGE_TO && -n ${WG_MANAGER_CONFIG:-} ]]; then
|
||||
cp -- "$MOCK_FLOCK_CHANGE_TO" "$WG_MANAGER_CONFIG"
|
||||
export MOCK_FLOCK_CHANGED_MARKER=1
|
||||
fi
|
||||
if [[ ${MOCK_FLOCK_BUSY:-0} == 1 ]]; then exit 1; fi
|
||||
if [[ ${MOCK_FLOCK_FAIL:-0} == 1 ]]; then exit 97; fi
|
||||
exit 0
|
||||
if [[ $# == 2 && $1 == -n && $2 =~ ^[0-9]+$ ]]; then
|
||||
[[ ${MOCK_FLOCK_BUSY:-0} == 1 ]] && exit 1
|
||||
[[ ${MOCK_FLOCK_FAIL:-0} == 1 ]] && exit 97
|
||||
exit 0
|
||||
fi
|
||||
if [[ $# -ge 3 && $1 == -n && $2 =~ ^[0-9]+$ && ( $3 == printf || $3 == cat ) ]]; then
|
||||
[[ ${MOCK_FLOCK_BUSY:-0} == 1 ]] && exit 1
|
||||
[[ ${MOCK_FLOCK_FAIL:-0} == 1 ]] && exit 97
|
||||
shift 2
|
||||
command "$@"
|
||||
exit $?
|
||||
fi
|
||||
if [[ $# == 1 && $1 =~ ^[0-9]+$ || $# == 2 && $1 == -s && $2 =~ ^[0-9]+$ ]]; then
|
||||
[[ ${MOCK_FLOCK_BUSY:-0} == 1 ]] && exit 1
|
||||
[[ ${MOCK_FLOCK_FAIL:-0} == 1 ]] && exit 97
|
||||
real_flock=${REAL_FLOCK:-}
|
||||
if [[ -z $real_flock ]]; then
|
||||
for candidate in /usr/bin/flock /bin/flock; do
|
||||
if [[ -x $candidate ]]; then real_flock=$candidate; break; fi
|
||||
done
|
||||
fi
|
||||
[[ -n $real_flock ]] || exit 98
|
||||
exec "$real_flock" "$@"
|
||||
fi
|
||||
if [[ $# -ge 2 && ( $1 == -s || $1 != -* ) && ${1#-} =~ ^[0-9]+$ || $1 == -s && ${2:-} =~ ^[0-9]+$ ]]; then
|
||||
[[ ${MOCK_FLOCK_BUSY:-0} == 1 ]] && exit 1
|
||||
[[ ${MOCK_FLOCK_FAIL:-0} == 1 ]] && exit 97
|
||||
real_flock=${REAL_FLOCK:-}
|
||||
if [[ -z $real_flock ]]; then
|
||||
for candidate in /usr/bin/flock /bin/flock; do
|
||||
if [[ -x $candidate ]]; then real_flock=$candidate; break; fi
|
||||
done
|
||||
fi
|
||||
[[ -n $real_flock ]] || exit 98
|
||||
exec "$real_flock" "$@"
|
||||
fi
|
||||
exit 99
|
||||
|
||||
+227
-3
@@ -11,7 +11,7 @@ source "${PROJECT_DIR}/wg-manager.sh"
|
||||
|
||||
case $# in
|
||||
0) ;;
|
||||
1) case $1 in config|parser|status|list|list-redirected|list-deterministic|transaction|rollback|locking|backups|mutation-preserves-source|secret-modes|add-client|address-allocation|add-collisions|add-rollback|no-secret-logs|add-golden-config|add-invalid-names|show-client|config-export|qr|path-safety|no-secret-default-output|remove-client|remove-cancel|remove-exact-block|remove-rollback|archive-client|archive-path-safety|remove-preserves-neighbors|diagnostics|diagnostics-systemd|diagnostics-firewall|diagnostics-forwarding|diagnostics-runtime-diff|diagnostics-read-only|diagnostics-exit-codes) ;; *) printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64 ;; esac ;;
|
||||
1) case $1 in config|parser|status|list|list-redirected|list-deterministic|transaction|rollback|locking|backups|mutation-preserves-source|secret-modes|add-client|address-allocation|add-collisions|add-rollback|no-secret-logs|add-golden-config|add-invalid-names|show-client|config-export|qr|path-safety|no-secret-default-output|remove-client|remove-cancel|remove-exact-block|remove-rollback|archive-client|archive-path-safety|remove-preserves-neighbors|diagnostics|diagnostics-systemd|diagnostics-firewall|diagnostics-forwarding|diagnostics-runtime-diff|diagnostics-read-only|diagnostics-exit-codes|audit|audit-no-secrets|backup-list|restore|restore-invalid|restore-rollback|backup-path-safety|runtime-file-modes) ;; *) printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64 ;; esac ;;
|
||||
*) printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64 ;;
|
||||
esac
|
||||
test_group=${1-}
|
||||
@@ -913,7 +913,7 @@ run_m4_tests() {
|
||||
assert_contains '192.0.2.3/32' "$(<"${CLIENT_DIR}/alice.conf")" 'allocated address'
|
||||
assert_contains 'PresharedKey = GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG=' "$(<"$fixture")" 'server peer preshared key'
|
||||
assert_equal '' "$(grep -E 'EEEE|FFFF|GGGG|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' "$TEST_MOCK_LOG" || true)" 'mock log without key material'
|
||||
local -a add_log_lines=(); mapfile -t add_log_lines <"$TEST_MOCK_LOG"
|
||||
local -a add_log_lines=(); mapfile -t add_log_lines < <(grep -E '^(flock -n [0-9]+$|wg show wg0 (public-key|dump)$|wg genkey$|wg pubkey$|wg genpsk$|wg-quick strip |wg syncconf )' "$TEST_MOCK_LOG")
|
||||
assert_equal 9 "${#add_log_lines[@]}" 'полный порядок add внешних вызовов'
|
||||
assert_equal 1 "$(grep -c '^wg show wg0 public-key$' "$TEST_MOCK_LOG" || true)" 'ровно один server public-key'
|
||||
assert_equal 1 "$(grep -c '^wg genkey$' "$TEST_MOCK_LOG" || true)" 'ровно один genkey'
|
||||
@@ -2029,7 +2029,7 @@ run_m3_tests() {
|
||||
assert_contains $'wg-quick strip ' "$transaction_log" 'argv strip'
|
||||
assert_contains $'wg syncconf wg0 ' "$transaction_log" 'argv syncconf'
|
||||
assert_contains $'wg show wg0 dump' "$transaction_log" 'argv verification dump'
|
||||
mapfile -t transaction_lines <"$TEST_MOCK_LOG"
|
||||
mapfile -t transaction_lines < <(grep -E '^(flock -n [0-9]+$|wg-quick strip |wg syncconf |wg show )' "$TEST_MOCK_LOG")
|
||||
assert_equal 5 "${#transaction_lines[@]}" 'точно пять внешних transaction вызовов'
|
||||
assert_equal 1 "$([[ ${transaction_lines[0]} =~ ^flock\ -n\ [0-9]+$ ]] && printf 1 || printf 0)" 'первый вызов flock'
|
||||
assert_equal 1 "$([[ ${transaction_lines[1]} == 'wg-quick strip '* && ${transaction_lines[2]} == 'wg-quick strip '* ]] && printf 1 || printf 0)" 'два strip подряд'
|
||||
@@ -2788,6 +2788,223 @@ EOF
|
||||
fi
|
||||
}
|
||||
|
||||
run_m8_tests() {
|
||||
local config_file=$TEST_ROOT/m8.conf fixture=$TEST_ROOT/m8-wg.conf output status backup audit_content audit_text first_backup second_backup replacement restore_before valid_count manual_now newest_stamp manifest_replacement retained manifest_before_hash
|
||||
cp -- "$PROJECT_DIR/tests/fixtures/m3/source.conf" "$fixture"; chmod 600 -- "$fixture"
|
||||
write_m2_config "$config_file" "$fixture"
|
||||
replace_config_line "$config_file" 'CLIENT_DIR=' "CLIENT_DIR=\"$TEST_ROOT/m8-clients\""
|
||||
export WG_MANAGER_TEST_MODE=1 WG_MANAGER_CONFIG=$config_file WG_MANAGER_BACKUP_DIR=$TEST_ROOT/m8-backups WG_MANAGER_LOG_DIR=$TEST_ROOT/m8-logs WG_MANAGER_METADATA_DIR=$TEST_ROOT/m8-meta WG_MANAGER_ARCHIVE_DIR=$TEST_ROOT/m8-archive WG_MANAGER_LOCK_FILE=$TEST_ROOT/m8.lock WG_MANAGER_NOW=1700000100 WG_MOCK_INTERFACE=wg0
|
||||
mkdir -p -- "$TEST_ROOT/m8-backups" "$TEST_ROOT/m8-logs" "$TEST_ROOT/m8-meta" "$TEST_ROOT/m8-clients" "$TEST_ROOT/m8-archive"
|
||||
chmod 700 -- "$TEST_ROOT/m8-backups" "$TEST_ROOT/m8-logs" "$TEST_ROOT/m8-meta" "$TEST_ROOT/m8-clients" "$TEST_ROOT/m8-archive"
|
||||
set_runtime_paths; load_config
|
||||
transaction_builder_copy() { cp -- "$WG_CONF" "$1"; }
|
||||
transaction_builder_fail() { return "$EXIT_VALIDATION"; }
|
||||
case $test_group in
|
||||
audit)
|
||||
begin_test 'audit covers transaction backup apply and rollback outcomes'
|
||||
export WG_MOCK_DUMP_FILE="$PROJECT_DIR/tests/fixtures/m3/runtime.dump"
|
||||
: >"$TEST_MOCK_LOG"
|
||||
set +e; run_transaction update audit transaction_builder_copy >/dev/null 2>&1; status=$?; set -e
|
||||
assert_equal 0 "$status" 'successful transaction is audited'
|
||||
audit_text=$(<"$LOG_DIR/wg-manager.log")
|
||||
assert_equal 1 "$(grep -c '^.*operation=backup .*outcome=success ' <<<"$audit_text")" 'backup event is emitted once'
|
||||
assert_equal 1 "$(grep -c '^.*operation=apply .*outcome=success ' <<<"$audit_text")" 'apply event is emitted once'
|
||||
export WG_SYNC_FAIL_ONCE_FILE="$TEST_ROOT/m8-sync-fail"
|
||||
set +e; run_transaction update audit transaction_builder_copy >/dev/null 2>&1; status=$?; set -e
|
||||
unset WG_SYNC_FAIL_ONCE_FILE
|
||||
assert_equal "$EXIT_OPERATION" "$status" 'failed apply with successful rollback keeps operation status'
|
||||
assert_contains 'operation=rollback' "$(<"$LOG_DIR/wg-manager.log")" 'successful rollback is audited'
|
||||
export WG_SYNC_FAIL_ONCE_FILE="$TEST_ROOT/m8-sync-fail-critical" TRANSACTION_ROLLBACK_REPLACE_FAIL=1
|
||||
set +e; run_transaction update audit transaction_builder_copy >/dev/null 2>&1; status=$?; set -e
|
||||
unset WG_SYNC_FAIL_ONCE_FILE TRANSACTION_ROLLBACK_REPLACE_FAIL
|
||||
assert_equal "$EXIT_ROLLBACK" "$status" 'rollback failure keeps critical status'
|
||||
assert_contains 'operation=rollback' "$(<"$LOG_DIR/wg-manager.log")" 'critical rollback is audited'
|
||||
assert_equal '' "$(grep -E 'PrivateKey|PresharedKey|EEEE|GGGG|BBBBBBBBBBBBBBBB' "$LOG_DIR/wg-manager.log" || true)" 'transaction audit contains no secrets'
|
||||
: >"$LOG_DIR/wg-manager.log"
|
||||
set +e; run_transaction update preflight transaction_builder_fail >/dev/null 2>&1; status=$?; set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" 'pre-apply builder failure retains validation status'
|
||||
assert_equal 0 "$(grep -Ec 'operation=(apply|rollback) ' "$LOG_DIR/wg-manager.log" || true)" 'pre-apply failure emits no apply or rollback event'
|
||||
set +e; output=$(list_audit 1 2>&1); status=$?; set -e
|
||||
assert_equal 0 "$status" 'audit listing accepts limit one'
|
||||
assert_equal 1 "$(grep -c '^timestamp=' <<<"$output")" 'audit listing limit one returns one record'
|
||||
set +e; list_audit 0 >/dev/null 2>&1; status=$?; set -e
|
||||
assert_equal "$EXIT_USAGE" "$status" 'audit listing rejects zero limit'
|
||||
set +e; list_audit 1001 >/dev/null 2>&1; status=$?; set -e
|
||||
assert_equal "$EXIT_USAGE" "$status" 'audit listing rejects excessive limit'
|
||||
: >"$LOG_DIR/wg-manager.log"
|
||||
local audit_index audit_pid audit_wait_status
|
||||
local -a audit_pids=()
|
||||
for audit_index in {1..10}; do
|
||||
( AUDIT_CLIENT=worker write_audit_event audit "worker${audit_index}" success success ) & audit_pids+=($!)
|
||||
done
|
||||
audit_wait_status=0
|
||||
for audit_pid in "${audit_pids[@]}"; do wait "$audit_pid" || audit_wait_status=1; done
|
||||
assert_equal 0 "$audit_wait_status" 'concurrent audit writers all complete'
|
||||
assert_equal 10 "$(grep -Ec '^timestamp=.* operation=audit target=worker[0-9]+ ' "$LOG_DIR/wg-manager.log" || true)" 'concurrent audit records remain complete'
|
||||
assert_equal 8 "$(awk 'length($0)>0 {print NF}' "$LOG_DIR/wg-manager.log" | sort -u | tr -d '\n')" 'concurrent audit records have stable field counts'
|
||||
;;
|
||||
audit-no-secrets)
|
||||
begin_test 'audit rejects unsafe fields and filters hostile records'
|
||||
set +e; write_audit_event 'bad operation' alice success success >/dev/null 2>&1; status=$?; set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" 'unsafe operation is rejected'
|
||||
audit_content=''; [[ -e $LOG_DIR/wg-manager.log ]] && audit_content=$(grep -E 'PrivateKey|PresharedKey|\[Interface\]|\[Peer\]' "$LOG_DIR/wg-manager.log" || true)
|
||||
assert_equal '' "$audit_content" 'audit output has no config content'
|
||||
printf '%s\n' 'PrivateKey=EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=' 'malformed hostile line' >"$LOG_DIR/wg-manager.log"
|
||||
set +e; output=$(list_audit 2>&1); status=$?; set -e
|
||||
assert_equal 0 "$status" 'malformed audit input is handled safely'
|
||||
assert_equal '' "$(grep -E 'PrivateKey|EEEE|malformed' <<<"$output" || true)" 'hostile audit input is never emitted'
|
||||
assert_equal '' "$(grep -E 'PrivateKey|PresharedKey|EEEE|GGGG|BBBBBBBBBBBBBBBB' <<<"$output" || true)" 'filtered audit output is secret-free'
|
||||
;;
|
||||
backup-list)
|
||||
begin_test 'backup listing returns valid newest entries and ignores hostile entries'
|
||||
backup=$(create_backup "$WG_CONF" manual "$WG_INTERFACE")
|
||||
first_backup=${backup##*/}
|
||||
export WG_MANAGER_NOW=1700000200
|
||||
backup=$(create_backup "$WG_CONF" manual "$WG_INTERFACE")
|
||||
second_backup=${backup##*/}
|
||||
export WG_MANAGER_NOW=1700000100
|
||||
mkdir -- "$BACKUP_DIR/20231114T221500Z-wg0-777"; chmod 700 -- "$BACKUP_DIR/20231114T221500Z-wg0-777"
|
||||
printf 'INTERFACE=wg0\nTIMESTAMP=20231114T221500Z\nOPERATION=manual\nTARGET=bad\nSOURCE_MODE=600\nSOURCE_UID=0\nSOURCE_GID=0\nCONFIG=wg.conf\nEXEC=touch %s\n' "$TEST_ROOT/m8-hostile-sentinel" >"$BACKUP_DIR/20231114T221500Z-wg0-777/manifest"; chmod 600 -- "$BACKUP_DIR/20231114T221500Z-wg0-777/manifest"
|
||||
set +e; output=$(list_backups 2>&1); status=$?; set -e
|
||||
assert_equal 0 "$status" 'backup list succeeds'
|
||||
assert_contains "$first_backup" "$output" 'first valid backup appears in list'
|
||||
assert_contains "$second_backup" "$output" 'newer valid backup appears in list'
|
||||
assert_equal 0 "$(grep -c '20231114T221500Z-wg0-777' <<<"$output")" 'invalid backup name is not listed'
|
||||
assert_file_absent "$TEST_ROOT/m8-hostile-sentinel" 'manifest payload is never executed'
|
||||
assert_equal 0 "$(grep -c 'Резервные копии отсутствуют' <<<"$output")" 'valid list is not reported empty'
|
||||
mv -- "$BACKUP_DIR" "$TEST_ROOT/m8-backups-missing"
|
||||
set +e; output=$(list_backups 2>&1); status=$?; set -e
|
||||
assert_equal 0 "$status" 'missing backup root is an empty list'
|
||||
assert_contains 'Резервные копии отсутствуют' "$output" 'missing backup root is reported safely'
|
||||
mv -- "$TEST_ROOT/m8-backups-missing" "$BACKUP_DIR"
|
||||
manifest_replacement=$TEST_ROOT/m8-manifest-replacement
|
||||
manifest_before_hash=$(sha256sum -- "$BACKUP_DIR/$first_backup/manifest" | awk '{print $1}')
|
||||
cp -- "$BACKUP_DIR/$first_backup/manifest" "$manifest_replacement"
|
||||
replace_config_line "$manifest_replacement" 'TARGET=' 'TARGET=swapped'
|
||||
export LIST_BACKUPS_REPLACE_MANIFEST=$manifest_replacement
|
||||
set +e; output=$(list_backups 2>&1); status=$?; set -e
|
||||
unset LIST_BACKUPS_REPLACE_MANIFEST LIST_BACKUPS_REPLACE_DONE
|
||||
assert_equal 0 "$status" 'backup list rejects a swapped manifest safely'
|
||||
assert_equal 0 "$(grep -Ec '(^|[[:space:]])swapped([[:space:]]|$)' <<<"$output")" 'swapped manifest fields are never displayed'
|
||||
assert_equal "$manifest_before_hash" "$(sha256sum -- "$BACKUP_DIR/$first_backup/manifest" | awk '{print $1}')" 'swapped manifest is restored byte-for-byte'
|
||||
;;
|
||||
restore)
|
||||
begin_test 'restore applies pinned backup and creates current-state backup'
|
||||
local old_config restore_id restored_hash current_backup_count
|
||||
old_config=$TEST_ROOT/m8-old.conf; cp -- "$fixture" "$old_config"
|
||||
backup=$(create_backup "$fixture" manual "$WG_INTERFACE"); restore_id=${backup##*/}
|
||||
printf '# changed-live-state\n' >>"$fixture"
|
||||
export WG_MOCK_DUMP_FILE="$PROJECT_DIR/tests/fixtures/m3/runtime.dump"
|
||||
set +e; output=$(restore_backup "$restore_id" --yes 2>&1); status=$?; set -e
|
||||
assert_equal 0 "$status" 'restore succeeds'
|
||||
assert_equal "$(sha256sum -- "$old_config" | awk '{print $1}')" "$(sha256sum -- "$fixture" | awk '{print $1}')" 'restore replaces live config with backup bytes'
|
||||
assert_contains "operation=restore" "$(<"$LOG_DIR/wg-manager.log")" 'restore audit is emitted'
|
||||
current_backup_count=$(find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d -name '*-wg0-*' | wc -l)
|
||||
assert_equal 1 "$(( current_backup_count >= 2 ? 1 : 0 ))" 'restore keeps source and current-state backups'
|
||||
assert_contains 'Резервная копия:' "$output" 'restore preview identifies timestamp'
|
||||
;;
|
||||
restore-invalid)
|
||||
begin_test 'restore rejects unsafe identifiers and noninteractive confirmation'
|
||||
local invalid_restore_status restore_id
|
||||
for invalid_id in '../unsafe' '-bad' '2023X114T221500Z-wg0-001' 'nested/item'; do
|
||||
set +e; restore_backup "$invalid_id" --yes >/dev/null 2>&1; invalid_restore_status=$?; set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$invalid_restore_status" "restore rejects $invalid_id"
|
||||
done
|
||||
backup=$(create_backup "$fixture" manual "$WG_INTERFACE"); restore_id=${backup##*/}
|
||||
set +e; restore_backup "$restore_id" >/dev/null 2>&1; invalid_restore_status=$?; set -e
|
||||
assert_equal "$EXIT_USAGE" "$invalid_restore_status" 'restore requires --yes without a terminal'
|
||||
cp -- "$fixture" "$TEST_ROOT/m8-invalid-live.conf"
|
||||
replace_config_line "$backup/manifest" 'INTERFACE=' 'INTERFACE=wg9'
|
||||
set +e; restore_backup "$restore_id" --yes >/dev/null 2>&1; invalid_restore_status=$?; set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$invalid_restore_status" 'restore rejects a backup for another interface'
|
||||
cp -- "$backup/manifest" "$TEST_ROOT/m8-invalid-manifest"
|
||||
replace_config_line "$TEST_ROOT/m8-invalid-manifest" 'INTERFACE=' "INTERFACE=$WG_INTERFACE"
|
||||
cp -- "$TEST_ROOT/m8-invalid-manifest" "$backup/manifest"
|
||||
printf '[Broken]\n' >>"$backup/wg.conf"
|
||||
set +e; restore_backup "$restore_id" --yes >/dev/null 2>&1; invalid_restore_status=$?; set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$invalid_restore_status" 'restore rejects a malformed backup config'
|
||||
assert_equal "$(sha256sum -- "$TEST_ROOT/m8-invalid-live.conf" | awk '{print $1}')" "$(sha256sum -- "$fixture" | awk '{print $1}')" 'invalid restore leaves live config unchanged'
|
||||
;;
|
||||
restore-rollback)
|
||||
begin_test 'restore apply failure rolls back disk and reports audit outcome'
|
||||
local restore_source restore_id rollback_before
|
||||
restore_source=$TEST_ROOT/m8-rollback.conf; cp -- "$fixture" "$restore_source"
|
||||
backup=$(create_backup "$fixture" manual "$WG_INTERFACE"); restore_id=${backup##*/}
|
||||
printf '# restore-candidate\n' >>"$fixture"; rollback_before=$(sha256sum -- "$fixture")
|
||||
export WG_SYNC_FAIL_ONCE_FILE="$TEST_ROOT/m8-restore-sync-fail" WG_MOCK_DUMP_FILE="$PROJECT_DIR/tests/fixtures/m3/runtime.dump"
|
||||
set +e; output=$(restore_backup "$restore_id" --yes 2>&1); status=$?; set -e
|
||||
unset WG_SYNC_FAIL_ONCE_FILE
|
||||
assert_equal "$EXIT_OPERATION" "$status" 'restore apply failure returns operation status'
|
||||
assert_equal "$rollback_before" "$(sha256sum -- "$fixture")" 'failed restore restores live disk'
|
||||
assert_contains 'operation=rollback' "$(<"$LOG_DIR/wg-manager.log")" 'restore rollback is audited'
|
||||
backup=$(create_backup "$fixture" manual "$WG_INTERFACE"); restore_id=${backup##*/}
|
||||
printf '# restore-critical-candidate\n' >>"$fixture"
|
||||
export WG_SYNC_FAIL_ONCE_FILE="$TEST_ROOT/m8-restore-sync-fail-critical" TRANSACTION_ROLLBACK_REPLACE_FAIL=1
|
||||
set +e; output=$(restore_backup "$restore_id" --yes 2>&1); status=$?; set -e
|
||||
unset WG_SYNC_FAIL_ONCE_FILE TRANSACTION_ROLLBACK_REPLACE_FAIL
|
||||
assert_equal "$EXIT_ROLLBACK" "$status" 'restore rollback failure is critical'
|
||||
assert_contains 'КРИТИЧНО' "$output" 'critical restore rollback is diagnosed'
|
||||
;;
|
||||
backup-path-safety)
|
||||
begin_test 'backup path safety rejects symlinked root'
|
||||
mv -- "$BACKUP_DIR" "$TEST_ROOT/m8-backups-real"; ln -s -- "$TEST_ROOT/m8-backups-real" "$BACKUP_DIR"
|
||||
set +e; output=$(list_backups 2>&1); status=$?; set -e
|
||||
assert_failure "$status" 'symlinked backup directory is rejected'
|
||||
rm -f -- "$BACKUP_DIR"; mv -- "$TEST_ROOT/m8-backups-real" "$BACKUP_DIR"
|
||||
export MOCK_FLOCK_BUSY=1
|
||||
set +e; output=$(manual_backup 2>&1); status=$?; set -e
|
||||
unset MOCK_FLOCK_BUSY
|
||||
assert_equal "$EXIT_OPERATION" "$status" 'manual backup respects lock contention'
|
||||
backup=$(create_backup "$WG_CONF" manual "$WG_INTERFACE"); restore_id=${backup##*/}
|
||||
replacement=$TEST_ROOT/m8-replacement.conf; cp -- "$WG_CONF" "$replacement"; printf '# replacement-source\n' >>"$replacement"
|
||||
restore_before=$(sha256sum -- "$WG_CONF" | awk '{print $1}')
|
||||
export RESTORE_PREOPEN_REPLACEMENT=$replacement
|
||||
set +e; output=$(restore_backup "$restore_id" --yes 2>&1); status=$?; set -e
|
||||
unset RESTORE_PREOPEN_REPLACEMENT
|
||||
assert_equal "$EXIT_VALIDATION" "$status" 'restore rejects source replacement before open'
|
||||
assert_equal "$restore_before" "$(sha256sum -- "$WG_CONF" | awk '{print $1}')" 'pre-open replacement does not alter live config'
|
||||
replacement=$TEST_ROOT/m8-lock-replacement; printf 'lock replacement\n' >"$replacement"; chmod 600 -- "$replacement"
|
||||
export MANUAL_BACKUP_PREOPEN_REPLACEMENT=$replacement
|
||||
set +e; output=$(manual_backup 2>&1); status=$?; set -e
|
||||
unset MANUAL_BACKUP_PREOPEN_REPLACEMENT
|
||||
assert_equal "$EXIT_VALIDATION" "$status" 'manual backup rejects lock replacement before open'
|
||||
replacement=$TEST_ROOT/m8-config-replacement; cp -- "$WG_CONF" "$replacement"; printf '# config replacement\n' >>"$replacement"
|
||||
restore_before=$(sha256sum -- "$WG_CONF" | awk '{print $1}')
|
||||
export MANUAL_BACKUP_PRECOPY_REPLACEMENT=$replacement
|
||||
set +e; output=$(manual_backup 2>&1); status=$?; set -e
|
||||
unset MANUAL_BACKUP_PRECOPY_REPLACEMENT
|
||||
assert_equal "$EXIT_VALIDATION" "$status" 'manual backup rejects config replacement before copy'
|
||||
assert_equal "$restore_before" "$(sha256sum -- "$WG_CONF" | awk '{print $1}')" 'config replacement restores original source'
|
||||
;;
|
||||
runtime-file-modes)
|
||||
begin_test 'manual backup runtime files are restrictive'
|
||||
set +e; backup=$(manual_backup 2>"$TEST_ROOT/m8-manual-backup.err"); status=$?; set -e
|
||||
assert_equal 0 "$status" 'manual backup succeeds through locked path'
|
||||
assert_file_exists "$backup/manifest" 'manual backup manifest exists'
|
||||
assert_equal 700 "$(stat -c '%a' -- "$BACKUP_DIR")" 'backup root is 0700'
|
||||
assert_equal 700 "$(stat -c '%a' -- "$backup")" 'backup directory is 0700'
|
||||
assert_equal 600 "$(stat -c '%a' -- "$backup/manifest")" 'manifest is 0600'
|
||||
assert_equal 600 "$(stat -c '%a' -- "$backup/wg.conf")" 'backup config is 0600'
|
||||
assert_contains 'operation=manual-backup' "$(<"$LOG_DIR/wg-manager.log")" 'manual backup is audited'
|
||||
replace_config_line "$config_file" 'BACKUP_RETENTION=' 'BACKUP_RETENTION="2"'
|
||||
for manual_now in 1700000300 1700000400 1700000500; do
|
||||
export WG_MANAGER_NOW=$manual_now
|
||||
set +e; manual_backup >/dev/null 2>&1; status=$?; set -e
|
||||
assert_equal 0 "$status" "manual backup succeeds at timestamp $manual_now"
|
||||
done
|
||||
valid_count=0
|
||||
for retained in "$BACKUP_DIR"/*; do
|
||||
validate_backup_manifest "$retained" && ((valid_count += 1))
|
||||
done
|
||||
assert_equal 2 "$valid_count" 'manual backup retention keeps exactly two valid backups'
|
||||
newest_stamp=$(date -u -d '@1700000500' '+%Y%m%dT%H%M%SZ')
|
||||
assert_equal 1 "$(find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d -name "${newest_stamp}-wg0-*" | wc -l)" 'retention keeps newest manual backup'
|
||||
assert_equal 700 "$(stat -c '%a' -- "$LOG_DIR")" 'log directory remains 0700'
|
||||
assert_equal 600 "$(stat -c '%a' -- "$LOCK_FILE")" 'manual lock remains 0600'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_m7_tests() {
|
||||
local config_file=$TEST_ROOT/diag.conf fixture=$TEST_ROOT/diag-wg0.conf output status saved_test_root
|
||||
rm -rf -- "$TEST_ROOT/diag-meta" "$TEST_ROOT/diag-clients" "$TEST_ROOT/diag-backups" "$TEST_ROOT/diag-logs" "$TEST_ROOT/diag-archive" "$TEST_ROOT/diag.lock" "$TEST_ROOT/ipv4-forward" "$TEST_ROOT/runtime-diff.dump"
|
||||
@@ -3107,6 +3324,8 @@ elif [[ $test_group == add-client || $test_group == address-allocation || $test_
|
||||
run_m4_tests
|
||||
elif [[ $test_group == remove-client || $test_group == remove-cancel || $test_group == remove-exact-block || $test_group == remove-rollback || $test_group == archive-client || $test_group == archive-path-safety || $test_group == remove-preserves-neighbors ]]; then
|
||||
run_m6_tests
|
||||
elif [[ $test_group == audit || $test_group == audit-no-secrets || $test_group == backup-list || $test_group == restore || $test_group == restore-invalid || $test_group == restore-rollback || $test_group == backup-path-safety || $test_group == runtime-file-modes ]]; then
|
||||
run_m8_tests
|
||||
elif [[ $test_group == diagnostics || $test_group == diagnostics-systemd || $test_group == diagnostics-firewall || $test_group == diagnostics-forwarding || $test_group == diagnostics-runtime-diff || $test_group == diagnostics-read-only || $test_group == diagnostics-exit-codes ]]; then
|
||||
run_m7_tests
|
||||
else
|
||||
@@ -3126,6 +3345,11 @@ else
|
||||
run_m7_tests
|
||||
done
|
||||
unset m7_group
|
||||
for m8_group in audit audit-no-secrets backup-list restore restore-invalid restore-rollback backup-path-safety runtime-file-modes; do
|
||||
test_group=$m8_group
|
||||
run_m8_tests
|
||||
done
|
||||
unset m8_group
|
||||
test_group=''
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user