T05: the lab and its labelled mutation catalogue
lab/app.py (users, tenants, auth, resources, sharing, read/write, revoke, audit), lab/http_api.py (JSON API + browser UI, stdlib only), 20 labelled composable version-stamped mutations, ground-truth matrix. 48 tests pass. Detection against the reference scenario: MECHANICAL 0/10 flagged (correct), DEFECT 6/6, SEMANTIC 2/4 with both inert cases declared. - F-0002: M16 and M18 initially escaped detection entirely. A use case protects exactly what it asserts. Resolved by adding two claims already stated as intent in INTENT.md; the six-mutation catalogue would never have surfaced this. - test-id axis added: stable selectors survive most UI mutations, which would make H-001 trivially false. Mutations now vary on preserves_test_ids so the hypothesis is analysed split by that axis rather than rigged. - M12 (semantic deferred revoke) and M19 (defect race) are behaviourally identical and asserted as such - the discrimination problem as a test. lab/minimal.py removed; superseded by lab/app.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 1629012@bnt-lap001 Assistant-Session: 78d4fb13-8a1e-474b-87a3-9b9261c49a39
This commit is contained in:
parent
4d3421ca57
commit
4ddb2f896c
22 changed files with 1091 additions and 207 deletions
Binary file not shown.
|
|
@ -19,7 +19,7 @@ from testdriver import (
|
|||
Scenario, SemanticAction, StateObserver, Step, UseCase,
|
||||
VerificationAsset, Watch, World,
|
||||
)
|
||||
from lab.minimal import ObservationChannel, build_baseline
|
||||
from lab.mutations import ObservationChannel, build_lab
|
||||
|
||||
RESOURCE = "R"
|
||||
API = frozenset({"api"})
|
||||
|
|
@ -40,6 +40,25 @@ def _carol_cannot_read(obs) -> bool:
|
|||
return obs["probe_read:carol:R"] is False
|
||||
|
||||
|
||||
def _bob_cannot_write(obs) -> bool:
|
||||
"""A READ grant must not confer write.
|
||||
|
||||
INTENT.md derives exactly this question from the reference use case:
|
||||
"Can Bob write when only read permission was granted?" It is part of what
|
||||
sharing means, not an extra.
|
||||
"""
|
||||
return obs["probe_write:bob:R"] is False
|
||||
|
||||
|
||||
def _revocation_is_audited(obs) -> bool:
|
||||
"""Revocation must leave a trace.
|
||||
|
||||
Enforcement being correct is not sufficient. An access change nobody can
|
||||
later evidence is a compliance failure even when the access itself is right.
|
||||
"""
|
||||
return any(event["event"] == "revoke" for event in obs["audit:R"])
|
||||
|
||||
|
||||
def _enforcement_matches_record(obs) -> bool:
|
||||
"""Enforcement and stored record must agree about every watched subject.
|
||||
|
||||
|
|
@ -77,8 +96,13 @@ USE_CASE = UseCase(
|
|||
Provenance.HUMAN, _bob_can_read, after_step="s2-grant"),
|
||||
Claim("c-carol-denied", "Carol can never read R",
|
||||
Provenance.HUMAN, _carol_cannot_read, after_step="s2-grant"),
|
||||
Claim("c-bob-cannot-write", "A READ grant does not let Bob write R",
|
||||
Provenance.HUMAN, _bob_cannot_write, after_step="s2-grant",
|
||||
source_ref="INTENT.md#security-by-use-case-mutation"),
|
||||
Claim("c-bob-revoked", "Bob cannot read R after revocation",
|
||||
Provenance.HUMAN, _bob_cannot_read, after_step="s3-revoke"),
|
||||
Claim("c-revoke-audited", "Revocation is recorded in the audit trail",
|
||||
Provenance.HUMAN, _revocation_is_audited, after_step="s3-revoke"),
|
||||
),
|
||||
invariants=(
|
||||
Invariant("i-enforcement-matches-record",
|
||||
|
|
@ -90,9 +114,15 @@ USE_CASE = UseCase(
|
|||
)
|
||||
|
||||
|
||||
def build(variant: str = "baseline"):
|
||||
"""Assemble world, driver, observer and asset from a known initial state."""
|
||||
lab, tokens = build_baseline()
|
||||
def build(*mutations: str):
|
||||
"""Assemble world, driver, observer and asset from a known initial state.
|
||||
|
||||
`mutations` names entries from the lab catalogue. The same scenario runs
|
||||
unchanged against every lab version — that is the point: the use case does
|
||||
not know the implementation moved.
|
||||
"""
|
||||
lab, tokens = build_lab(*mutations)
|
||||
variant = "+".join(mutations) if mutations else "baseline"
|
||||
cast = Cast()
|
||||
for name in ("alice", "bob", "carol"):
|
||||
cast.add(Actor(id=name, display_name=name.title(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue