diff --git a/scripts/s1_restore.py b/scripts/s1_restore.py index e4b4e6e..6641703 100755 --- a/scripts/s1_restore.py +++ b/scripts/s1_restore.py @@ -210,7 +210,13 @@ def extract_bundle( expected = validated["manifest"]["os_config_members"] if observed != expected: raise RestoreError("decrypted archive membership or modes differ from manifest") - archive.extractall(path=stage, filter="data") + # Python 3.12 added the extraction filter argument. On older + # supported runtimes the complete member/link/type validation + # above is the equivalent fail-closed gate before extraction. + if sys.version_info >= (3, 12): + archive.extractall(path=stage, filter="data") + else: + archive.extractall(path=stage) plain_archive.unlink() packages = stage / "packages.txt" _decrypt(executable, identity, artifacts["packages"], packages) diff --git a/tests/test_s1_backup_recovery.py b/tests/test_s1_backup_recovery.py index 4ebcc05..5e50cca 100644 --- a/tests/test_s1_backup_recovery.py +++ b/tests/test_s1_backup_recovery.py @@ -163,6 +163,18 @@ class S1BackupRecoveryTests(unittest.TestCase): with self.assertRaisesRegex(RestoreError, "digest mismatch"): validate_bundle(bundle, self.spec) + def test_isolated_restore_supports_python_311_tar_api(self) -> None: + bundle = self._create() + with mock.patch("s1_restore.sys.version_info", (3, 11, 0)): + restored = extract_bundle( + bundle=bundle, + spec_path=self.spec, + destination=self.root / "restore-python311", + identity=self.identity, + age_binary=shutil.which("age"), + ) + self.assertEqual("fixture-host\n", (restored / "etc/hostname").read_text()) + def test_archive_traversal_and_escaping_link_fail(self) -> None: member = tarfile.TarInfo("../../etc/shadow") with self.assertRaisesRegex(RestoreError, "unsafe archive member"): diff --git a/workplans/RAIL-HO-WP-0012-s1-backup-recovery-loop.md b/workplans/RAIL-HO-WP-0012-s1-backup-recovery-loop.md index c2dc227..52f5366 100644 --- a/workplans/RAIL-HO-WP-0012-s1-backup-recovery-loop.md +++ b/workplans/RAIL-HO-WP-0012-s1-backup-recovery-loop.md @@ -178,9 +178,9 @@ decrypted configuration. ## Source delivery record — 2026-08-23 -- `make validate-s1-backup` passes the declaration and 15 fixture recovery +- `make validate-s1-backup` passes the declaration and 16 fixture recovery tests using disposable age identities and storage. -- The full repository suite passes 45 tests. The timer calendar is accepted by +- The full repository suite passes 46 tests. The timer calendar is accepted by `systemd-analyze`; deployment YAML and separation from bootstrap are checked as source contracts. - Native `ansible-playbook --syntax-check` was unavailable on the development