Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a15374-f59f-7a31-86d2-d23b5d2ed181
126 lines
7.5 KiB
Python
126 lines
7.5 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import hashlib
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
ROOT = Path(__file__).parents[1]
|
|
sys.path.insert(0, str(ROOT / 'tools'))
|
|
from guardrails import extract_rule, evaluate
|
|
from build_site import build, load_manifest
|
|
from test_publication import _fixture
|
|
from test_release import _release
|
|
from verify_release import verify
|
|
|
|
CANON_FIXTURE = '---\nid: canon-build-mode-change-authorization\ntitle: Build-Mode Change Authorization\nstatus: accepted\nrevision: accepted-1\nowner: the-custodian\nlast_reviewed: 2026-10-11\nreview_interval: 3m\nexpires_on: production-close-down\n---\n\n# Build-Mode Change Authorization\n\n```yaml\nrule: build-mode-change-authorization\nversion: 0.1\napplies_while: build-mode\nblue_lane_requires: # every field must be present on the change record\n - diff_ref # path or commit of reviewed diff / dry-run output\n - rollback # command or procedure reference\n - abort_owner # repo or agent name\n - receipt_path # where the execution receipt is written\nred_lane_categories:\n - destructive-deletion\n - access-scope-expansion\n - secret-scope-expansion\n - spend-ceiling\n - public-publication\norange_lane_categories:\n - founder-identity-act\n - platform-admin-login\n```\n'
|
|
|
|
|
|
class GuardrailTest(unittest.TestCase):
|
|
def setUp(self):
|
|
# Synthetic fixtures must not inherit the CI release source lock.
|
|
source_env = mock.patch.dict(os.environ, {"POLICY_NEXUS_SOURCE_ROOT": ""})
|
|
source_env.start()
|
|
self.addCleanup(source_env.stop)
|
|
self.source = CANON_FIXTURE
|
|
self.rule = extract_rule(self.source)
|
|
self.record = {'categories': [], 'diff_ref': 'reviewed.patch', 'rollback': 'revert source/image',
|
|
'abort_owner': 'policy-nexus', 'receipt_path': 'receipt.json'}
|
|
|
|
def test_canon_block_contract(self):
|
|
self.assertEqual(self.rule, {
|
|
'rule': 'build-mode-change-authorization', 'version': 0.1,
|
|
'applies_while': 'build-mode',
|
|
'blue_lane_requires': ['diff_ref', 'rollback', 'abort_owner', 'receipt_path'],
|
|
'red_lane_categories': ['destructive-deletion', 'access-scope-expansion',
|
|
'secret-scope-expansion', 'spend-ceiling', 'public-publication'],
|
|
'orange_lane_categories': ['founder-identity-act', 'platform-admin-login']})
|
|
|
|
def test_blue_requires_complete_record_and_confirmed_build_mode(self):
|
|
self.assertEqual(evaluate(self.rule, self.record, build_mode=True), {'lane': 'blue'})
|
|
self.assertEqual(evaluate(self.rule, self.record, build_mode=False)['lane'], 'incomplete')
|
|
for key in self.rule['blue_lane_requires']:
|
|
for invalid in ['', ' ', None, True, [], {}]:
|
|
with self.subTest(key=key, value=invalid):
|
|
result = evaluate(self.rule, self.record | {key: invalid}, build_mode=True)
|
|
self.assertEqual(result['lane'], 'incomplete')
|
|
self.assertIn(key, result['missing'])
|
|
|
|
def test_all_red_and_orange_categories_and_red_precedence(self):
|
|
for lane in ('red', 'orange'):
|
|
for category in self.rule[f'{lane}_lane_categories']:
|
|
self.assertEqual(evaluate(self.rule, {'categories': [category]}, build_mode=True)['lane'], lane)
|
|
self.assertEqual(evaluate(self.rule, {'categories': ['public-publication', 'platform-admin-login']}, build_mode=True)['lane'], 'red')
|
|
|
|
def test_unknown_or_undeclared_categories_cannot_grant_blue(self):
|
|
for categories in [None, 'public-publication', [False], ['typo']]:
|
|
self.assertEqual(evaluate(self.rule, self.record | {'categories': categories}, build_mode=True)['lane'], 'incomplete')
|
|
|
|
def test_unsupported_yaml_duplicate_fields_and_duplicate_blocks_refused(self):
|
|
block = self.source[self.source.index('```yaml'):self.source.index('```', self.source.index('```yaml') + 7) + 3]
|
|
for source in [self.source + '\n' + block + '\n', self.source.replace('version: 0.1', 'version: 0.1\nversion: 0.1'),
|
|
self.source.replace(' - rollback', ' - rollback: &alias'),
|
|
self.source.replace('version: 0.1', 'version: 0.2')]:
|
|
with self.assertRaises(ValueError):
|
|
extract_rule(source)
|
|
|
|
def test_build_published_artifact_equals_canon_and_expiry_is_prominent(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
manifest = _fixture(root)
|
|
data = json.loads(manifest.read_text())
|
|
document = data['documents'][0]
|
|
source = root / 'canon-repo' / document['source_path']
|
|
source.write_text(self.source)
|
|
document.update(id='canon-build-mode-change-authorization', guardrail_path='guardrails/build-mode-change-authorization.json')
|
|
manifest.write_text(json.dumps(data))
|
|
records = build(manifest, root / 'build')
|
|
artifact = root / 'build' / document['guardrail_path']
|
|
self.assertEqual(json.loads(artifact.read_text()), extract_rule(source.read_text()))
|
|
page = (root / 'build' / document['canonical_path']).read_text()
|
|
self.assertIn('Expires on: production-close-down', page[:page.index('</header>')])
|
|
self.assertIn('blue_lane_requires:', page)
|
|
self.assertIn('guardrail_digest', records[0])
|
|
self.assertIn('Build-Mode Change Authorization', (root / 'build/index.html').read_text())
|
|
|
|
def test_release_verifier_rejects_missing_tampered_or_invalid_guardrail(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
output = _release(root)
|
|
manifest_path = output / 'publication-manifest.json'
|
|
manifest = json.loads(manifest_path.read_text())
|
|
artifact = output / 'guardrails/rule.json'
|
|
artifact.parent.mkdir()
|
|
good = (json.dumps(self.rule) + '\n').encode()
|
|
artifact.write_bytes(good)
|
|
document = manifest['documents'][0]
|
|
document.update(guardrail_path='guardrails/rule.json', guardrail_digest=hashlib.sha256(good).hexdigest())
|
|
manifest_path.write_text(json.dumps(manifest))
|
|
verify(output)
|
|
artifact.write_bytes(good + b' ')
|
|
with self.assertRaisesRegex(ValueError, 'guardrail artifact digest'):
|
|
verify(output)
|
|
artifact.unlink()
|
|
with self.assertRaises(OSError):
|
|
verify(output)
|
|
invalid = json.dumps(self.rule | {'version': 99}).encode()
|
|
artifact.write_bytes(invalid)
|
|
document['guardrail_digest'] = hashlib.sha256(invalid).hexdigest()
|
|
manifest_path.write_text(json.dumps(manifest))
|
|
with self.assertRaisesRegex(ValueError, 'unsupported machine rule'):
|
|
verify(output)
|
|
|
|
def test_guardrail_path_collision_and_traversal_refused(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
manifest = _fixture(Path(directory))
|
|
original = json.loads(manifest.read_text())
|
|
for path in ['../outside.json', '/outside.json', 'example.html']:
|
|
data = json.loads(json.dumps(original))
|
|
data['documents'][0]['guardrail_path'] = path
|
|
manifest.write_text(json.dumps(data))
|
|
with self.assertRaises(ValueError):
|
|
load_manifest(manifest)
|