From c43afd46a725efae2f8bbc4f495cfd9a9dccbb7f Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 4 Nov 2026 01:49:40 +0100 Subject: [PATCH] NK-WP-0056-T06 prep: admission helper, write-role window-open/close, service-client case; T15 evidence - bounded-executor/prepare_admission.py: the live admission from the pilot example, pins read from the image's commit, window <= 2 h (audit-core's declared bound), validated. - executor-lanes.py window-open/window-close: CCR-2026-0058's window-scoped write role, created for one admitted open window under attended platform-admin and deleted afterwards (idempotent). - security-cases/nk-bind-executor-approval.yaml: the case of record, proposed (founder acceptance). - docs/evidence/2026-11-04-nk56-t15-audit-sender: 8 checks against audit-core 876c3f6. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 523928@bnt-lap001 Assistant-Session: 6c737afb-9c14-4152-9f02-477214b9c350 --- bounded-executor/prepare_admission.py | 92 +++++++++++++++++++ .../tests/test_prepare_admission.py | 55 +++++++++++ .../README.md | 44 +++++++++ .../receipt.json | 32 +++++++ security-cases/nk-bind-executor-approval.yaml | 25 +++++ tests/test_executor_lanes.py | 50 ++++++++++ tools/service-binds/executor-lanes.py | 76 +++++++++++++-- 7 files changed, 368 insertions(+), 6 deletions(-) create mode 100644 bounded-executor/prepare_admission.py create mode 100644 bounded-executor/tests/test_prepare_admission.py create mode 100644 docs/evidence/2026-11-04-nk56-t15-audit-sender/README.md create mode 100644 docs/evidence/2026-11-04-nk56-t15-audit-sender/receipt.json create mode 100644 security-cases/nk-bind-executor-approval.yaml diff --git a/bounded-executor/prepare_admission.py b/bounded-executor/prepare_admission.py new file mode 100644 index 0000000..874827e --- /dev/null +++ b/bounded-executor/prepare_admission.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Prepare the live pilot admission (NK-WP-0056-T06) from the pilot example. + +The executor checks every pinned file against the copy inside its image (source_root /app), so the pins are taken +from the commit the pinned image was built from (`--commit`, read with `git show`), not from the working tree. +The window opens at `--not-before` and lasts `--hours`. The budget, plan, checks and scope ceiling are the +example's, so the admission stays inside the executor's declared grants (make bounded-executor-test). + + python3 bounded-executor/prepare_admission.py --id BEA-2026-0002 --commit 9bd7bf0 \\ + --not-before 2026-11-05T09:00:00Z --hours 2 --out admissions/BEA-2026-0002.yaml + +Prints the admission and plan digests. Writes nothing else and contacts nothing. +""" +from __future__ import annotations + +import argparse +import datetime as dt +import hashlib +import importlib.util +import json +import pathlib +import subprocess +import sys + +import yaml + +HERE = pathlib.Path(__file__).resolve().parent +REPO = HERE.parent +EXAMPLE = REPO / "examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml" +_spec = importlib.util.spec_from_file_location("bea_validate_prepare", REPO / "tools/bounded-execution-admission/validate.py") +bea = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(bea) + + +def _git(*args: str) -> bytes: + return subprocess.run(["git", "-C", str(REPO), *args], capture_output=True, check=True).stdout + + +def _z(t: dt.datetime) -> str: + return t.astimezone(dt.UTC).isoformat(timespec="seconds").replace("+00:00", "Z") + + +def prepare(admission_id: str, commit: str, not_before: dt.datetime, hours: float) -> dict: + full = _git("rev-parse", "--verify", commit + "^{commit}").decode().strip() + doc = bea.load(EXAMPLE) + spec = doc["spec"] + spec.update(id=admission_id, work_ref="NK-WP-0056-T06", + title="Rotate the svc-authelia directory bind (live pilot, one consumer)") + spec["window"] = {"not_before": _z(not_before), "not_after": _z(not_before + dt.timedelta(hours=hours))} + files = [{"path": f["path"], "sha256": hashlib.sha256(_git("show", f"{full}:{f['path']}")).hexdigest()} + for f in spec["plan"]["source"]["files"]] + spec["plan"]["source"].update(commit=full[:12], files=files) + pins = {f["path"]: f["sha256"] for f in files} + for check in spec["checks"]: + check["sha256"] = pins[check["ref"]] + spec["plan_digest"] = bea.plan_digest(doc) + report = bea.evaluate(doc, json.loads(bea.SCHEMA.read_text()), as_of=not_before) + if not report["valid"]: + raise ValueError("admission invalid: " + ",".join(sorted({f["rule"] for f in report["findings"]}))) + return doc + + +def main(argv=None) -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0]) + parser.add_argument("--id", required=True) + parser.add_argument("--commit", required=True, help="the commit the pinned executor image was built from") + parser.add_argument("--not-before", required=True, help="window start, RFC 3339 with offset") + parser.add_argument("--hours", type=float, default=2.0) + parser.add_argument("--out", required=True, type=pathlib.Path) + args = parser.parse_args(argv) + if args.out.exists(): + raise SystemExit("output exists; admissions are immutable, choose a new id") + if not 0 < args.hours <= 2: + raise SystemExit("--hours must be in (0, 2]: audit-core's declared bound for a run without receipt is 2 h") + try: + doc = prepare(args.id, args.commit, dt.datetime.fromisoformat(args.not_before.replace("Z", "+00:00")), + args.hours) + except (ValueError, subprocess.CalledProcessError) as error: + print(f"refused: {error}" if isinstance(error, ValueError) else "refused: unknown commit or path", + file=sys.stderr) + return 4 + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text("# NK-WP-0056-T06 live pilot admission. Immutable once requested: a change is a new id.\n" + + yaml.safe_dump(doc, sort_keys=False, width=120)) + print(json.dumps({"admission": args.id, "admission_digest": bea.admission_digest(doc), + "plan_digest": doc["spec"]["plan_digest"], "window": doc["spec"]["window"], + "pinned_commit": doc["spec"]["plan"]["source"]["commit"]}, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/bounded-executor/tests/test_prepare_admission.py b/bounded-executor/tests/test_prepare_admission.py new file mode 100644 index 0000000..5c999ca --- /dev/null +++ b/bounded-executor/tests/test_prepare_admission.py @@ -0,0 +1,55 @@ +"""Live pilot admission preparation (NK-WP-0056-T06).""" +from __future__ import annotations + +import datetime as dt +import hashlib +import importlib.util +import json +import pathlib +import subprocess +import sys +import tempfile +import unittest + +HERE = pathlib.Path(__file__).resolve().parent +spec = importlib.util.spec_from_file_location("prepare_admission", HERE.parent / "prepare_admission.py") +P = importlib.util.module_from_spec(spec) +sys.modules["prepare_admission"] = P +spec.loader.exec_module(P) +START = dt.datetime(2026, 11, 5, 9, 0, tzinfo=dt.UTC) + + +@unittest.skipUnless((P.REPO / ".git").exists(), "needs a git checkout (CI tests a source tarball)") +class Prepare(unittest.TestCase): + def setUp(self): + self.commit = subprocess.run(["git", "-C", str(P.REPO), "rev-parse", "HEAD"], capture_output=True, + text=True, check=True).stdout.strip() + + def test_pins_the_commits_content_and_is_valid(self): + doc = P.prepare("BEA-2026-0099", self.commit, START, 2) + spec = doc["spec"] + self.assertEqual(spec["window"], {"not_before": "2026-11-05T09:00:00Z", "not_after": "2026-11-05T11:00:00Z"}) + self.assertEqual(spec["plan"]["source"]["commit"], self.commit[:12]) + for f in spec["plan"]["source"]["files"]: + blob = subprocess.run(["git", "-C", str(P.REPO), "show", f"{self.commit}:{f['path']}"], + capture_output=True, check=True).stdout + self.assertEqual(f["sha256"], hashlib.sha256(blob).hexdigest()) + report = P.bea.evaluate(doc, json.loads(P.bea.SCHEMA.read_text()), as_of=START) + self.assertTrue(report["valid"], report["findings"]) + + def test_refusals(self): + with tempfile.TemporaryDirectory() as tmp: + out = pathlib.Path(tmp) / "a.yaml" + out.write_text("exists") + with self.assertRaises(SystemExit): + P.main(["--id", "BEA-2026-0099", "--commit", self.commit, "--not-before", "2026-11-05T09:00:00Z", + "--out", str(out)]) + with self.assertRaises(SystemExit): + P.main(["--id", "BEA-2026-0099", "--commit", self.commit, "--not-before", "2026-11-05T09:00:00Z", + "--hours", "3", "--out", str(pathlib.Path(tmp) / "b.yaml")]) + self.assertEqual(P.main(["--id", "BEA-2026-0099", "--commit", "0" * 40, "--not-before", + "2026-11-05T09:00:00Z", "--out", str(pathlib.Path(tmp) / "c.yaml")]), 4) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/evidence/2026-11-04-nk56-t15-audit-sender/README.md b/docs/evidence/2026-11-04-nk56-t15-audit-sender/README.md new file mode 100644 index 0000000..b2be1d3 --- /dev/null +++ b/docs/evidence/2026-11-04-nk56-t15-audit-sender/README.md @@ -0,0 +1,44 @@ +# NK-WP-0056-T15: executor audit sender against audit-core's real ingestion (2026-11-04) + +Agent-run, local only, synthetic tokens, everything removed afterwards: +`python3 bounded-executor/exercise_audit_sender.py --receipt FILE`. + +**Setup:** + +- **audit-core:** source `876c3f6`. It runs in-process with `IngestionApplication` and the SQLite backend. The sender + registry is built the way production builds it: a Secret-shaped `AUDIT_CORE_SENDERS` entry, deliberately minted with + the defaults, plus the Git scope overlay `deploy/senders-scope.json`. +- **Executor:** the real code. `executor.Run.execute()` runs on in-memory operations and emits through + `clients.AuditCore` over HTTP with a token file. Two runs: one completed, one reverted. + +Receipt: [`receipt.json`](receipt.json). It is value-free; the harness asserts that no generated token appears in it. + +| Check | Result | +| --- | --- | +| Completed run: started and receipt | 202 each | +| Reverted run: started and receipt | 202 each | +| Both emitted pairs against receipt schema v0.1 (EAR-1..EAR-10) | valid | +| Retry of the same event | 200 duplicate; the client counts it as stored | +| Same id with another body | 409; the client reports "run id reused", never retried | +| Wrong source, wrong tenant (`tenant:platform`), another sender's token, no token, unknown token | refused, and nothing stored under that run id | +| A started event with no receipt, whose admission window closed 2 h ago | `opening_without_closing`, `bound: window.not_after` | +| The closed runs | not flagged | + +**Built for this task** (`d7133e3`): + +- `canon/schemas/execution-attribution-receipt_v0.1.schema.json` +- `tools/execution-attribution-receipt/`: the validator, and fixtures generated from the executor +- the started event now carries `data.window.not_after`, which audit-core asked for in AUDIT-WP-0018-T03 + +**Limitations:** + +- audit-core runs from source, not from the released receiver image. +- The executor's operations are in-memory; the rotation itself is T12's synthetic pilot. +- Plain HTTP on loopback. + +**Live path:** + +- Custody of the sender bearer is CCR-2026-0063 (railiance-platform, proposed). It is delivered by ESO as + `sso/bounded-executor-tokens` key `audit-core`, which the run Job already mounts. +- Ingress is `audit-core-nk-bounded-executor-ingress` (T16). +- The founder seed is in audit-core `docs/bounded-executor-receipts.md`, "Founder runbook". diff --git a/docs/evidence/2026-11-04-nk56-t15-audit-sender/receipt.json b/docs/evidence/2026-11-04-nk56-t15-audit-sender/receipt.json new file mode 100644 index 0000000..1776986 --- /dev/null +++ b/docs/evidence/2026-11-04-nk56-t15-audit-sender/receipt.json @@ -0,0 +1,32 @@ +{ + "schema": "netkingdom.bounded-executor.audit-sender-exercise/v1", + "task": "NK-WP-0056-T15", + "target": "disposable local processes; synthetic tokens", + "started_at": "2026-11-04T00:40:11+00:00", + "audit_core_commit": "876c3f6647798daff56086e03d413f4559bd32a1", + "senders_scope_sha256": "bd29068f9738e3f20939a1e42f96b3c8f836ef6c1bfbc60b39361ca7aa19ebfc", + "net_kingdom_commit": "d7133e3a0489373fa7c9fbf581e36509218e496b", + "executor_source_sha256": { + "executor.py": "e82cd03eba32f4542c1d4842d13f442af8db098deb380160c3fc6c158b1d0d58", + "clients.py": "f0ce580f1beba91b7154051a35c088205de01f26d63a119a23bd926fe1c772cb" + }, + "receipt_schema_sha256": "5036bffc81380183e86a7c0c6199c8e72446f7e4e66a79ea4fc4123475d8ac3b", + "limitations": [ + "audit-core runs from source in-process (SQLite backend), not the released receiver image", + "the executor's operations are in-memory; the bind rotation itself is T12's synthetic pilot", + "plain HTTP on loopback; the live path is the ESO-delivered bearer over the sso ingress (T16)" + ], + "checks": { + "completed_run_started_and_receipt_accepted_202": true, + "reverted_run_started_and_receipt_accepted_202": true, + "emitted_pairs_pass_receipt_schema_v0_1": true, + "retry_is_duplicate_200": true, + "same_id_other_body_conflict_409_reported_as_defect": true, + "wrong_source_tenant_or_token_refused_and_not_stored": true, + "started_without_receipt_flagged_by_its_own_window": true, + "closed_runs_not_flagged": true + }, + "status": "passed", + "cleanup_complete": true, + "finished_at": "2026-11-04T00:40:11+00:00" +} diff --git a/security-cases/nk-bind-executor-approval.yaml b/security-cases/nk-bind-executor-approval.yaml new file mode 100644 index 0000000..7878853 --- /dev/null +++ b/security-cases/nk-bind-executor-approval.yaml @@ -0,0 +1,25 @@ +# KeyCape service client for the bounded executor's approval calls (NK-WP-0056-T06 prerequisite). +# The case of record; identical parameters to the type's first example. Status proposed until the founder +# accepts it (type acceptance: owner and founder). Going live makes oidc-service-client 0.2 `implemented`. +apiVersion: netkingdom.io/security-case/v0.1 +kind: SecurityCase +spec: + id: nk-bind-executor-approval + type: oidc-service-client@0.2 + status: proposed + owner: + repo: net-kingdom + person: founder + parameters: + client_id: nk-bind-executor-approval + display_name: NetKingdom bounded executor (approval-engine) + audience: approval-engine + allowed_scopes: approval:create,approval:read,approval:consume + service_subject: service:nk-bind-executor + tenant: tenant:platform + roles: bounded-executor + token_lifetime: 15m + consuming_service: bounded-executor + consumer_namespace: sso + consumer_service_account: nk-bind-executor + consumer_owner_repo: rapp-bounded-executor diff --git a/tests/test_executor_lanes.py b/tests/test_executor_lanes.py index 634bb66..c1ae30f 100644 --- a/tests/test_executor_lanes.py +++ b/tests/test_executor_lanes.py @@ -72,6 +72,8 @@ class FakeBao: if path == JOURNEY and token == 'reader': return out({'data': {'data': {'PASSWORD': 'SYNTHETIC-PW-VALUE', 'TOTP_SEED': 'SYNTHETIC-SEED-VALUE'}, 'metadata': {'version': 1}}}) return out({'data': self.store[path]}) if path in self.store else missing + if args[0] == 'delete': + self.store.pop(args[-1], None); return out({}) if args[0] == 'write': path = args[-2] if args[-1] == '-' else args[-1] if path == 'auth/token/revoke': self.revoked.append(payload['token']); return out({}) @@ -175,3 +177,51 @@ def test_verify_refusals(tmp_path, monkeypatch, mutate, reason): mutate(m, fake) r = run(m, 'verify', d) assert (r['status'], r['reason']) == ('failed', reason) + + +ADMISSION = ROOT / 'examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml' + + +def admission_at(tmp_path, m): + """The pilot example, its window around a fixed instant.""" + import datetime as dt + bea = m.load('bea_for_test', m.BEA) + doc = bea.load(ADMISSION) + path = tmp_path / 'a.yaml' + path.write_text(json.dumps(doc)) + start = bea.parse_time(doc['spec']['window']['not_before']) + return path, start + dt.timedelta(minutes=10) + + +def test_window_open_creates_the_exact_write_role_once_then_close_removes_it(tmp_path, monkeypatch): + m, fake, d = build(tmp_path, monkeypatch) + run(m, 'setup', d) + path, now = admission_at(tmp_path, m) + r = {} + m.window_open(d, path, r, now=now) + role = 'auth/kubernetes/role/' + m.LANES['write']['role'] + assert r['status'] == 'window_opened' and r['admission'] == 'BEA-2026-0001' and role in fake.store + m.check_role(fake.store[role], m.LANES['write']) + with pytest.raises(m.Refused, match='write_role_already_open'): + m.window_open(d, path, {}, now=now) + c = {} + m.window_close(path, c) + assert c['status'] == 'window_closed' and c['write_role_was_present'] and role not in fake.store + c = {} + m.window_close(path, c) # idempotent, also late + assert c['status'] == 'window_closed' and not c['write_role_was_present'] + + +@pytest.mark.parametrize('mutate, reason', [ + (lambda m, f, now: (now - __import__('datetime').timedelta(hours=1)), 'admission_window_not_open'), + (lambda m, f, now: (f.identity.update(policies=['root']), now)[1], 'attended_platform_admin_required'), + (lambda m, f, now: (setattr(f, 'cas', False), now)[1], 'cas_required_not_set'), + (lambda m, f, now: (f.store.update({m.BOUNDARY: {'policy': ''}}), now)[1], 'agent_fence_missing'), +]) +def test_window_open_refusals(tmp_path, monkeypatch, mutate, reason): + m, fake, d = build(tmp_path, monkeypatch) + run(m, 'setup', d) + path, now = admission_at(tmp_path, m) + with pytest.raises(m.Refused, match=reason): + m.window_open(d, path, {}, now=mutate(m, fake, now)) + assert 'auth/kubernetes/role/' + m.LANES['write']['role'] not in fake.store diff --git a/tools/service-binds/executor-lanes.py b/tools/service-binds/executor-lanes.py index 0d5261d..7a0c8ba 100644 --- a/tools/service-binds/executor-lanes.py +++ b/tools/service-binds/executor-lanes.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """Attended OpenBao setup and proof for the bounded executor's two lanes (NK-WP-0056-T10, T06). - write CCR-2026-0058 policy only. Its Kubernetes role is window-scoped: created per admitted run, - never here. Also sets cas_required on the svc-authelia key. + write CCR-2026-0058 policy only. Its Kubernetes role is window-scoped: created per admitted run by + window-open, deleted by window-close. setup sets cas_required on the svc-authelia key. journey CCR-2026-0059 policy and the standing read role for sso/nk-bind-executor. setup: under both approved, sha256-bound CCRs (binding confirmed) and an attended platform-admin session: @@ -11,6 +11,11 @@ setup: under both approved, sha256-bound CCRs (binding confirmed) and an attend verify: read back policies, cas_required and the role; log in as sso/nk-bind-executor (audience openbao) and prove that the token reads only the journey path, with exactly its policy and at most 15m. Prove that a wrong ServiceAccount, a wrong audience and a login to the write role are all denied. +window-open --admission FILE: for one admitted run (T06): the admission is valid, names this executor and the write + lane, and its window is open now; the policy, cas_required and the fence hold; the write role is absent. Then + create the write role exactly as CCR-2026-0058 declares it. +window-close --admission FILE: after the run's receipt or at window end, whichever comes first: delete the write role + and prove it is gone. Always allowed, also late; needs no CCR digests. Receipts hold closed booleans only. The journey values are read into memory to check their field names, and never printed or written. """ @@ -229,22 +234,81 @@ def verify(digests, kubeconfig, receipt): source_ccrs={n: c['id'] for n, c in ccrs.items()}, ccr_sha256=digests) +BEA = REPO / 'tools/bounded-execution-admission/validate.py' +WRITE_SCOPE = 'openbao/' + LANES['write']['kv'] + + +def admitted_window(admission_path, now=None): + """The admission that opens the window: valid, for this executor, naming the write lane, window open now.""" + bea = load('bea_validate_lanes', BEA) + doc = bea.load(Path(admission_path)) + now = now or __import__('datetime').datetime.now(__import__('datetime').UTC) + if not bea.evaluate(doc, json.loads(bea.SCHEMA.read_text()), as_of=now)['valid']: raise Refused('admission_invalid') + spec = doc['spec'] + if spec['executor']['identity'] != 'service:' + SA: raise Refused('admission_other_executor') + if not any(c['mode'] == 'OPERATE' and c['scope'] == WRITE_SCOPE for c in spec['scope_ceiling']): + raise Refused('admission_does_not_name_write_lane') + start, end = (bea.parse_time(spec['window'][k]) for k in ('not_before', 'not_after')) + if not start <= now < end: raise Refused('admission_window_not_open') + return {'admission': spec['id'], 'admission_digest': bea.admission_digest(doc), + 'window': dict(spec['window'])} + + +def window_open(digests, admission_path, receipt, now=None): + """Create the write role for one admitted window (CCR-2026-0058 lifecycle.role_window). Refuses if it exists.""" + receipt['phase'] = 'contract'; ccrs = contracts(digests) + receipt['phase'] = 'admission'; receipt.update(admitted_window(admission_path, now)) + receipt['phase'] = 'identity'; attended_platform_admin() + receipt['phase'] = 'preconditions' + write = LANES['write'] + policy_check(write)(data(bao(['read', '-format=json', 'sys/policies/acl/' + write['policy']]))['data']) + if not cas_required(): raise Refused('cas_required_not_set') + if not fence_present(): raise Refused('agent_fence_missing') + if not write_role_absent(): raise Refused('write_role_already_open') + receipt['phase'] = 'create_role' + ensure('auth/kubernetes/role/' + write['role'], role_payload(write), lambda actual: check_role(actual, write)) + receipt.update(phase='done', status='window_opened', write_role_created=True, exact_role=True, + close_by=receipt['window']['not_after'], source_ccrs={n: c['id'] for n, c in ccrs.items()}, + ccr_sha256=digests) + + +def window_close(admission_path, receipt): + """Delete the write role after the run's receipt or at window end (whichever comes first) and prove it is gone. + Needs no open window: closing late must always be possible.""" + bea = load('bea_validate_lanes', BEA) + spec = bea.load(Path(admission_path))['spec'] + receipt.update(admission=spec['id']) + receipt['phase'] = 'identity'; attended_platform_admin() + receipt['phase'] = 'delete_role' + was_open = not write_role_absent() + if was_open: + bao(['delete', 'auth/kubernetes/role/' + LANES['write']['role']]) + if not write_role_absent(): raise Refused('write_role_still_present') + receipt.update(phase='done', status='window_closed', write_role_was_present=was_open, write_role_absent=True) + + def main(): p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - p.add_argument('action', choices=['setup', 'verify']) - p.add_argument('--write-ccr-sha256', required=True, help='sha256 of the CCR-2026-0058 file') - p.add_argument('--journey-ccr-sha256', required=True, help='sha256 of the CCR-2026-0059 file') + p.add_argument('action', choices=['setup', 'verify', 'window-open', 'window-close']) + p.add_argument('--write-ccr-sha256', help='sha256 of the CCR-2026-0058 file (all but window-close)') + p.add_argument('--journey-ccr-sha256', help='sha256 of the CCR-2026-0059 file (all but window-close)') p.add_argument('--kubeconfig', help='admin kubeconfig; required for verify') + p.add_argument('--admission', help='the admitted BEA file; required for window-open and window-close') p.add_argument('--receipt', required=True) a = p.parse_args() if a.action == 'verify' and not a.kubeconfig: p.error('--kubeconfig is required for verify') + if a.action.startswith('window-') and not a.admission: p.error('--admission is required for ' + a.action) + if a.action != 'window-close' and not (a.write_ccr_sha256 and a.journey_ccr_sha256): + p.error('--write-ccr-sha256 and --journey-ccr-sha256 are required for ' + a.action) digests = {'write': a.write_ccr_sha256, 'journey': a.journey_ccr_sha256} fd = os.open(a.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) r = {'status': 'failed', 'action': a.action, 'values_recorded': False, 'source_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest()} try: if a.action == 'setup': setup(digests, r) - else: verify(digests, a.kubeconfig, r) + elif a.action == 'verify': verify(digests, a.kubeconfig, r) + elif a.action == 'window-open': window_open(digests, a.admission, r) + else: window_close(a.admission, r) except Exception as error: r.update(status='failed', reason=str(error) if isinstance(error, Refused) else type(error).__name__) finally: