NK-WP-0048: canon accepted; SecurityCaseType schemas, validator and template (T01, T03)
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:
parent
dcacdf57f3
commit
d8cf42bab6
11 changed files with 1122 additions and 7 deletions
40
security-case-types/_template/README.md
Normal file
40
security-case-types/_template/README.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# SecurityCaseType template
|
||||
|
||||
Copy `_template/` to `security-case-types/<type-id>/` and rewrite every
|
||||
section. Canon: `canon/standards/security-case-types_v0.1.md`. A type is
|
||||
acknowledged through informed-decision only when this README, `type.yaml`, the
|
||||
example cases and the tests are complete and green.
|
||||
|
||||
## Purpose
|
||||
|
||||
Which recurring use case the type serves, and the hand-built instances it
|
||||
replaces or adopts (reference instances). For the template: a loopback bootstrap
|
||||
identity for one operator, composed from `net-kingdom.local-identity`.
|
||||
|
||||
## Threat model
|
||||
|
||||
What the type protects, from whom, and how. Name the assets (credentials,
|
||||
sessions, signing keys), the actors (anonymous, low-assurance operator, tenant,
|
||||
compromised workload) and what each step does to prevent misuse. For the
|
||||
template: the store is loopback-only and private to its owner; the signing key
|
||||
is referenced from custody, never written into a case.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
The other shapes this pattern could have taken and why they were not chosen.
|
||||
This is the part an informed-decision is mostly about.
|
||||
|
||||
## Attended steps
|
||||
|
||||
Every step marked `attended: true`, why it cannot run unattended, and how
|
||||
ops-warden groups such steps into as few attended sessions as possible.
|
||||
|
||||
## Known limits
|
||||
|
||||
What the type does not cover, and when a request should become a new type or a
|
||||
new major version instead of a case.
|
||||
|
||||
## Verification and rollback
|
||||
|
||||
How the checks in `type.yaml` prove a case is live without reading any value,
|
||||
and how rollback restores the state from before the case.
|
||||
15
security-case-types/_template/examples/example-operator.yaml
Normal file
15
security-case-types/_template/examples/example-operator.yaml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Example SecurityCase for the template type. Example values only.
|
||||
apiVersion: netkingdom.io/security-case/v0.1
|
||||
kind: SecurityCase
|
||||
spec:
|
||||
id: example-operator
|
||||
type: template-example@0.1
|
||||
status: proposed
|
||||
owner:
|
||||
repo: net-kingdom
|
||||
person: example-owner
|
||||
parameters:
|
||||
operator_username: bootstrap-admin
|
||||
operator_fullname: Bootstrap Reference Operator
|
||||
operator_email: bootstrap@example.invalid
|
||||
signing_key_ref: bao:platform/workloads/example/local-identity-signing
|
||||
53
security-case-types/_template/tests/test_type.py
Normal file
53
security-case-types/_template/tests/test_type.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""Tests every SecurityCaseType carries: the type validates, its example cases
|
||||
validate against it, and each example renders into a composer plan.
|
||||
|
||||
Copied with the template; keep it and add type-specific tests beside it.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
TYPE_DIR = Path(__file__).resolve().parents[1]
|
||||
REPO = TYPE_DIR.parents[1]
|
||||
VALIDATOR = REPO / "tools" / "security-case-types" / "security_case_validator.py"
|
||||
SPEC = importlib.util.spec_from_file_location("security_case_validator", VALIDATOR)
|
||||
validator = importlib.util.module_from_spec(SPEC)
|
||||
sys.modules[SPEC.name] = validator
|
||||
SPEC.loader.exec_module(validator)
|
||||
|
||||
TYPE = yaml.safe_load((TYPE_DIR / "type.yaml").read_text())
|
||||
EXAMPLES = sorted((TYPE_DIR / "examples").glob("*.yaml"))
|
||||
DECLARATIONS = sorted(str(p) for p in (REPO / "capabilities" / "playbooks").glob("*.yaml"))
|
||||
# Until the type is implemented, examples are checked as if it were.
|
||||
IMPLEMENTED = TYPE["spec"]["status"] == "implemented"
|
||||
|
||||
|
||||
def errors(issues):
|
||||
return [f"{i.path}: {i.message}" for i in issues if i.level == "ERROR"]
|
||||
|
||||
|
||||
def test_type_is_complete():
|
||||
assert errors(validator.validate_type(TYPE, TYPE_DIR)) == []
|
||||
|
||||
|
||||
def test_examples_exist_and_validate():
|
||||
assert EXAMPLES
|
||||
for path in EXAMPLES:
|
||||
case = yaml.safe_load(path.read_text())
|
||||
assert errors(validator.validate_case(case, TYPE, require_implemented=IMPLEMENTED)) == [], path.name
|
||||
|
||||
|
||||
def test_examples_render_deterministically():
|
||||
declarations, load_issues = validator.composer.load_declarations(DECLARATIONS)
|
||||
assert errors(load_issues) == []
|
||||
for path in EXAMPLES:
|
||||
case = yaml.safe_load(path.read_text())
|
||||
issues, first = validator.render_case(case, TYPE, declarations, require_implemented=IMPLEMENTED)
|
||||
assert errors(issues) == [], path.name
|
||||
_, second = validator.render_case(case, TYPE, declarations, require_implemented=IMPLEMENTED)
|
||||
assert first["digest"] == second["digest"]
|
||||
assert first["execution"] == {"permitted": False}
|
||||
assert first["composition"]["execution"]["permitted"] is False
|
||||
115
security-case-types/_template/type.yaml
Normal file
115
security-case-types/_template/type.yaml
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# SecurityCaseType template (canon/standards/security-case-types_v0.1.md).
|
||||
#
|
||||
# Copy this directory to security-case-types/<type-id>/ and replace every part.
|
||||
# This template is itself a valid, runnable type: it composes the checked-in
|
||||
# net-kingdom.local-identity declaration (a C0 bootstrap identity), so that its
|
||||
# tests prove the validator and renderer end to end. It is not for real cases.
|
||||
apiVersion: netkingdom.io/security-case-type/v0.1
|
||||
kind: SecurityCaseType
|
||||
spec:
|
||||
id: template-example
|
||||
version: "0.1"
|
||||
status: draft
|
||||
title: "Template: local bootstrap identity for one operator"
|
||||
purpose: >-
|
||||
Shows every section a type must define, using a loopback bootstrap identity
|
||||
as the example. A real type states which recurring use case it serves and
|
||||
which hand-built instances it replaces.
|
||||
|
||||
# What a case fills in. Secret values are never parameters: a secret appears
|
||||
# only as a custody reference (sensitivity: secret_reference).
|
||||
parameters:
|
||||
- name: operator_username
|
||||
type: string
|
||||
case_settable: true
|
||||
constraints: {pattern: "^[a-z_][a-z0-9_-]{0,31}$"}
|
||||
sensitivity: operational
|
||||
description: Username of the one operator this case is for.
|
||||
- name: operator_fullname
|
||||
type: string
|
||||
case_settable: true
|
||||
constraints: {pattern: "^[^\\n\\r]{1,120}$"}
|
||||
sensitivity: operational
|
||||
description: Display name of the operator.
|
||||
- name: operator_email
|
||||
type: string
|
||||
case_settable: true
|
||||
constraints: {pattern: "^[^@\\s]+@[^@\\s]+$"}
|
||||
sensitivity: operational
|
||||
description: Contact address of the operator.
|
||||
- name: signing_key_ref
|
||||
type: string
|
||||
case_settable: true
|
||||
constraints: {pattern: "^bao:platform/workloads/[a-z0-9/-]+$"}
|
||||
sensitivity: secret_reference
|
||||
description: Custody reference for the signing key; never the key itself.
|
||||
- name: store_home
|
||||
type: string
|
||||
case_settable: false
|
||||
default: .local/local-identity
|
||||
sensitivity: operational
|
||||
description: Where the store lives; fixed by the type, not by cases.
|
||||
|
||||
# Which declared playbook capabilities the type composes, with exact provider
|
||||
# pins, and how case parameters bind to the declarations' parameters.
|
||||
composition:
|
||||
authority: netkingdom
|
||||
initial_trust: [bare_host_trust]
|
||||
capabilities: [c0.bootstrap-identity]
|
||||
providers:
|
||||
c0.bootstrap-identity: net-kingdom.local-identity
|
||||
parameter_bindings:
|
||||
net-kingdom.local-identity.bootstrap_username: operator_username
|
||||
net-kingdom.local-identity.bootstrap_fullname: operator_fullname
|
||||
net-kingdom.local-identity.bootstrap_email: operator_email
|
||||
net-kingdom.local-identity.local_identity_home: store_home
|
||||
|
||||
# What ops-mason builds per case (structure only, never values).
|
||||
materialization:
|
||||
- id: signing-key-path
|
||||
kind: kv_path
|
||||
template: "{signing_key_ref}"
|
||||
- id: signing-key-policy
|
||||
kind: openbao_policy
|
||||
template: "local-identity-{operator_username}-read"
|
||||
- id: catalog-entry
|
||||
kind: catalog_entry
|
||||
template: "local-identity-{operator_username}"
|
||||
|
||||
# What ops-warden carries out, step by step, on whose entry point.
|
||||
follow_through:
|
||||
- id: init-store
|
||||
owner: net-kingdom
|
||||
entry_point: "uv run --project local-identity local-identity init"
|
||||
auth: "local operator shell on the target host"
|
||||
warden_executes: false
|
||||
attended: true
|
||||
attended_justification: >-
|
||||
The bootstrap identity exists before any managed identity, so no
|
||||
unattended principal can run it.
|
||||
|
||||
# Checks that prove the case is live; never reading a secret value.
|
||||
verification:
|
||||
- id: discovery-serves
|
||||
check: "loopback discovery document issuer matches the planned port"
|
||||
expect: present
|
||||
reads_secret_value: false
|
||||
- id: store-private
|
||||
check: "store under {store_home} is not readable by other users"
|
||||
expect: deny
|
||||
reads_secret_value: false
|
||||
|
||||
# The exact inverse of every follow-through step and materialized item.
|
||||
rollback:
|
||||
- reverses: init-store
|
||||
action: "stop the loopback issuer and remove {store_home}"
|
||||
- reverses: signing-key-path
|
||||
action: "remove the KV path {signing_key_ref}"
|
||||
- reverses: signing-key-policy
|
||||
action: "delete policy local-identity-{operator_username}-read"
|
||||
- reverses: catalog-entry
|
||||
action: "remove catalog entry local-identity-{operator_username}"
|
||||
|
||||
acceptance:
|
||||
accepted_by: [owner]
|
||||
risk_class: low
|
||||
Loading…
Add table
Add a link
Reference in a new issue