Implement TEN-WP-0011 security layer conformance
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 37s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 37s
Engine/PIP declaration is now checkable (layer.yaml plus a Tooling-client scan). Writes persist a decision record or the published fail-closed stance, live-lookup freshness is published, events_for is tenant-scoped, and mutation evidence drains to audit-core from a local outbox without blocking the mutation. Sender registration is requested as AUDIT-IN-0002. Boundary-contract amendment is requested as NET-IN-0002. Assistant: grok Assistant-Session: 01a04cea-e5e8-7081-a0fc-808ebbc35fa9
This commit is contained in:
parent
80961af91e
commit
672cf4da6e
40 changed files with 2285 additions and 361 deletions
149
scripts/check_layer_conformance.py
Normal file
149
scripts/check_layer_conformance.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Check tenant-engine against the NetKingdom security layer model (§5, §11).
|
||||
|
||||
Read-only. Makes two mechanical checks:
|
||||
|
||||
1. INTENT.md frontmatter and layer.yaml agree on layer and role.
|
||||
2. No catalogued Tooling client (OpenBao, key-cape) appears in src/
|
||||
unless it maps to a declared §5.1 / §5.2 / §5.3 entry.
|
||||
|
||||
PostgreSQL / SQLite / httpx-to-flex-auth / httpx-to-audit-core are not
|
||||
Tooling contacts. They are listed in layer.yaml non_tooling_clients so
|
||||
the inventory is total.
|
||||
|
||||
Exit 0 clean, 1 undeclared contact, 2 declaration malformed.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError: # pragma: no cover - dev extra
|
||||
print("FAIL: PyYAML is required (pip install pyyaml)", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC = ROOT / "src" / "tenant_engine"
|
||||
DECL = ROOT / "layer.yaml"
|
||||
INTENT = ROOT / "INTENT.md"
|
||||
|
||||
# Catalogued Tooling in statute §4 today: key-cape and OpenBao.
|
||||
# Import roots that would constitute a direct client of those.
|
||||
TOOLING_IMPORTS = {
|
||||
"hvac": "OpenBao / Vault client",
|
||||
"bao": "OpenBao client",
|
||||
"keycloak": "key-cape / Keycloak client",
|
||||
"ldap3": "direct LDAP client (key-cape tooling)",
|
||||
"python_ldap": "direct LDAP client (key-cape tooling)",
|
||||
}
|
||||
|
||||
TOOLING_ARGV = re.compile(r"""\[\s*(?:["']bao["']|bao_bin\b|bao_binary\b)\s*,""")
|
||||
OPENBAO_ADDR = re.compile(r"\b(?:VAULT_ADDR|BAO_ADDR|X-Vault-Token)\b")
|
||||
|
||||
|
||||
def load_declaration() -> dict:
|
||||
if not DECL.exists():
|
||||
print(f"FAIL: no declaration at {DECL.relative_to(ROOT)} (§11)", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
try:
|
||||
data = yaml.safe_load(DECL.read_text())
|
||||
except yaml.YAMLError as exc:
|
||||
print(f"FAIL: {DECL.name} is not parseable: {exc}", file=sys.stderr)
|
||||
raise SystemExit(2) from exc
|
||||
for key in ("layer", "role", "repository", "standard_version", "tooling_contacts"):
|
||||
if key not in data:
|
||||
print(f"FAIL: {DECL.name} missing required key '{key}'", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
if str(data["layer"]).lower() != "engine":
|
||||
print(f"FAIL: declared layer is {data['layer']!r}, expected engine", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
if str(data["role"]).lower() != "pip":
|
||||
print(f"FAIL: declared role is {data['role']!r}, expected pip", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
if data["repository"] != "tenant-engine":
|
||||
print(f"FAIL: repository is {data['repository']!r}", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
return data
|
||||
|
||||
|
||||
def intent_frontmatter() -> dict:
|
||||
text = INTENT.read_text()
|
||||
if not text.startswith("---"):
|
||||
print("FAIL: INTENT.md has no YAML frontmatter", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
block = text.split("---", 2)[1]
|
||||
data = yaml.safe_load(block) or {}
|
||||
if str(data.get("layer", "")).lower() != "engine":
|
||||
print(f"FAIL: INTENT.md layer is {data.get('layer')!r}, expected Engine", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
if str(data.get("role", "")).lower() != "pip":
|
||||
print(f"FAIL: INTENT.md role is {data.get('role')!r}, expected PIP", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
return data
|
||||
|
||||
|
||||
def imported_modules(path: Path) -> set[str]:
|
||||
try:
|
||||
tree = ast.parse(path.read_text())
|
||||
except SyntaxError:
|
||||
return set()
|
||||
found: set[str] = set()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
found.update(alias.name.split(".")[0] for alias in node.names)
|
||||
elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module:
|
||||
found.add(node.module.split(".")[0])
|
||||
return found
|
||||
|
||||
|
||||
def scan() -> list[tuple[Path, str, str]]:
|
||||
hits: list[tuple[Path, str, str]] = []
|
||||
for path in sorted(SRC.rglob("*.py")):
|
||||
text = path.read_text()
|
||||
for module in sorted(imported_modules(path)):
|
||||
if module in TOOLING_IMPORTS:
|
||||
hits.append((path, module, TOOLING_IMPORTS[module]))
|
||||
if TOOLING_ARGV.search(text) or OPENBAO_ADDR.search(text):
|
||||
hits.append((path, "openbao-invocation", "OpenBao argv or address"))
|
||||
return hits
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--report", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
decl = load_declaration()
|
||||
intent_frontmatter()
|
||||
hits = scan()
|
||||
|
||||
if args.report:
|
||||
print(f"tenant-engine — layer {decl['layer']}, role {decl['role']}, "
|
||||
f"standard v{decl['standard_version']}")
|
||||
print(f" tooling contacts declared: {len(decl.get('tooling_contacts') or [])}")
|
||||
print(f" non-tooling clients: {len(decl.get('non_tooling_clients') or [])}")
|
||||
|
||||
if hits:
|
||||
print("", file=sys.stderr)
|
||||
print("FAIL: undeclared Tooling-layer client (§11 undeclared violation)", file=sys.stderr)
|
||||
for path, module, what in hits:
|
||||
print(f" {path.relative_to(ROOT)}: {module} — {what}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if decl.get("tooling_contacts"):
|
||||
print("FAIL: tooling_contacts is not empty; this engine claimed none", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if not args.report:
|
||||
print(f"OK: Engine/PIP declaration matches INTENT.md; no Tooling client in "
|
||||
f"{SRC.relative_to(ROOT)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue