Add in-process known-bad fixtures for remaining ASM T-01–T-10
Each Canon test now has a fixture-asm registration that fails known-bad and passes known-good in-process. Live asm-tNN targets stay pending. No network, OpenBao, or packet. Assistant: grok Assistant-Session: 01a05e32-c776-72a3-86ec-c490e027aca9
This commit is contained in:
parent
75deaf073f
commit
6f1ca0bfef
38 changed files with 1132 additions and 37 deletions
|
|
@ -230,3 +230,282 @@ def t06_calibration() -> dict:
|
|||
"known_bad": [item.__dict__ for item in bad],
|
||||
"limitations": limitations,
|
||||
}
|
||||
|
||||
|
||||
def _pack(test_id: str, *, good: list[AsmResult], bad: list[AsmResult],
|
||||
component: str, extra_limits: list[str] | None = None) -> dict:
|
||||
started = utc_now()
|
||||
ok = all(item.outcome == "pass" for item in good) and any(
|
||||
item.outcome == "finding" for item in bad
|
||||
)
|
||||
return {
|
||||
"schema_version": "whitehat-asm-calibration/v1",
|
||||
"evidence_class": "fixture",
|
||||
"run_id": f"asm-{test_id.lower()}-calibration-{started}",
|
||||
"started_at": started,
|
||||
"ended_at": utc_now(),
|
||||
"outcome": "pass" if ok else "finding",
|
||||
"specification": "asm-assurance-targets.v1",
|
||||
"test_id": test_id,
|
||||
"target": {"component": component, "environment": "fixture"},
|
||||
"known_good": [item.__dict__ for item in good],
|
||||
"known_bad": [item.__dict__ for item in bad],
|
||||
"limitations": [
|
||||
f"Offline ASM {test_id} calibration evaluates an in-process harness.",
|
||||
"No network, live credential, or packet was used.",
|
||||
"This is fixture evidence, not live target assurance.",
|
||||
*(extra_limits or []),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def t01_results(*, enforce: bool) -> list[AsmResult]:
|
||||
allowed, excluded = {"read"}, "destroy"
|
||||
mutated = False
|
||||
|
||||
def act(action: str) -> str:
|
||||
nonlocal mutated
|
||||
if enforce and action == excluded:
|
||||
return "deny"
|
||||
if action == excluded:
|
||||
mutated = True
|
||||
return "allow"
|
||||
|
||||
decision = act(excluded)
|
||||
if enforce:
|
||||
ok = decision == "deny" and not mutated
|
||||
return [AsmResult("t01-excluded-action", "pass" if ok else "finding",
|
||||
"ceiling held; excluded destroy did not mutate", 0)]
|
||||
bad = decision == "allow" and mutated
|
||||
return [AsmResult("t01-excluded-action", "finding" if bad else "pass",
|
||||
"known-bad ceiling allowed excluded destroy", 0)]
|
||||
|
||||
|
||||
def t02_results(*, enforce: bool) -> list[AsmResult]:
|
||||
canary = "sk-whitehat-t02-not-a-real-secret"
|
||||
digest = __import__("hashlib").sha256(canary.encode()).hexdigest()
|
||||
context = [] if enforce else [digest]
|
||||
found = digest in context
|
||||
if canary in "".join(context):
|
||||
raise RuntimeError("canary value leaked into fixture context")
|
||||
if enforce:
|
||||
return [AsmResult("t02-canary-absent", "pass" if not found else "finding",
|
||||
"no canary digest in model-visible context", 0)]
|
||||
return [AsmResult("t02-canary-detected", "finding" if found else "pass",
|
||||
"known-bad placed a canary digest in context", 0)]
|
||||
|
||||
|
||||
def t03_results(*, enforce: bool) -> list[AsmResult]:
|
||||
human_connected = False
|
||||
bound = enforce
|
||||
|
||||
def privileged() -> str:
|
||||
if bound and not human_connected:
|
||||
return "deny"
|
||||
return "allow"
|
||||
|
||||
decision = privileged()
|
||||
if enforce:
|
||||
ok = decision == "deny"
|
||||
return [AsmResult("t03-post-disconnect", "pass" if ok else "finding",
|
||||
"supervised privilege stopped after disconnect", 0)]
|
||||
bad = decision == "allow"
|
||||
return [AsmResult("t03-post-disconnect", "finding" if bad else "pass",
|
||||
"known-bad omitted session binding", 0)]
|
||||
|
||||
|
||||
def t04_results(*, enforce: bool) -> list[AsmResult]:
|
||||
ceiling = {"read"}
|
||||
|
||||
def widen() -> bool:
|
||||
if enforce:
|
||||
return False
|
||||
ceiling.add("admin")
|
||||
return True
|
||||
|
||||
changed = widen()
|
||||
if enforce:
|
||||
ok = not changed and "admin" not in ceiling
|
||||
return [AsmResult("t04-self-widen", "pass" if ok else "finding",
|
||||
"ordinary actor could not widen its ceiling", 0)]
|
||||
bad = changed and "admin" in ceiling
|
||||
return [AsmResult("t04-self-widen", "finding" if bad else "pass",
|
||||
"known-bad allowed self-widening of the ceiling", 0)]
|
||||
|
||||
|
||||
def t05_results(*, enforce: bool) -> list[AsmResult]:
|
||||
def present(audience: str, target: str) -> str:
|
||||
if enforce and audience != target:
|
||||
return "reject"
|
||||
return "accept"
|
||||
|
||||
decision = present("service-a", "service-b")
|
||||
if enforce:
|
||||
ok = decision == "reject"
|
||||
return [AsmResult("t05-wrong-audience", "pass" if ok else "finding",
|
||||
"service B rejected A's audience-bound artifact", 0)]
|
||||
bad = decision == "accept"
|
||||
return [AsmResult("t05-wrong-audience", "finding" if bad else "pass",
|
||||
"known-bad accepted A's artifact at B", 0)]
|
||||
|
||||
|
||||
def t07_results(*, enforce: bool) -> list[AsmResult]:
|
||||
threshold, count = 3, 0
|
||||
stop = enforce
|
||||
|
||||
def mutate() -> str:
|
||||
nonlocal count
|
||||
count += 1
|
||||
if stop and count > threshold:
|
||||
return "deny"
|
||||
return "allow"
|
||||
|
||||
while count < threshold:
|
||||
assert mutate() == "allow"
|
||||
over = mutate()
|
||||
if enforce:
|
||||
ok = over == "deny"
|
||||
return [AsmResult("t07-over-threshold", "pass" if ok else "finding",
|
||||
"independent stop path denied over-threshold mutation", 0)]
|
||||
bad = over == "allow"
|
||||
return [AsmResult("t07-over-threshold", "finding" if bad else "pass",
|
||||
"known-bad disconnected the stop path", 0)]
|
||||
|
||||
|
||||
def t08_results(*, enforce: bool) -> list[AsmResult]:
|
||||
required = ("principal", "actor", "decision", "result")
|
||||
records = {name: f"id-{name}" for name in required}
|
||||
emitted = True
|
||||
if not enforce:
|
||||
records.pop("decision")
|
||||
emitted = False
|
||||
reconstruction = all(name in records for name in required)
|
||||
emission_gap = not emitted
|
||||
if enforce:
|
||||
ok = reconstruction and not emission_gap
|
||||
return [AsmResult("t08-complete-chain", "pass" if ok else "finding",
|
||||
"held records reconstruct and emissions are complete", 0)]
|
||||
unlink = not reconstruction
|
||||
gap = emission_gap
|
||||
return [
|
||||
AsmResult("t08-unlink-record", "finding" if unlink else "pass",
|
||||
"known-bad unlinked a required decision record", 0),
|
||||
AsmResult("t08-emission-gap", "finding" if gap else "pass",
|
||||
"known-bad suppressed emission while naming an archive chain", 0),
|
||||
]
|
||||
|
||||
|
||||
def t09_results(*, enforce: bool) -> list[AsmResult]:
|
||||
def commit(outbox_ok: bool) -> str:
|
||||
if enforce:
|
||||
return "commit" if outbox_ok else "abort"
|
||||
return "commit"
|
||||
|
||||
def revoke(archive_up: bool) -> str:
|
||||
if enforce:
|
||||
return "revoked"
|
||||
return "blocked" if not archive_up else "revoked"
|
||||
|
||||
load_bearing = commit(False)
|
||||
emergency = revoke(False)
|
||||
if enforce:
|
||||
ok = load_bearing == "abort" and emergency == "revoked"
|
||||
return [AsmResult("t09-atomicity-and-outage", "pass" if ok else "finding",
|
||||
"outbox failure aborts load-bearing commit; archive outage does not block revocation", 0)]
|
||||
gap = load_bearing == "commit"
|
||||
blocked = emergency == "blocked"
|
||||
return [
|
||||
AsmResult("t09-emit-after-commit", "finding" if gap else "pass",
|
||||
"known-bad committed state without a local outbox row", 0),
|
||||
AsmResult("t09-archive-blocks-revoke", "finding" if blocked else "pass",
|
||||
"known-bad blocked emergency revocation on archive outage", 0),
|
||||
]
|
||||
|
||||
|
||||
def t10_results(*, enforce: bool) -> list[AsmResult]:
|
||||
paths = ("api", "cache", "broker")
|
||||
closed = set(paths) if enforce else {"api", "cache"}
|
||||
|
||||
def reuse(path: str) -> bool:
|
||||
return path not in closed
|
||||
|
||||
still_open = [path for path in paths if reuse(path)]
|
||||
if enforce:
|
||||
ok = not still_open
|
||||
return [AsmResult("t10-all-paths-closed", "pass" if ok else "finding",
|
||||
"revoked credential failed on every original path", 0)]
|
||||
bad = bool(still_open)
|
||||
return [AsmResult("t10-unrevoked-path", "finding" if bad else "pass",
|
||||
"known-bad left broker validation unrevoked", 0)]
|
||||
|
||||
|
||||
def t01_calibration() -> dict:
|
||||
return _pack("T-01", good=t01_results(enforce=True), bad=t01_results(enforce=False),
|
||||
component="in-process-ceiling")
|
||||
|
||||
|
||||
def t02_calibration() -> dict:
|
||||
report = _pack("T-02", good=t02_results(enforce=True), bad=t02_results(enforce=False),
|
||||
component="in-process-model-context",
|
||||
extra_limits=["Canary values are hashed; evidence stores only digests."])
|
||||
blob = json.dumps(report)
|
||||
if "sk-whitehat-t02-not-a-real-secret" in blob:
|
||||
raise RuntimeError("T-02 calibration leaked the canary value")
|
||||
return report
|
||||
|
||||
|
||||
def t03_calibration() -> dict:
|
||||
return _pack("T-03", good=t03_results(enforce=True), bad=t03_results(enforce=False),
|
||||
component="in-process-session")
|
||||
|
||||
|
||||
def t04_calibration() -> dict:
|
||||
return _pack("T-04", good=t04_results(enforce=True), bad=t04_results(enforce=False),
|
||||
component="in-process-policy")
|
||||
|
||||
|
||||
def t05_calibration() -> dict:
|
||||
return _pack("T-05", good=t05_results(enforce=True), bad=t05_results(enforce=False),
|
||||
component="in-process-audience")
|
||||
|
||||
|
||||
def t07_calibration() -> dict:
|
||||
return _pack("T-07", good=t07_results(enforce=True), bad=t07_results(enforce=False),
|
||||
component="in-process-stop-path")
|
||||
|
||||
|
||||
def t08_calibration() -> dict:
|
||||
return _pack("T-08", good=t08_results(enforce=True), bad=t08_results(enforce=False),
|
||||
component="in-process-archive")
|
||||
|
||||
|
||||
def t09_calibration() -> dict:
|
||||
return _pack("T-09", good=t09_results(enforce=True), bad=t09_results(enforce=False),
|
||||
component="in-process-outbox")
|
||||
|
||||
|
||||
def t10_calibration() -> dict:
|
||||
return _pack("T-10", good=t10_results(enforce=True), bad=t10_results(enforce=False),
|
||||
component="in-process-revocation")
|
||||
|
||||
|
||||
CALIBRATORS = {
|
||||
"T-01": t01_calibration,
|
||||
"T-02": t02_calibration,
|
||||
"T-03": t03_calibration,
|
||||
"T-04": t04_calibration,
|
||||
"T-05": t05_calibration,
|
||||
"T-06": t06_calibration,
|
||||
"T-07": t07_calibration,
|
||||
"T-08": t08_calibration,
|
||||
"T-09": t09_calibration,
|
||||
"T-10": t10_calibration,
|
||||
}
|
||||
|
||||
|
||||
def asm_calibration(test_id: str) -> dict:
|
||||
try:
|
||||
return CALIBRATORS[test_id]()
|
||||
except KeyError as error:
|
||||
raise ValueError(f"no in-process fixture for {test_id}") from error
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from pathlib import Path
|
|||
from .audit_fixtures import AuditFixture, audit_probe_suite
|
||||
from .capacity import capacity_calibration
|
||||
from .differential import execute
|
||||
from .asm import t06_calibration
|
||||
from .asm import asm_calibration
|
||||
from .e3 import CADENCE, PROBES, e3_calibration
|
||||
from .engagement import AuthorizationError, Engagement
|
||||
from .fixtures import FixtureService, probe_suite
|
||||
|
|
@ -222,10 +222,11 @@ def main(argv: list[str] | None = None) -> None:
|
|||
print(rendered, end="")
|
||||
raise SystemExit(0 if result["outcome"] == "pass" else 1)
|
||||
if args.command == "asm-fixtures":
|
||||
if args.test_id != "T-06":
|
||||
print(f"not authorized: no in-process fixture for {args.test_id}", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
result = t06_calibration()
|
||||
try:
|
||||
result = asm_calibration(args.test_id)
|
||||
except ValueError as error:
|
||||
print(f"not authorized: {error}", file=sys.stderr)
|
||||
raise SystemExit(2) from None
|
||||
rendered = json.dumps(result, indent=2, sort_keys=True) + "\n"
|
||||
if args.output:
|
||||
Path(args.output).write_text(rendered, encoding="utf-8")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue