phase: 7, status: plan-as-execute, persona: security-engineer, task: T-7.4..T-7.8
---ci--- project: acdl phase: 7 milestone: v1.1 status: plan-as-execute persona: security-engineer task: [T-7.4, T-7.5, T-7.6, T-7.7, T-7.8] requirements.covered: [REQ-18, REQ-20, REQ-21] ---/ci--- Wave 3 (security-engineer, 5 files sequential): - T-7.4: schemas/policy_check_result.schema.json (REQ-18 schema half) — canonical shape from ARCHITECTURE.md §12.6; engine enum [checkov,kyverno,opa]; severity enum [critical,high,medium,low,info]; result enum [pass,fail,skipped,error]. Validates as Draft 2020-12; valid instance validates. - T-7.5: adapters/terraform/policy/checkov_adapter.py (REQ-18 adapter half) — Checkov JSON -> PolicyCheckResult; RULE_MAP has all 11 Checkov rule IDs (CKV_AWS_41/45/46/20/57/24/25/1/40/7/33) mapped to the 4 L2 checks + tag/naming; emits ACDL_TAG_NAMING SKIPPED per D-043; stdlib only; tolerates both Checkov JSON shapes. Synthetic fixture produces 3 records all valid against the schema. - T-7.6: platform/audit_ledger_design.md (REQ-20) — three tiers (S3 Object Lock compliance 7yr, acdl-evidence hot index, DynamoDB outbox RPO=0); spike scope (D-041) = hash chain + outbox write; v1.2 build-out = Object Lock + JWS (KMS key, quarterly rotation) + async worker + DLQ + daily checkpoints. Outbox item shape, RPO/RTO table, decision trail. - T-7.7: platform/hitl_matrix_design.md (REQ-21 design half) — pre-execution gate model; Gitea-specific mechanics (workflow_dispatch + gitea.actor per D-042, no Environments API); full 8-concern matrix verbatim from §10.4; timeout 1d warn / 2d freeze; rejection -> HELD + supersedes; CODEOWNERS routing; SoD pointer to the .py. - T-7.8: platform/separation_of_duties.py (REQ-21 impl half) — check(outbox_client, contract_id, current_prod_approver) -> (ok, reason); None outbox -> no-op; equal -> SEPARATION_OF_DUTIES_VIOLATION; distinct -> ok; route_halt_artifact stub; stdlib only (duck-typed outbox_client). All 5 SoD cases verified.
This commit is contained in:
@@ -0,0 +1,99 @@
|
|||||||
|
"""Translate Checkov JSON output to ACDL PolicyCheckResult records.
|
||||||
|
|
||||||
|
Reads Checkov's JSON output (one framework key, e.g. terraform_plan),
|
||||||
|
emits a list of PolicyCheckResult dicts conforming to
|
||||||
|
schemas/policy_check_result.schema.json. Run Checkov with --soft-fail so
|
||||||
|
Checkov never exits non-zero; the confidence signal decides the gate, not
|
||||||
|
Checkov's exit code.
|
||||||
|
|
||||||
|
Spike scope (D-043): tag/naming is a single SKIPPED record. A custom
|
||||||
|
Checkov YAML rule for tag presence lands in v1.2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
RULE_MAP = {
|
||||||
|
"CKV_AWS_41": ("secrets-in-plaintext", "high"),
|
||||||
|
"CKV_AWS_45": ("secrets-in-plaintext", "high"),
|
||||||
|
"CKV_AWS_46": ("secrets-in-plaintext", "high"),
|
||||||
|
"CKV_AWS_20": ("public-ingress", "high"),
|
||||||
|
"CKV_AWS_57": ("public-ingress", "high"),
|
||||||
|
"CKV_AWS_24": ("public-ingress", "medium"),
|
||||||
|
"CKV_AWS_25": ("public-ingress", "medium"),
|
||||||
|
"CKV_AWS_1": ("iam-wildcard", "high"),
|
||||||
|
"CKV_AWS_40": ("iam-wildcard", "medium"),
|
||||||
|
"CKV_AWS_7": ("kms-key-reference", "medium"),
|
||||||
|
"CKV_AWS_33": ("kms-key-reference", "medium"),
|
||||||
|
}
|
||||||
|
|
||||||
|
_RESULT_MAP = {"PASSED": "pass", "FAILED": "fail", "SKIPPED": "skipped"}
|
||||||
|
|
||||||
|
|
||||||
|
def _iso8601_now():
|
||||||
|
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
|
||||||
|
def _to_pcr(checkov_record, contract_id, result_str):
|
||||||
|
check_id = checkov_record.get("check_id", "")
|
||||||
|
default_sev = RULE_MAP.get(check_id, (check_id, "info"))[1]
|
||||||
|
severity = checkov_record.get("severity", default_sev)
|
||||||
|
if isinstance(severity, str):
|
||||||
|
severity = severity.lower()
|
||||||
|
return {
|
||||||
|
"contractId": contract_id,
|
||||||
|
"evaluatedAt": _iso8601_now(),
|
||||||
|
"engine": "checkov",
|
||||||
|
"ruleId": check_id,
|
||||||
|
"severity": severity,
|
||||||
|
"result": _RESULT_MAP.get(result_str, "error"),
|
||||||
|
"message": checkov_record.get("check_name", ""),
|
||||||
|
"evidence": {
|
||||||
|
"file_path": checkov_record.get("file_path"),
|
||||||
|
"resource": checkov_record.get("resource"),
|
||||||
|
"resource_address": checkov_record.get("resource_address"),
|
||||||
|
"code_block": checkov_record.get("code_block"),
|
||||||
|
},
|
||||||
|
"resourceRef": checkov_record.get("resource_address") or checkov_record.get("resource", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _emit_tag_naming_skipped(contract_id):
|
||||||
|
return {
|
||||||
|
"contractId": contract_id,
|
||||||
|
"evaluatedAt": _iso8601_now(),
|
||||||
|
"engine": "checkov",
|
||||||
|
"ruleId": "ACDL_TAG_NAMING",
|
||||||
|
"severity": "info",
|
||||||
|
"result": "skipped",
|
||||||
|
"message": "tag/naming check deferred to v1.2 (D-043)",
|
||||||
|
"evidence": {},
|
||||||
|
"resourceRef": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def adapt(checkov_json_path, contract_id):
|
||||||
|
with open(checkov_json_path, "r", encoding="utf-8") as fh:
|
||||||
|
data = json.load(fh)
|
||||||
|
out = []
|
||||||
|
for framework, body in data.items():
|
||||||
|
results = body.get("results", body) if isinstance(body, dict) else {}
|
||||||
|
if not isinstance(results, dict):
|
||||||
|
continue
|
||||||
|
for rec in results.get("passed_checks", []):
|
||||||
|
out.append(_to_pcr(rec, contract_id, "PASSED"))
|
||||||
|
for rec in results.get("failed_checks", []):
|
||||||
|
out.append(_to_pcr(rec, contract_id, "FAILED"))
|
||||||
|
for rec in results.get("skipped_checks", []):
|
||||||
|
out.append(_to_pcr(rec, contract_id, "SKIPPED"))
|
||||||
|
out.append(_emit_tag_naming_skipped(contract_id))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("usage: checkov_adapter.py <checkov.json> <contract-id>", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
print(json.dumps(adapt(sys.argv[1], sys.argv[2]), indent=2))
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# ACDL Tiered Audit Ledger Design (REQ-20)
|
||||||
|
|
||||||
|
> **Status:** design authored in Phase 07 (milestone v1.1); the spike
|
||||||
|
> (Phases 08-10) implements the **v1.0 hash chain + DynamoDB outbox write**
|
||||||
|
> (D-041); the v1.2 build-out implements S3 Object Lock + JWS + async
|
||||||
|
> worker + DLQ + daily checkpoints.
|
||||||
|
|
||||||
|
The audit stream is the platform's tamper-evident record of every delivery
|
||||||
|
action. The vision's "Audit truth lives outside the repository" bet [1]
|
||||||
|
and "Not a mutable audit log" anti-goal [1] are the binding constraints.
|
||||||
|
Version-control history does not satisfy regulatory evidence; the ledger
|
||||||
|
is the source of truth.
|
||||||
|
|
||||||
|
## Three tiers
|
||||||
|
|
||||||
|
- **Cold tier (source of truth):** S3 with **Object Lock in compliance
|
||||||
|
mode**, **7-year retention** (ARCHITECTURE.md §9). No one — including
|
||||||
|
root — can delete or overwrite until retention expires. The regulatory
|
||||||
|
record.
|
||||||
|
- **Hot tier (query index):** the `acdl-evidence` audit repo (unchanged
|
||||||
|
from the v1.0 demo). Not part of the chain; a queryable mirror the
|
||||||
|
evidence UI (`evidence-ui/index.html`) reads. Lightweight attestation
|
||||||
|
linkage lives in the repo; the regulatory event body lives in S3.
|
||||||
|
- **Outbox (write path):** DynamoDB, **RPO = 0** (synchronous write before
|
||||||
|
contract submission ack). Single-region in v1 (`us-east-1`).
|
||||||
|
|
||||||
|
## Spike scope (D-041) — what Phases 08-10 implement
|
||||||
|
|
||||||
|
- **DynamoDB outbox:** table `acdl-outbox`, `PAY_PER_REQUEST` (D-044),
|
||||||
|
PK `contractId`, SK `eventType#eventTs`, TTL `expire_at` = now + 365d
|
||||||
|
(1-year storage per ARCHITECTURE.md §8).
|
||||||
|
- **`prev_event_hash` chain:** SHA-256 over canonical JSON
|
||||||
|
(`json.dumps(event, sort_keys=True, separators=(",", ":"))`), lifted
|
||||||
|
from the v1.0 demo's `evidence_writer.py`. Auto-genesis: first event
|
||||||
|
has `prev_hash="GENESIS"`.
|
||||||
|
- **Synchronous write** via boto3 `put_item` (strong-consistent by
|
||||||
|
default). No separate async worker / DLQ in the spike (RTO = workflow
|
||||||
|
re-run).
|
||||||
|
- **Mirror to `acdl-evidence`:** unchanged from v1.0 — the finalize step
|
||||||
|
commits `audit.json` to the evidence repo (the hot tier).
|
||||||
|
- **Spike evidence event shape:**
|
||||||
|
`{seq, ts, stage, event, prev_hash, hash, contractId, environment, stack, score, band}`.
|
||||||
|
|
||||||
|
## v1.2 build-out — what Phase 07 designs but the spike defers
|
||||||
|
|
||||||
|
- **S3 Object Lock:** bucket `acdl-evidence-lock-<account-id>`, Object
|
||||||
|
Lock enabled at creation, compliance mode, 7-yr retention
|
||||||
|
(`RetainUntilDate` = now + 7y). The outbox→S3 path is an async worker
|
||||||
|
that reads from the outbox and writes to Object Lock.
|
||||||
|
- **JWS detached signature (RFC 7515):** the event payload is
|
||||||
|
canonical-JSON-serialized, SHA-256 hashed, signed with a private key;
|
||||||
|
the signature is stored *detached* alongside the payload. Signing key =
|
||||||
|
**platform-level KMS key** (not per-contract — a per-contract key would
|
||||||
|
explode the key-management surface), rotated **quarterly**. The `jws`
|
||||||
|
field is added to the event shape in v1.2.
|
||||||
|
- **Async worker + DLQ:** a Lambda (or a Gitea Actions scheduled workflow)
|
||||||
|
reads the outbox, writes to S3 Object Lock, signs with KMS. DLQ = an
|
||||||
|
SQS dead-letter queue for failed writes. RTO = DLQ replay.
|
||||||
|
- **Daily checkpoints (§9):** a daily job reads the last event hash and
|
||||||
|
writes a "checkpoint" event to the ledger (+ optionally to a public
|
||||||
|
notarization service). The spike runs in minutes, not days — no
|
||||||
|
checkpoint in spike.
|
||||||
|
|
||||||
|
## JWS vs chain — orthogonality note
|
||||||
|
|
||||||
|
The `prev_event_hash` chain gives ordering/tamper-evidence *within* the
|
||||||
|
log (a deleted event breaks the chain visibly); JWS gives authenticity
|
||||||
|
*per event* (a forged event is detectable without re-reading the whole
|
||||||
|
chain). The chain is spike-scope; JWS is v1.2. Together they cover both
|
||||||
|
integrity properties the vision's "Not a mutable audit log" anti-goal
|
||||||
|
requires.
|
||||||
|
|
||||||
|
## Outbox item shape (full, spike + v1.2)
|
||||||
|
|
||||||
|
- PK `contractId` (UUID).
|
||||||
|
- SK `eventType#eventTs` (e.g. `POLICY_CHECKED#2026-07-21T12:00:00Z`).
|
||||||
|
- `payload` (the event body — hash-chained in spike, JWS-signed in v1.2).
|
||||||
|
- `prev_event_hash` (chain link; `GENESIS` for the first event).
|
||||||
|
- `hash` (this event's SHA-256 over canonical JSON).
|
||||||
|
- `approver_qa` (Gitea username of the QA approver; empty in dev-only
|
||||||
|
spike; populated on qa-promotion — D-042).
|
||||||
|
- `approver_prod` (SRE username; empty in spike).
|
||||||
|
- `environment`, `stack`, `score`, `band`.
|
||||||
|
- `expire_at` (TTL = now + 365d).
|
||||||
|
- **v1.2 only:** `jws` (detached signature), `checkpoint_ref`.
|
||||||
|
|
||||||
|
## RPO / RTO table
|
||||||
|
|
||||||
|
| Phase | RPO | RTO |
|
||||||
|
|-------|-----|-----|
|
||||||
|
| Spike (D-041) | 0 (sync outbox write) | workflow re-run |
|
||||||
|
| v1.2 | 0 (sync outbox) | async worker DLQ replay |
|
||||||
|
|
||||||
|
## Decision trail
|
||||||
|
|
||||||
|
- **D-041** — spike scope = hash chain + outbox write; Object Lock + JWS
|
||||||
|
+ worker + DLQ are v1.2.
|
||||||
|
- **D-044** — outbox mode `PAY_PER_REQUEST`; PK/SK; TTL `expire_at` =
|
||||||
|
now + 365d; no separate async worker in spike.
|
||||||
|
- **D-042** — approver identities (`approver_qa`, `approver_prod`) live
|
||||||
|
in the outbox; the separation-of-duties check
|
||||||
|
(`platform/separation_of_duties.py`) reads `approver_qa` and compares
|
||||||
|
to the prod-dispatch `gitea.actor`.
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# ACDL Human-in-the-Loop Matrix + Separation-of-Duties Design (REQ-21)
|
||||||
|
|
||||||
|
> **Status:** design authored in Phase 07 (milestone v1.1); v1.2 wires the
|
||||||
|
> gates. The spike (Phases 08-10) is **dev-only**; HITL is not exercised
|
||||||
|
> (the spike contract has `environment: dev`).
|
||||||
|
|
||||||
|
The vision's "Lower Environments are Autonomous; Higher Environments are
|
||||||
|
Attested" tenet [1] and the "deliberate human attestation — not as a
|
||||||
|
rubber stamp" requirement [1] are the binding constraints.
|
||||||
|
|
||||||
|
## Gate model (ARCHITECTURE.md §10.1)
|
||||||
|
|
||||||
|
**Pre-execution gates.** The contract is held in a "validated but not
|
||||||
|
applied" state until the human attests. qa, prod, dr are attestation
|
||||||
|
gates. No partial deployment to roll back on rejection (qa, prod); dr is
|
||||||
|
a separate deployment against a separate cluster/region. The
|
||||||
|
canary/deployment-rollback model is explicitly not in scope for v1.
|
||||||
|
|
||||||
|
## Gitea-specific gate mechanics (D-042)
|
||||||
|
|
||||||
|
Gitea has **no Environments API** and ignores `environment:` blocks
|
||||||
|
(v1.0 D-013; re-confirmed in RESEARCH TARGET 1). The pre-execution gate
|
||||||
|
is modeled as a `workflow_dispatch` with approval inputs:
|
||||||
|
|
||||||
|
- **qa gate:** `workflow_dispatch` with `approve_qa: true`; the dispatch
|
||||||
|
run's `gitea.actor` is the QA approver.
|
||||||
|
- **prod gate:** `workflow_dispatch` with `approve_prod: true`;
|
||||||
|
`gitea.actor` is the SRE approver.
|
||||||
|
- **dr gate:** `workflow_dispatch` with `approve_dr: true`; same.
|
||||||
|
|
||||||
|
The approver identity of record = `gitea.actor` of the dispatch run
|
||||||
|
(D-042). There is no other approval-identity signal in Gitea. The v1.2
|
||||||
|
real-OIDC path (blocked on go-gitea/gitea#36988) does not change this —
|
||||||
|
OIDC authorizes the *runner* to AWS, it does not change how the platform
|
||||||
|
records the *human* approver.
|
||||||
|
|
||||||
|
## Reviewer routing (ARCHITECTURE.md §10.2)
|
||||||
|
|
||||||
|
Gitea CODEOWNERS routes the right reviewer to the right gate:
|
||||||
|
|
||||||
|
- qa → QA team
|
||||||
|
- prod → SRE team
|
||||||
|
- dr → SRE team
|
||||||
|
|
||||||
|
CODEOWNERS **routes**; it does **not** enforce identity distinctness (that
|
||||||
|
is the platform-internal outbox check in
|
||||||
|
`platform/separation_of_duties.py`).
|
||||||
|
|
||||||
|
## Full 8-concern attestation matrix (§10.4, lifted verbatim)
|
||||||
|
|
||||||
|
| Env | Concern | Evidence artifact | Freshness | Source | Attester |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| qa | Functional correctness | Last successful run of contract-declared validation.e2eSuite with pass rate ≥ 99% | Last 24h | Test runner declared in contract | QA |
|
||||||
|
| qa | Performance baseline | Load test report (k6 / Gatling / Locust) showing p99 latency < declared NFR and throughput > declared minimum | Last 7d | Load test runner declared in contract | QA |
|
||||||
|
| qa | Security posture | Vulnerability scan (Trivy, Snyk, or contract-declared equivalent) with no criticals/highs, signed by Security on-call | Last 24h | Security scanner + Security team signature | QA |
|
||||||
|
| qa | Contract NFRs | Platform-generated report: schema valid, NFR assertions (latency, throughput, error rate) within declared bounds | At submission | Platform contract validator | QA |
|
||||||
|
| prod | Operational readiness | Runbook published, dashboard exists, on-call rotation assigned, alerts configured | At submission, validated against last 30d history | Platform + SRE | SRE |
|
||||||
|
| prod | Incident response | Sev-1 runbook tabletop or live drill completed | Last 90d | SRE drill record | SRE |
|
||||||
|
| prod | Capacity / cost | FinOps forecast for next 30d within budget envelope, cost anomaly baseline stored, budget alert configured | Forecast valid for next 30d | FinOps + SRE | SRE |
|
||||||
|
| prod | Resilience | DR drill, chaos engineering report, backup verified | DR: 180d; chaos: 90d; backup: 30d | SRE + Platform | SRE |
|
||||||
|
| dr | dr-region deploy with the most recent prod-bound dr drill as canary evidence | dr drill report | Last 180d | SRE | SRE |
|
||||||
|
|
||||||
|
## Timeout behavior (§10.5)
|
||||||
|
|
||||||
|
| Time | State | Action |
|
||||||
|
|---|---|---|
|
||||||
|
| Submission | PENDING_ATTESTATION | Notify responsible team |
|
||||||
|
| 1 business day | PENDING_ATTESTATION_WARNING | Notify team + platform on-call (elevated path); emit `PENDING_ATTESTATION_TIMEOUT_WARNING` event |
|
||||||
|
| 2 business days | PENDING_ATTESTATION_AUTO_FREEZE | Auto-freeze; require re-submission; emit `PENDING_ATTESTATION_AUTO_FREEZE` event; new submission linked via `supersedes` |
|
||||||
|
|
||||||
|
**Implementation:** a Gitea `on: schedule` workflow (runs hourly) that
|
||||||
|
scans the DynamoDB outbox for `PENDING_ATTESTATION` events with `ts`
|
||||||
|
older than 1/2 business days and emits the warn/freeze events. Not
|
||||||
|
implemented in the spike (dev-only).
|
||||||
|
|
||||||
|
## Rejection and rollback (§10.6)
|
||||||
|
|
||||||
|
Rejection returns the contract to a `HELD` state with the rejection
|
||||||
|
reason captured as a `PROMOTION_REJECTED` event. The consumer fixes the
|
||||||
|
cause and re-submits; the new submission is linked to the rejected one
|
||||||
|
via `supersedes` (a contract-schema field — `schemas/contract.schema.json`).
|
||||||
|
The audit chain is **extended, not torn up** (the "Not a mutable audit
|
||||||
|
log" anti-goal). No partial deployment to roll back at any v1 gate.
|
||||||
|
|
||||||
|
## Separation of duties (§10.3) — pointer to the .py
|
||||||
|
|
||||||
|
The identity-distinctness check is platform-internal, not GitHub-native,
|
||||||
|
not Kyverno (in v1). Sequence:
|
||||||
|
|
||||||
|
1. On promotion dev → qa, the platform reads the QA approver's identity
|
||||||
|
from the `workflow_dispatch` run's `gitea.actor` and writes it to the
|
||||||
|
DynamoDB outbox keyed by `contractId` (attribute `approver_qa`).
|
||||||
|
2. On promotion qa → prod, the platform reads the stored `approver_qa`
|
||||||
|
from the outbox and the new SRE approver's `gitea.actor` from the
|
||||||
|
prod-dispatch run.
|
||||||
|
3. If `approver_qa == approver_prod`, the platform blocks the prod
|
||||||
|
promotion, writes a `SEPARATION_OF_DUTIES_VIOLATION` event to the
|
||||||
|
evidence stream, and routes a halt artifact to the SRE on-call.
|
||||||
|
4. The check is implemented in `platform/separation_of_duties.py`
|
||||||
|
(T-7.8). The platform is the only writer to the outbox; the check is
|
||||||
|
in the same process that has authority to block the promotion.
|
||||||
|
|
||||||
|
## Spike scope note
|
||||||
|
|
||||||
|
The spike is dev-only (REQ-27 contract has `environment: dev`), so HITL
|
||||||
|
is not exercised. Phase 07 authors the design; Phase 10's
|
||||||
|
`verify_phase10.sh` does not assert HITL behavior. v1.2 wires the gates
|
||||||
|
against this design.
|
||||||
|
|
||||||
|
## Decision trail
|
||||||
|
|
||||||
|
- **D-042** — approver identity = `gitea.actor` of the `workflow_dispatch`
|
||||||
|
run; no Environments API in Gitea.
|
||||||
|
- **D-013** (v1.0) — the `workflow_dispatch` approval-input fallback,
|
||||||
|
re-used for the real platform's pre-execution gate model.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Check that qaApprover != prodApprover for a contract (ARCHITECTURE.md
|
||||||
|
§10.3, D-042). Reads `approver_qa` from the DynamoDB outbox for the
|
||||||
|
contractId, compares to the prod-dispatch `gitea.actor`. Blocks on
|
||||||
|
equality, emits `SEPARATION_OF_DUTIES_VIOLATION`, routes a halt artifact
|
||||||
|
to SRE on-call.
|
||||||
|
|
||||||
|
Spike scope (A-8.1): the spike is dev-only (REQ-27 contract has
|
||||||
|
environment: dev); HITL is not exercised. This module is authored to its
|
||||||
|
full v1.2 shape but the spike calls it with current_prod_approver=None
|
||||||
|
and a None outbox_client — the check returns (True, 'no QA approver
|
||||||
|
recorded (dev-only spike)').
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
def check(outbox_client, contract_id: str,
|
||||||
|
current_prod_approver: Optional[str]) -> Tuple[bool, str]:
|
||||||
|
"""Return (ok, reason). ok=False means block the prod promotion."""
|
||||||
|
if outbox_client is None:
|
||||||
|
return (True, "no outbox client (dev-only spike)")
|
||||||
|
item = outbox_client.get(contract_id)
|
||||||
|
if item is None:
|
||||||
|
return (True, "no prior approver (first promotion)")
|
||||||
|
qa_approver = item.get("approver_qa")
|
||||||
|
if not qa_approver:
|
||||||
|
return (True, "no QA approver recorded (dev-only spike)")
|
||||||
|
if current_prod_approver is None:
|
||||||
|
return (True, "no prod approver supplied (dev-only spike)")
|
||||||
|
if qa_approver == current_prod_approver:
|
||||||
|
return (False,
|
||||||
|
f"SEPARATION_OF_DUTIES_VIOLATION: "
|
||||||
|
f"qaApprover==prodApprover=={qa_approver}")
|
||||||
|
return (True, "distinct")
|
||||||
|
|
||||||
|
|
||||||
|
def route_halt_artifact(contract_id: str, violation_reason: str,
|
||||||
|
oncall_client) -> None:
|
||||||
|
"""Route a halt artifact to SRE on-call. Spike: stub that logs. v1.2
|
||||||
|
wires a real pager."""
|
||||||
|
print(f"[halt-artifact] contract={contract_id} reason={violation_reason} "
|
||||||
|
f"oncall={oncall_client}", flush=True)
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://acdl.cloudinit.dev/schemas/policy_check_result.schema.json",
|
||||||
|
"title": "ACDL PolicyCheckResult",
|
||||||
|
"description": "Normalized policy check result — the contract between policy engines and the confidence signal. Engine-specific adapters (checkov_adapter.py, future kyverno_adapter) translate native engine output to this shape. The confidence signal consumes a list of these as its policy input; it is engine-agnostic. The severity enum drives the severity->penalty mapping (critical hard-override, high -0.2, medium -0.05, low -0.01, info 0.0).",
|
||||||
|
"$comment": "Canonical PolicyCheckResult (ARCHITECTURE.md §12.6). The confidence signal (platform/confidence_signal.py) consumes a list of these as its policy input; it is engine-agnostic. Adapters translate native output to this shape; the signal never reads engine-specific evidence.",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["contractId", "evaluatedAt", "engine", "ruleId", "severity", "result", "message", "resourceRef"],
|
||||||
|
"properties": {
|
||||||
|
"contractId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "The contract this check was evaluated against."
|
||||||
|
},
|
||||||
|
"evaluatedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "ISO-8601 timestamp of evaluation."
|
||||||
|
},
|
||||||
|
"engine": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["checkov", "kyverno", "opa"],
|
||||||
|
"description": "Policy engine that produced this result."
|
||||||
|
},
|
||||||
|
"ruleId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Rule identifier (e.g. CKV_AWS_24, KYVERNO_NO_PRIVILEGED, ACDL_TAG_NAMING)."
|
||||||
|
},
|
||||||
|
"severity": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["critical", "high", "medium", "low", "info"],
|
||||||
|
"description": "Severity drives the confidence signal's penalty mapping (ARCHITECTURE.md §8)."
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["pass", "fail", "skipped", "error"],
|
||||||
|
"description": "Check outcome."
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Human-readable result message."
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"description": "Engine-specific payload (file_path, resource, code_block, etc.). Opaque to the confidence signal; present for audit/debug."
|
||||||
|
},
|
||||||
|
"resourceRef": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "IR-typed resource identifier (the resource this check evaluated)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user