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.
68 lines
3.0 KiB
Python
68 lines
3.0 KiB
Python
"""REQ-107: route_halt_artifact is a real implementation (SNS + outbox fallback)."""
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from core.separation_of_duties import route_halt_artifact
|
|
|
|
|
|
def test_route_halt_publishes_to_sns_when_arn_set(monkeypatch):
|
|
"""With ACDL_SOD_HALT_TOPIC_ARN set, the SNS client receives the publish."""
|
|
monkeypatch.setenv("ACDL_SOD_HALT_TOPIC_ARN", "arn:aws:sns:us-east-1:000000000000:acdl-sod-halt")
|
|
sns_client = mock.MagicMock()
|
|
route_halt_artifact("contract-123", "SEPARATION_OF_DUTIES_VIOLATION: x==y",
|
|
oncall_client=sns_client)
|
|
sns_client.publish.assert_called_once()
|
|
call = sns_client.publish.call_args
|
|
assert call.kwargs["TopicArn"] == "arn:aws:sns:us-east-1:000000000000:acdl-sod-halt"
|
|
assert "contract-123" in call.kwargs["Message"]
|
|
assert "SEPARATION_OF_DUTIES_VIOLATION" in call.kwargs["Message"]
|
|
assert call.kwargs["Subject"] == "ACDL SoD halt"
|
|
|
|
|
|
def test_route_halt_falls_back_to_stderr_when_arn_unset(monkeypatch, capsys):
|
|
"""Without ACDL_SOD_HALT_TOPIC_ARN, a stderr emission occurs."""
|
|
monkeypatch.delenv("ACDL_SOD_HALT_TOPIC_ARN", raising=False)
|
|
# Mock outbox_writer.write_event to avoid AWS calls.
|
|
with mock.patch("core.outbox_writer.write_event", return_value=None):
|
|
route_halt_artifact("contract-456", "violation", oncall_client=None)
|
|
captured = capsys.readouterr()
|
|
assert "contract-456" in captured.err
|
|
assert "violation" in captured.err
|
|
|
|
|
|
def test_route_halt_outbox_fallback_writes_event(monkeypatch):
|
|
"""Without the SNS ARN, the outbox fallback writes a SEPARATION_OF_DUTIES_VIOLATION event."""
|
|
monkeypatch.delenv("ACDL_SOD_HALT_TOPIC_ARN", raising=False)
|
|
with mock.patch("core.outbox_writer.write_event") as mock_write:
|
|
route_halt_artifact("contract-789", "sod violation", oncall_client=None)
|
|
mock_write.assert_called_once()
|
|
event = mock_write.call_args[0][0]
|
|
assert event["contractId"] == "contract-789"
|
|
assert event["eventType"] == "SEPARATION_OF_DUTIES_VIOLATION"
|
|
assert "sod violation" in event["reason"]
|
|
|
|
|
|
def test_route_halt_sns_failure_falls_back_to_outbox(monkeypatch):
|
|
"""If SNS publish raises, the outbox fallback is used."""
|
|
monkeypatch.setenv("ACDL_SOD_HALT_TOPIC_ARN", "arn:aws:sns:us-east-1:000000000000:acdl-sod-halt")
|
|
sns_client = mock.MagicMock()
|
|
sns_client.publish.side_effect = Exception("SNS down")
|
|
with mock.patch("core.outbox_writer.write_event") as mock_write:
|
|
route_halt_artifact("contract-fail", "violation", oncall_client=sns_client)
|
|
mock_write.assert_called_once()
|
|
|
|
|
|
def test_sns_topic_defined_in_terraform():
|
|
"""terraform/platform/main.tf defines the acdl-sod-halt SNS topic."""
|
|
tf = (ROOT / "terraform" / "platform" / "main.tf").read_text()
|
|
assert "aws_sns_topic" in tf
|
|
assert "acdl-sod-halt" in tf
|
|
assert "acdl_sod_halt_topic_arn" in tf |