feat: add manifest_validator.py — security chokepoint for compose and manifest validation
Unit Tests / test (push) Successful in 11m18s

Rejects privileged compose configs (network_mode:host, pid:host, ipc:host,
userns_mode:host, cap_add:ALL, string commands, missing cell-network,
reserved container names). Validates manifest schema_version=3, image
digest pinning (sha256 required, :tag-only rejected), and provision hook
format. Wired into ServiceComposer.write_compose() and
ServiceStoreManager.install() as a single enforcement point.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 18:45:45 -04:00
parent 62b31b072b
commit 1f2f9d9f6e
4 changed files with 495 additions and 7 deletions
+19 -5
View File
@@ -58,6 +58,12 @@ def _make_manager(tmp_dir=None, installed=None, identity=None):
return mgr
_VALID_IMAGE = (
'git.pic.ngo/roof/myapp@sha256:'
+ 'a' * 64
)
def _valid_manifest(**overrides):
"""Return a minimal valid manifest, with optional field overrides."""
m = {
@@ -65,7 +71,7 @@ def _valid_manifest(**overrides):
'name': 'My App',
'version': '1.0.0',
'author': 'Test Author',
'image': 'git.pic.ngo/roof/myapp:latest',
'image': _VALID_IMAGE,
'container_name': 'cell-myapp',
}
m.update(overrides)
@@ -143,16 +149,24 @@ class TestValidateManifestImage(unittest.TestCase):
self.assertFalse(ok)
self.assertTrue(any('image must match' in e for e in errs))
def test_image_matching_git_pic_ngo_roof_with_tag_passes(self):
m = _valid_manifest(image='git.pic.ngo/roof/something:1.2.3')
def test_image_matching_git_pic_ngo_roof_with_digest_passes(self):
digest = 'a' * 64
m = _valid_manifest(image=f'git.pic.ngo/roof/something@sha256:{digest}')
ok, errs = ServiceStoreManager._validate_manifest(m)
self.assertTrue(ok)
self.assertEqual(errs, [])
def test_image_git_pic_ngo_roof_no_tag_passes(self):
def test_image_tag_only_rejected(self):
# Digest pinning is required; tag-only images are rejected.
m = _valid_manifest(image='git.pic.ngo/roof/something:1.2.3')
ok, errs = ServiceStoreManager._validate_manifest(m)
self.assertFalse(ok)
def test_image_git_pic_ngo_roof_no_tag_rejected(self):
# No tag and no digest — rejected because digest pin is required.
m = _valid_manifest(image='git.pic.ngo/roof/myservice')
ok, errs = ServiceStoreManager._validate_manifest(m)
self.assertTrue(ok)
self.assertFalse(ok)
def test_image_wrong_registry_rejected(self):
m = _valid_manifest(image='ghcr.io/roof/myapp:latest')