Harden isolated release builds in Flatpak
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -30,7 +31,12 @@ from govoplan_release.release_execution import ( # noqa: E402
|
||||
verify_repository_preflight_binding,
|
||||
_clone_catalog_sources,
|
||||
_checkout_frozen_source,
|
||||
_flatpak_proxy_environment,
|
||||
_run_isolated_wheel_builder,
|
||||
_trusted_flatpak_builder_proxy,
|
||||
_trusted_host_bubblewrap,
|
||||
_verify_exact_publication_delta,
|
||||
isolated_builder_launcher,
|
||||
)
|
||||
from govoplan_release.selective_catalog import ( # noqa: E402
|
||||
enforce_frozen_selected_source_provenance,
|
||||
@@ -39,6 +45,91 @@ from govoplan_release.selective_catalog import ( # noqa: E402
|
||||
|
||||
|
||||
class ReleaseExecutionTests(unittest.TestCase):
|
||||
def test_flatpak_proxy_environment_drops_unrelated_values(self) -> None:
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/attacker/bus",
|
||||
"XDG_RUNTIME_DIR": "/run/user/1000",
|
||||
"PYTHONPATH": "/attacker",
|
||||
"GIT_DIR": "/attacker/repository",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
environment = _flatpak_proxy_environment()
|
||||
|
||||
self.assertEqual("/usr/bin:/bin", environment["PATH"])
|
||||
self.assertEqual(
|
||||
"unix:path=/run/flatpak/bus",
|
||||
environment["DBUS_SESSION_BUS_ADDRESS"],
|
||||
)
|
||||
self.assertNotIn("XDG_RUNTIME_DIR", environment)
|
||||
self.assertNotIn("PYTHONPATH", environment)
|
||||
self.assertNotIn("GIT_DIR", environment)
|
||||
|
||||
def test_host_bubblewrap_preflight_requires_root_owned_safe_binary(self) -> None:
|
||||
with patch(
|
||||
"govoplan_release.release_execution.subprocess.run",
|
||||
return_value=SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout="81ed|0\n",
|
||||
),
|
||||
):
|
||||
self.assertTrue(_trusted_host_bubblewrap())
|
||||
|
||||
with patch(
|
||||
"govoplan_release.release_execution.subprocess.run",
|
||||
return_value=SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout="81ff|1000\n",
|
||||
),
|
||||
):
|
||||
self.assertFalse(_trusted_host_bubblewrap())
|
||||
|
||||
def test_builder_launcher_uses_only_preflighted_flatpak_proxy(self) -> None:
|
||||
with patch(
|
||||
"govoplan_release.release_execution._trusted_flatpak_builder_proxy",
|
||||
return_value=True,
|
||||
):
|
||||
self.assertEqual(
|
||||
("/usr/bin/flatpak-spawn", "--host", "/usr/bin/bwrap"),
|
||||
isolated_builder_launcher(),
|
||||
)
|
||||
|
||||
def test_flatpak_proxy_requires_recognized_container(self) -> None:
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
self.assertFalse(_trusted_flatpak_builder_proxy())
|
||||
|
||||
def test_flatpak_builder_launch_uses_sanitized_environment(self) -> None:
|
||||
process = SimpleNamespace(wait=lambda timeout: 0)
|
||||
command = (
|
||||
"/usr/bin/flatpak-spawn",
|
||||
"--host",
|
||||
"/usr/bin/bwrap",
|
||||
)
|
||||
with (
|
||||
patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/attacker/bus",
|
||||
"PYTHONPATH": "/attacker",
|
||||
},
|
||||
clear=True,
|
||||
),
|
||||
patch(
|
||||
"govoplan_release.release_execution.subprocess.Popen",
|
||||
return_value=process,
|
||||
) as popen,
|
||||
):
|
||||
self.assertEqual(0, _run_isolated_wheel_builder(command))
|
||||
|
||||
environment = popen.call_args.kwargs["env"]
|
||||
self.assertEqual(
|
||||
"unix:path=/run/flatpak/bus",
|
||||
environment["DBUS_SESSION_BUS_ADDRESS"],
|
||||
)
|
||||
self.assertNotIn("PYTHONPATH", environment)
|
||||
|
||||
def test_git_environment_ignores_inherited_redirection_and_commands(self) -> None:
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
@@ -510,6 +601,11 @@ class ReleaseExecutionTests(unittest.TestCase):
|
||||
'[project]\nname = "govoplan-files"\nversion = "1.2.3"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
_git(workspace, "init", "-b", "main", str(python_repo))
|
||||
_git(python_repo, "config", "user.name", "Release Test")
|
||||
_git(python_repo, "config", "user.email", "release@example.test")
|
||||
_git(python_repo, "add", "pyproject.toml")
|
||||
_git(python_repo, "commit", "-m", "Source")
|
||||
specs = (
|
||||
RepositorySpec(
|
||||
name="govoplan-files",
|
||||
@@ -530,6 +626,12 @@ class ReleaseExecutionTests(unittest.TestCase):
|
||||
|
||||
def build(command: tuple[str, ...]) -> int:
|
||||
wheel_dir = Path(command[command.index("--bind") + 1])
|
||||
second_bind = command.index("--bind", command.index("--bind") + 1)
|
||||
source_dir = Path(command[second_bind + 1])
|
||||
(source_dir / "build-mutated.txt").write_text(
|
||||
"isolated copy",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(
|
||||
wheel_dir / "govoplan_files-1.2.3-py3-none-any.whl"
|
||||
).write_bytes(b"wheel")
|
||||
@@ -581,10 +683,18 @@ class ReleaseExecutionTests(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
self.assertFalse((python_repo / "build-mutated.txt").exists())
|
||||
self.assertEqual({"govoplan-files"}, set(artifacts))
|
||||
self.assertEqual(1, runner.call_count)
|
||||
command = runner.call_args.args[0]
|
||||
self.assertIn(str(python_repo), command)
|
||||
self.assertNotIn(str(python_repo), command)
|
||||
self.assertTrue(
|
||||
any(
|
||||
value.startswith(str(candidate / "artifacts"))
|
||||
and value.endswith("/source")
|
||||
for value in command
|
||||
)
|
||||
)
|
||||
self.assertIn("--unshare-all", command)
|
||||
self.assertIn("PIP_NO_INDEX", command)
|
||||
self.assertNotIn(str(Path.home()), command)
|
||||
|
||||
Reference in New Issue
Block a user