NK-WP-0048: canon accepted; SecurityCaseType schemas, validator and template (T01, T03)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 4s

security-case-types_v0.1 accepted by the founder 2026-10-08. Adds JSON schemas
for types and cases, tools/security-case-types/security_case_validator.py
(validate-type, validate-case, render via the scenario composer, plan-only),
and a runnable _template type with README sections, an example case and tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5457@bnt-lap001
Assistant-Session: 792bf84a-3783-4f5d-a811-cded61dd2012
This commit is contained in:
tegwick 2026-10-08 22:29:13 +02:00
parent dcacdf57f3
commit d8cf42bab6
11 changed files with 1122 additions and 7 deletions

View file

@ -0,0 +1,159 @@
import copy
import importlib.util
import sys
from pathlib import Path
import yaml
TOOL_DIR = Path(__file__).resolve().parents[1]
REPO = TOOL_DIR.parents[1]
SPEC = importlib.util.spec_from_file_location("security_case_validator", TOOL_DIR / "security_case_validator.py")
v = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = v
SPEC.loader.exec_module(v)
TEMPLATE_DIR = REPO / "security-case-types" / "_template"
TYPE = yaml.safe_load((TEMPLATE_DIR / "type.yaml").read_text())
CASE = yaml.safe_load((TEMPLATE_DIR / "examples" / "example-operator.yaml").read_text())
DECLS, _ = v.composer.load_declarations([str(REPO / "capabilities/playbooks/net-kingdom.local-identity.yaml")])
def msgs(issues):
return [f"{i.path}: {i.message}" for i in issues if i.level == "ERROR"]
def implemented(t=None):
t = copy.deepcopy(t or TYPE)
t["spec"]["status"] = "implemented"
return t
def test_template_type_validates():
assert msgs(v.validate_type(TYPE, TEMPLATE_DIR)) == []
def test_missing_section_is_refused():
t = copy.deepcopy(TYPE)
del t["spec"]["rollback"]
assert any("rollback" in m for m in msgs(v.validate_type(t)))
def test_unpinned_provider_is_refused():
t = copy.deepcopy(TYPE)
t["spec"]["composition"]["providers"] = {}
assert any("provider pin" in m for m in msgs(v.validate_type(t)))
def test_unknown_capability_is_refused():
t = copy.deepcopy(TYPE)
t["spec"]["composition"]["capabilities"] = ["c9.invented"]
assert any("unknown capability" in m for m in msgs(v.validate_type(t)))
def test_attended_step_needs_justification():
t = copy.deepcopy(TYPE)
del t["spec"]["follow_through"][0]["attended_justification"]
assert any("attended" in m for m in msgs(v.validate_type(t)))
def test_every_step_needs_a_rollback():
t = copy.deepcopy(TYPE)
t["spec"]["rollback"] = [r for r in t["spec"]["rollback"] if r["reverses"] != "init-store"]
assert any("init-store" in m for m in msgs(v.validate_type(t)))
def test_verification_needs_a_negative_check_and_never_reads_values():
t = copy.deepcopy(TYPE)
t["spec"]["verification"] = [c for c in t["spec"]["verification"] if c["expect"] != "deny"]
t["spec"]["verification"][0]["reads_secret_value"] = True
found = msgs(v.validate_type(t))
assert any("negative check" in m for m in found)
assert any("never reads a value" in m for m in found)
def test_template_with_unknown_parameter_is_refused():
t = copy.deepcopy(TYPE)
t["spec"]["materialization"][1]["template"] = "policy-{nonexistent}"
assert any("unknown parameter" in m for m in msgs(v.validate_type(t)))
def test_high_risk_type_needs_founder_acceptance():
t = copy.deepcopy(TYPE)
t["spec"]["acceptance"]["risk_class"] = "high"
assert any("founder" in m for m in msgs(v.validate_type(t)))
def test_credential_shaped_value_in_type_is_refused():
t = copy.deepcopy(TYPE)
t["spec"]["purpose"] = "token hvs.CAESIabcdefghijklmnop"
assert any("credential-shaped" in m for m in msgs(v.validate_type(t)))
def test_case_against_unimplemented_type_is_refused():
assert any("only implemented types" in m for m in msgs(v.validate_case(CASE, TYPE)))
assert msgs(v.validate_case(CASE, implemented())) == []
def test_case_cannot_carry_a_secret_value():
c = copy.deepcopy(CASE)
c["spec"]["parameters"]["signing_key_ref"] = "-----BEGIN PRIVATE KEY-----"
found = msgs(v.validate_case(c, implemented()))
assert any("custody reference" in m for m in found)
assert any("credential-shaped" in m for m in found)
def test_case_parameter_rules():
c = copy.deepcopy(CASE)
c["spec"]["parameters"]["operator_username"] = "Not Valid"
c["spec"]["parameters"]["store_home"] = "/tmp/elsewhere"
c["spec"]["parameters"]["surprise"] = 1
del c["spec"]["parameters"]["operator_email"]
found = msgs(v.validate_case(c, implemented()))
assert any("does not match" in m for m in found)
assert any("does not let cases set it" in m for m in found)
assert any("not a parameter of this type" in m for m in found)
assert any("operator_email: required" in m for m in found)
def test_accepted_case_needs_acceptance_record():
c = copy.deepcopy(CASE)
c["spec"]["status"] = "accepted"
assert any("acceptance" in m for m in msgs(v.validate_case(c, implemented())))
c["spec"]["acceptance"] = {"at": "2026-10-08", "record": "decision:x", "by": [{"role": "owner", "name": "a"}]}
assert msgs(v.validate_case(c, implemented())) == []
def test_render_is_deterministic_plan_only_and_substitutes_parameters():
issues, r = v.render_case(CASE, implemented(), DECLS)
assert msgs(issues) == []
assert r["execution"] == {"permitted": False}
assert r["materialization"][1]["template"] == "local-identity-bootstrap-admin-read"
assert r["rollback"][1]["action"].endswith("bao:platform/workloads/example/local-identity-signing")
_, again = v.render_case(CASE, implemented(), DECLS)
assert r["digest"] == again["digest"]
def test_load_type_checks_reference_and_version(tmp_path):
root = tmp_path / "types"
(root / "template-example").mkdir(parents=True)
(root / "template-example" / "type.yaml").write_text(yaml.safe_dump(TYPE))
issues, data, _ = v.load_type(root, "template-example@0.1")
assert data is not None and not msgs(issues)
issues, data, _ = v.load_type(root, "template-example@1.0")
assert data is None and any("version" in m for m in msgs(issues))
issues, data, _ = v.load_type(root, "nope")
assert data is None
def test_template_conforms_to_the_json_schemas():
import json
import pytest
jsonschema = pytest.importorskip("jsonschema")
type_schema = json.loads((REPO / "canon/schemas/security-case-type_v0.1.schema.json").read_text())
case_schema = json.loads((REPO / "canon/schemas/security-case_v0.1.schema.json").read_text())
jsonschema.validate(TYPE, type_schema)
jsonschema.validate(CASE, case_schema)
bad = copy.deepcopy(CASE)
bad["spec"]["surprise"] = True
with pytest.raises(jsonschema.ValidationError):
jsonschema.validate(bad, case_schema)