bastion.toml manifest parser with variable validation and dependency declarations. Declarative compliance policy schema with per-platform check implementations. Template loader with variable substitution (Bastion-owned files only — never touches Ansible/Terraform). PolicyRegistry and AccordRegistry with builtin fallbacks. BOUNDARY: loader never touches automation framework files. Signed-off-by: Tyler King <tking@guildhouse.dev>
180 lines
6.9 KiB
Python
180 lines
6.9 KiB
Python
# Copyright 2026 Guildhouse Dev
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
"""Tests for the template system — manifest, policy, loader, registry."""
|
|
|
|
import os
|
|
import pytest
|
|
from pathlib import Path
|
|
|
|
from gsap_broker.templates.manifest import TemplateManifest
|
|
from gsap_broker.templates.policy import CompliancePolicy
|
|
from gsap_broker.templates.loader import TemplateLoader, LoadResult
|
|
from gsap_broker.templates.registry import AccordRegistry, PolicyRegistry
|
|
|
|
FIXTURE_DIR = Path(__file__).parent / "fixtures" / "sample-template"
|
|
|
|
|
|
# ── Manifest parsing ─────────────────────────────────────────────
|
|
|
|
def test_parse_full_manifest():
|
|
"""TEST 2: Full bastion.toml parsed correctly."""
|
|
m = TemplateManifest.from_toml(FIXTURE_DIR / "bastion.toml")
|
|
assert m.template.name == "test-baseline"
|
|
assert m.template.version == "0.1.0"
|
|
assert m.template.vertical == "testing"
|
|
assert "test-framework" in m.template.compliance_frameworks
|
|
assert m.compatibility.bastion_min == "0.3.0"
|
|
assert "intune" in m.compatibility.connectors_required
|
|
assert "org_name" in m.variables
|
|
assert m.variables["org_name"].required is True
|
|
assert m.variables["api_key"].sensitive is True
|
|
|
|
|
|
def test_parse_minimal_manifest(tmp_path):
|
|
"""TEST 3: Minimal bastion.toml with only required fields."""
|
|
toml = tmp_path / "bastion.toml"
|
|
toml.write_text('[template]\nname = "minimal"\nversion = "0.1.0"\n')
|
|
m = TemplateManifest.from_toml(toml)
|
|
assert m.template.name == "minimal"
|
|
assert m.compatibility.bastion_min == "0.5.0" # default
|
|
assert m.contents.policies == "policies/" # default
|
|
|
|
|
|
def test_validate_variables_missing_required():
|
|
"""TEST 4: Missing required variable produces error."""
|
|
m = TemplateManifest.from_toml(FIXTURE_DIR / "bastion.toml")
|
|
errors = m.validate_variables({}) # org_name missing
|
|
assert any("org_name" in e for e in errors)
|
|
|
|
|
|
def test_validate_variables_all_provided():
|
|
"""TEST 5: All required variables provided — no errors."""
|
|
m = TemplateManifest.from_toml(FIXTURE_DIR / "bastion.toml")
|
|
errors = m.validate_variables({"org_name": "TestCorp"})
|
|
assert errors == []
|
|
|
|
|
|
# ── Policy parsing ───────────────────────────────────────────────
|
|
|
|
def test_parse_policy():
|
|
"""TEST 6: Multi-condition policy parsed correctly."""
|
|
p = CompliancePolicy.from_toml(FIXTURE_DIR / "policies" / "test-workstation.toml")
|
|
assert p.name == "test-workstation-policy"
|
|
assert p.framework == "test-framework"
|
|
assert len(p.conditions) == 2
|
|
assert p.conditions[0].id == "disk-encryption"
|
|
assert p.conditions[0].severity == "critical"
|
|
assert p.breach_response.critical == "suspend_access"
|
|
assert p.schedule.interval_seconds == 300
|
|
|
|
|
|
def test_policy_platform_filtering():
|
|
"""TEST 7: conditions_for_platform filters correctly."""
|
|
p = CompliancePolicy.from_toml(FIXTURE_DIR / "policies" / "test-workstation.toml")
|
|
linux_conds = p.conditions_for_platform("linux")
|
|
windows_conds = p.conditions_for_platform("windows")
|
|
|
|
# disk-encryption has linux check, antivirus doesn't
|
|
assert len(linux_conds) == 1
|
|
assert linux_conds[0].id == "disk-encryption"
|
|
|
|
# Both conditions have windows checks
|
|
assert len(windows_conds) == 2
|
|
|
|
|
|
# ── Template loader ──────────────────────────────────────────────
|
|
|
|
def test_template_loader_full():
|
|
"""TEST 8: Full template load — policies and accords registered."""
|
|
policies = PolicyRegistry()
|
|
accords = AccordRegistry()
|
|
loader = TemplateLoader(policies, accords)
|
|
|
|
result = loader.load(FIXTURE_DIR, {"org_name": "TestCorp"})
|
|
|
|
assert result.success
|
|
assert "test-workstation-policy" in result.policies_loaded
|
|
assert "standard-operations" in result.accords_loaded
|
|
assert policies.get("test-workstation-policy") is not None
|
|
assert accords.get("standard-operations") is not None
|
|
|
|
|
|
def test_variable_substitution_bastion_files_only():
|
|
"""TEST 9: Variable substitution applies to Bastion files, not Ansible files."""
|
|
policies = PolicyRegistry()
|
|
accords = AccordRegistry()
|
|
loader = TemplateLoader(policies, accords)
|
|
|
|
loader.load(FIXTURE_DIR, {"org_name": "AcmeCorp"})
|
|
|
|
# Policy file should have substitution applied
|
|
policy = policies.get("test-workstation-policy")
|
|
assert policy is not None
|
|
assert "AcmeCorp" in policy.description
|
|
|
|
# Ansible playbook should be UNTOUCHED
|
|
playbook_path = FIXTURE_DIR / "ansible" / "playbooks" / "test-playbook.yml"
|
|
content = playbook_path.read_text()
|
|
assert "${org_name}" in content # NOT substituted
|
|
|
|
|
|
def test_variable_substitution_sensitive_not_logged(caplog):
|
|
"""TEST 10: Sensitive variable values not in log output."""
|
|
import logging
|
|
policies = PolicyRegistry()
|
|
accords = AccordRegistry()
|
|
loader = TemplateLoader(policies, accords)
|
|
|
|
with caplog.at_level(logging.DEBUG):
|
|
loader.load(FIXTURE_DIR, {"org_name": "TestCorp", "api_key": "SECRET_VALUE_123"})
|
|
|
|
# The sensitive value should not appear in logs
|
|
assert "SECRET_VALUE_123" not in caplog.text
|
|
# Non-sensitive org_name value DOES appear (confirms substitution ran)
|
|
assert "TestCorp" in caplog.text
|
|
|
|
|
|
def test_missing_bastion_toml(tmp_path):
|
|
"""Missing bastion.toml produces error, not crash."""
|
|
policies = PolicyRegistry()
|
|
accords = AccordRegistry()
|
|
loader = TemplateLoader(policies, accords)
|
|
|
|
result = loader.load(tmp_path, {})
|
|
assert not result.success
|
|
assert any("bastion.toml" in e for e in result.errors)
|
|
|
|
|
|
# ── Registries ───────────────────────────────────────────────────
|
|
|
|
def test_accord_registry_builtin_fallback():
|
|
"""TEST 11: AccordRegistry falls back to builtins for known accords."""
|
|
reg = AccordRegistry()
|
|
# shell-exec is a builtin
|
|
assert reg.get("shell-exec") is not None
|
|
assert reg.get("shell-exec")["capability_ceiling"] == "CAP_MUTATE"
|
|
# unknown returns None
|
|
assert reg.get("nonexistent") is None
|
|
|
|
|
|
def test_accord_registry_template_overrides_builtin():
|
|
"""Template-loaded accord overrides builtin with same name."""
|
|
reg = AccordRegistry()
|
|
reg.register("shell-exec", {"name": "shell-exec", "custom": True})
|
|
assert reg.get("shell-exec")["custom"] is True
|
|
|
|
|
|
def test_policy_registry_by_framework():
|
|
"""PolicyRegistry.for_framework filters correctly."""
|
|
reg = PolicyRegistry()
|
|
p1 = CompliancePolicy(name="p1", framework="hipaa")
|
|
p2 = CompliancePolicy(name="p2", framework="pci")
|
|
p3 = CompliancePolicy(name="p3", framework="hipaa")
|
|
reg.register(p1)
|
|
reg.register(p2)
|
|
reg.register(p3)
|
|
|
|
hipaa = reg.for_framework("hipaa")
|
|
assert len(hipaa) == 2
|
|
assert all(p.framework == "hipaa" for p in hipaa)
|