from __future__ import annotations import importlib.util from pathlib import Path import sys import unittest META_ROOT = Path(__file__).resolve().parents[1] RESOLVER = ( META_ROOT / "tools" / "checks" / "security-audit" / "resolve_workspace_mount.py" ) def load_resolver_module(): spec = importlib.util.spec_from_file_location( "security_audit_mount_resolver", RESOLVER, ) if spec is None or spec.loader is None: raise RuntimeError(f"Could not load {RESOLVER}") module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) return module class SecurityAuditMountResolverTests(unittest.TestCase): def setUp(self) -> None: self.resolver = load_resolver_module() self.root = "/workspace/add-ideas/govoplan/govoplan" self.workspace = "/workspace/add-ideas/govoplan" def test_resolves_only_the_named_workspace_volume_for_both_scopes(self) -> None: mounts = [ { "Type": "bind", "Source": "/var/run/docker.sock", "Destination": "/var/run/docker.sock", "RW": True, }, { "Type": "volume", "Name": "actions-workspace", "Source": "/var/lib/docker/volumes/actions-workspace/_data", "Destination": self.workspace, "RW": True, }, { "Type": "volume", "Name": "actions-environment", "Source": "/var/lib/docker/volumes/actions-environment/_data", "Destination": "/var/run/act", "RW": True, }, ] for scope in ("current", "govoplan"): with self.subTest(scope=scope): self.assertEqual( (f"type=volume,source=actions-workspace,target={self.workspace}"), self.resolver.resolve_workspace_mount( mounts, root=self.root, scope=scope, reports_dir="audit-reports", ), ) def test_resolves_a_workspace_bind_without_other_mounts(self) -> None: mounts = [ { "Type": "bind", "Source": "/srv/gitea/actions/task-123", "Destination": self.workspace, "RW": True, }, { "Type": "bind", "Source": "/var/run/docker.sock", "Destination": "/var/run/docker.sock", "RW": True, }, ] self.assertEqual( (f"type=bind,source=/srv/gitea/actions/task-123,target={self.workspace}"), self.resolver.resolve_workspace_mount( mounts, root=self.root, scope="govoplan", reports_dir="audit-reports", ), ) def test_nested_repository_mount_is_valid_only_for_current_scope(self) -> None: mounts = [ { "Type": "volume", "Name": "all-repositories", "Destination": self.workspace, "RW": True, }, { "Type": "volume", "Name": "current-repository", "Destination": self.root, "RW": True, }, ] self.assertIn( "source=current-repository", self.resolver.resolve_workspace_mount( mounts, root=self.root, scope="current", reports_dir="audit-reports", ), ) with self.assertRaisesRegex( self.resolver.MountResolutionError, "nested job-container mounts", ): self.resolver.resolve_workspace_mount( mounts, root=self.root, scope="govoplan", reports_dir="audit-reports", ) def test_rejects_unsafe_or_unusable_mount_layouts(self) -> None: cases = { "missing": [], "path-boundary": [ { "Type": "volume", "Name": "wrong-workspace", "Destination": "/workspace/add-ideas/govoplan-other", "RW": True, } ], "read-only": [ { "Type": "volume", "Name": "actions-workspace", "Destination": self.workspace, "RW": False, } ], "ambiguous": [ { "Type": "volume", "Name": name, "Destination": self.workspace, "RW": True, } for name in ("workspace-one", "workspace-two") ], "scope-too-narrow": [ { "Type": "volume", "Name": "repository-only", "Destination": self.root, "RW": True, } ], } for name, mounts in cases.items(): with ( self.subTest(name=name), self.assertRaises(self.resolver.MountResolutionError), ): self.resolver.resolve_workspace_mount( mounts, root=self.root, scope="govoplan", reports_dir="audit-reports", ) def test_rejects_reports_outside_the_selected_workspace_mount(self) -> None: mounts = [ { "Type": "volume", "Name": "actions-workspace", "Destination": self.workspace, "RW": True, } ] with self.assertRaisesRegex( self.resolver.MountResolutionError, "reports path .* outside", ): self.resolver.resolve_workspace_mount( mounts, root=self.root, scope="current", reports_dir="/tmp/audit-reports", ) def test_rejects_broad_or_sensitive_workspace_bind_sources(self) -> None: for source in ( "/", "/home", "/var/run/docker.sock", "/srv/../etc/shadow", ): with ( self.subTest(source=source), self.assertRaisesRegex( self.resolver.MountResolutionError, "too broad or sensitive", ), ): self.resolver.resolve_workspace_mount( [ { "Type": "bind", "Source": source, "Destination": self.workspace, "RW": True, } ], root=self.root, scope="govoplan", reports_dir="audit-reports", ) with self.assertRaises(self.resolver.MountResolutionError): self.resolver.resolve_workspace_mount( [ { "Type": "bind", "Source": "//var/lib/workspace", "Destination": self.workspace, "RW": True, } ], root=self.root, scope="govoplan", reports_dir="audit-reports", ) if __name__ == "__main__": unittest.main()