e74a8c2f5d
---ci--- project: acdl phase: 42 milestone: v1.9 status: execute ---/ci--- Phase 42 — stub-implementation (REQ-107..111, D-084): route_halt_artifact (REQ-107): - core/separation_of_duties.py: real SNS publish (ACDL_SOD_HALT_TOPIC_ARN) + outbox fallback (SEPARATION_OF_DUTIES_VIOLATION event via outbox_writer) + stderr emission. No silent print-only stub. - terraform/platform/main.tf: aws_sns_topic.acdl-sod-halt + output. HITL attestation gates (REQ-108): - core/hitl_gates.py: attest(contract_id, env, approver, evidence, outbox_client) records approver_qa/approver_prod/approver_dr to outbox, runs SoD check on prod, invokes attestation matrix, returns (ok, reason). Dev skips (autonomous). approver_from_env() reads GITHUB_ACTOR/GITEA_ACTOR. - scripts/run_platform.sh: Step 7b HITL gate before apply for qa/prod/dr. 8-concern attestation matrix (REQ-109, D-084): - core/attestation_matrix.py: check(env, evidence) runs the 8 concerns from hitl_matrix_design.md §10.4. Offline-testable (contract_nfrs, schema_validity, policy_pass) run for real. Operator-supplied accept signed artifacts validated for freshness (FRESHNESS_DAYS table) + schema. Signature skip when ACDL_ATTESTATION_SIGNING_KEY_ID unset (D-089). Fail loud if missing/expired for prod/dr. Wiz real client (REQ-110): - adapters/wiz/wiz_adapter.py: WizClient (GraphQL API, Bearer auth, pagination via pageInfo.hasNextPage + endCursor). fetch_and_adapt translates issues → PolicyCheckResult; graceful degrade when WIZ_API_TOKEN/WIZ_API_URL unset. Kyverno fleshed out (REQ-111): - adapters/kyverno/kyverno_adapter.py: full PolicyReport → PolicyCheckResult mapping (pass/fail/skip/warn + severity + skip-with- reason + resource ref construction from kind/name/namespace). adapt_inactive() emits KYVERNO_INACTIVE_TF_STACK guard. --kube-version stub parsed for future GitOps. Tests: +47 (test_route_halt_artifact.py, test_hitl_gates.py, test_attestation_matrix.py, test_wiz_adapter_real_client.py, expanded test_kyverno_adapter.py). Existing wiz_adapter tests updated for the real client's control.name ruleId. 493 passed; run_ci.sh green; run_platform.sh --check-only green.
91 lines
3.1 KiB
Python
91 lines
3.1 KiB
Python
"""HITL pre-execution attestation gates (REQ-108, D-084).
|
|
|
|
Records the approver identity (`gitea.actor` / `github.actor`) to the
|
|
DynamoDB outbox for the contractId (attribute `approver_qa` /
|
|
`approver_prod` / `approver_dr`), runs the separation-of-duties check on
|
|
prod, invokes the 8-concern attestation matrix for the target env, and
|
|
returns (ok, reason). Dev skips (autonomous). `scripts/run_platform.sh`
|
|
calls `attest` before apply for qa/prod/dr.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from typing import Optional, Tuple
|
|
|
|
|
|
def _approver_attr(env: str) -> str:
|
|
return {"qa": "approver_qa", "prod": "approver_prod", "dr": "approver_dr"}.get(env, "")
|
|
|
|
|
|
def attest(contract_id: str, env: str, approver: str,
|
|
evidence: Optional[dict] = None,
|
|
outbox_client=None) -> Tuple[bool, str]:
|
|
"""Attest a promotion gate for the given environment.
|
|
|
|
Args:
|
|
contract_id: the contract UUID.
|
|
env: dev/qa/prod/dr.
|
|
approver: the approver's username (`gitea.actor` / `github.actor`).
|
|
evidence: optional operator-supplied evidence artifacts (for the
|
|
attestation matrix operator-supplied concerns).
|
|
outbox_client: optional moto-mocked DynamoDB outbox client for tests.
|
|
|
|
Returns:
|
|
(ok, reason). ok=False means block the promotion.
|
|
"""
|
|
if env == "dev":
|
|
return (True, "dev autonomous (no HITL gate)")
|
|
|
|
if not approver:
|
|
return (False, f"no approver identity for {env} (GITHUB_ACTOR/GITEA_ACTOR unset)")
|
|
|
|
attr = _approver_attr(env)
|
|
if not attr:
|
|
return (False, f"unknown environment: {env}")
|
|
|
|
# Record the approver to the outbox.
|
|
if outbox_client is not None:
|
|
outbox_client.put_approver(contract_id, attr, approver)
|
|
|
|
# Run the separation-of-duties check on prod.
|
|
if env == "prod":
|
|
from core.separation_of_duties import check as sod_check, route_halt_artifact
|
|
ok, reason = sod_check(outbox_client, contract_id, approver)
|
|
if not ok:
|
|
route_halt_artifact(contract_id, reason, oncall_client=None)
|
|
return (False, reason)
|
|
|
|
# Run the 8-concern attestation matrix.
|
|
from core.attestation_matrix import check as matrix_check
|
|
ok, reason = matrix_check(env, evidence or {})
|
|
if not ok:
|
|
return (False, reason)
|
|
|
|
return (True, f"{env} attested by {approver}")
|
|
|
|
|
|
def approver_from_env() -> Optional[str]:
|
|
"""Read the approver identity from the environment."""
|
|
return os.environ.get("GITHUB_ACTOR") or os.environ.get("GITEA_ACTOR")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# CLI: hitl_gates.py <contract_id> <env> [evidence.json]
|
|
if len(sys.argv) < 3:
|
|
print("usage: hitl_gates.py <contract_id> <env> [evidence.json]", file=sys.stderr)
|
|
sys.exit(2)
|
|
_cid = sys.argv[1]
|
|
_env = sys.argv[2]
|
|
_evidence = {}
|
|
if len(sys.argv) >= 4 and os.path.isfile(sys.argv[3]):
|
|
import json
|
|
with open(sys.argv[3]) as f:
|
|
_evidence = json.load(f)
|
|
_approver = approver_from_env() or ""
|
|
ok, reason = attest(_cid, _env, _approver, _evidence)
|
|
if ok:
|
|
print(f"HITL PASS: {reason}")
|
|
sys.exit(0)
|
|
else:
|
|
print(f"HITL BLOCK: {reason}", file=sys.stderr)
|
|
sys.exit(1) |