Compliance Evidence and Control Mapping

An auditor does not accept “we run backup validation nightly” as proof that a contingency control is satisfied; they demand a dated, tamper-evident record that a specific control was exercised, by which pipeline, against which artifact, with what verdict. Within Recovery Validation Telemetry & Compliance, this stage is the translation layer that turns machine telemetry into that record — it consumes the structured output emitted by your validation telemetry and metrics layer and by every checksum validation pipeline run, then binds each outcome to the control it satisfies. The operational gap is precise: teams generate abundant drill telemetry and abundant control catalogs, but nothing deterministically joins the two, so at audit time an engineer manually reconstructs which drill proved which control — a slow, error-prone exercise that produces exactly the kind of evidence auditors distrust.

Control mapping closes that gap by treating evidence as a first-class data structure rather than a report generated on demand. Each drill or validation run emits an evidence record; each record is bound to one or more control identifiers drawn from NIST SP 800-34 Rev. 1, the SOC 2 Availability criteria, or ISO 22301 continuity clauses; and the binding is checked for completeness against a required-control set scoped to an audit window. A control with no passing evidence in that window is a coverage gap, and a coverage gap is an audit finding waiting to happen. Because a “passing” drill is only meaningful when its recovery time landed inside the envelope your RTO and RPO mapping defines, the verdict carried on each evidence record is the same verdict the recovery-envelope check produced — evidence and telemetry are two views of one fact, never two independent assertions.

Architecture and Execution Workflow

Six-stage compliance evidence and control-mapping flow A vertical pipeline: load the control map, collect drill telemetry, bind evidence to controls, detect coverage gaps, sign and persist the evidence, then export an auditor-facing report. Load control map Collect drill telemetry Bind evidence to controls Detect coverage gaps Sign + persist evidence Export auditor report

Figure. Evidence flows from a loaded control map and raw drill telemetry through control binding and gap detection into a signed, persisted store and an auditor-facing export.

The workflow is deliberately linear and idempotent: re-running it over the same telemetry window produces the same evidence records, the same coverage verdict, and the same content hashes. That determinism is what lets an auditor re-execute the export months later and confirm the record was never altered. Each stage below owns one transformation, and the interfaces between them are plain data structures — an evidence record, a control map, a coverage report — so any stage can be swapped (a different telemetry source, a different signing backend) without disturbing the others.

Loading the Control Map

The control map is the authoritative join between abstract control language and concrete pipeline components. It is declarative configuration, versioned in the same repository as the drills it describes, and it names — for every control the organization claims to satisfy through automated recovery testing — which component produces the evidence, what artifact type counts as proof, and how fresh that proof must be. Keeping it out of code means a compliance owner can amend a mapping during an audit cycle without a deployment, and the version identifier stamped on every export ties the evidence back to the exact mapping in force when it was generated.

python
#!/usr/bin/env python3
"""Load and validate a control map that binds compliance controls to pipeline components."""
from __future__ import annotations

from dataclasses import dataclass, field


@dataclass(frozen=True)
class ControlSpec:
    """One control the organization proves through automated recovery testing."""

    control_id: str          # e.g. "CP-9", "A1.2", "8.4"
    framework: str           # "NIST-800-34", "SOC2", "ISO-22301"
    component: str           # pipeline component expected to produce evidence
    artifact_type: str       # "checksum-report", "restore-drill", "replication-check"
    max_age_days: int        # evidence older than this no longer counts as coverage
    required: bool = True     # a required control with no evidence is a hard gap


def load_control_map(raw: list[dict]) -> dict[str, ControlSpec]:
    """Build a control_id -> ControlSpec index from parsed configuration."""
    index: dict[str, ControlSpec] = {}
    for entry in raw:
        spec = ControlSpec(
            control_id=entry["control_id"],
            framework=entry["framework"],
            component=entry["component"],
            artifact_type=entry["artifact_type"],
            max_age_days=int(entry["max_age_days"]),
            required=bool(entry.get("required", True)),
        )
        if spec.control_id in index:
            raise ValueError(f"duplicate control_id {spec.control_id!r} in control map")
        index[spec.control_id] = spec
    return index

Collecting Drill Telemetry

The collection stage reads the structured telemetry already emitted by validation runs and DR drills and normalizes it into evidence records. It does not re-run any validation — that would defeat the point of an audit trail, which must reflect what actually happened, not what happens when the auditor is watching. The canonical evidence record carries the fields an auditor asks for in every framework: which control it speaks to, which component produced it, a pointer to the underlying artifact, the drill or run identifier, a UTC timestamp, the recorded verdict, and a content hash of the source telemetry so the record cannot silently drift from the fact it represents.

python
#!/usr/bin/env python3
"""Normalize raw drill telemetry into immutable, hashable evidence records."""
from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass, asdict
from datetime import datetime, timezone


@dataclass(frozen=True)
class EvidenceRecord:
    """A single audit fact: a control exercised by a component with a verdict."""

    control_id: str
    component: str
    artifact: str            # URI or path to the underlying proof artifact
    drill_id: str
    timestamp: str           # ISO-8601 UTC
    verdict: str             # "pass", "fail", "error"
    source_hash: str         # hash of the raw telemetry this record summarizes


def _hash_payload(payload: dict) -> str:
    canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()


def to_evidence(telemetry: dict, control_id: str) -> EvidenceRecord:
    """Project one telemetry event onto the audit control it satisfies."""
    ts = telemetry.get("completed_at") or datetime.now(timezone.utc).isoformat()
    return EvidenceRecord(
        control_id=control_id,
        component=telemetry["component"],
        artifact=telemetry["artifact_uri"],
        drill_id=telemetry["drill_id"],
        timestamp=ts,
        verdict=telemetry["verdict"],
        source_hash=_hash_payload(telemetry),
    )


if __name__ == "__main__":
    sample = {
        "component": "checksum-validator",
        "artifact_uri": "s3://dr-evidence/2026-07-18/base.sha256",
        "drill_id": "drill-20260718-01",
        "verdict": "pass",
        "completed_at": "2026-07-18T02:14:07+00:00",
    }
    print(json.dumps(asdict(to_evidence(sample, "CP-9")), indent=2))

Binding Evidence to Controls and Detecting Gaps

Binding is where telemetry stops being observability data and becomes compliance evidence. The coverage checker walks the control map, and for each required control asks a single question: is there at least one pass evidence record for this control’s component and artifact type, dated inside the audit window and no older than the control’s freshness limit? A control that answers yes is covered; a required control that answers no is a gap. Freshness matters as much as existence — a CP-9 backup control proven by a checksum run from eight months ago is not evidence that backups work today, so the checker enforces max_age_days per control rather than a single global window.

python
#!/usr/bin/env python3
"""Detect coverage gaps by matching evidence records against a control map."""
from __future__ import annotations

from datetime import datetime, timedelta, timezone


def _parse(ts: str) -> datetime:
    return datetime.fromisoformat(ts).astimezone(timezone.utc)


def find_gaps(control_map: dict, evidence: list, window_days: int, now: datetime) -> list[str]:
    """Return control_ids that are required but lack fresh passing evidence."""
    window_start = now - timedelta(days=window_days)
    gaps: list[str] = []
    for control_id, spec in control_map.items():
        if not spec.required:
            continue
        freshness_floor = now - timedelta(days=spec.max_age_days)
        covered = any(
            rec.control_id == control_id
            and rec.verdict == "pass"
            and rec.component == spec.component
            and _parse(rec.timestamp) >= max(window_start, freshness_floor)
            for rec in evidence
        )
        if not covered:
            gaps.append(control_id)
    return gaps

Because find_gaps returns a concrete list of failing control identifiers rather than a boolean, the caller can distinguish a fully covered audit window (empty list, promote) from a partial one (name the gaps, quarantine the export) and drive an exit code off the difference — the same 0/1/2 contract every gate in this reference honors.

Integration with DR Drill Orchestration

Control mapping is a downstream consumer, not an originator: it never triggers a drill and never re-hashes an artifact. It subscribes to the outcomes that drill scheduling and orchestration already produces, and it treats each artifact pointer as a reference into the immutable audit trails store where the raw proof is retained. That separation keeps the evidence layer honest — it can only report what the orchestrator actually ran. When a gap is detected, the natural remediation is to schedule an out-of-cycle drill for the uncovered control, so the coverage report becomes an input back into scheduling: the set of controls with no fresh passing evidence is precisely the set of drills the next cycle must prioritize.

The binding stage also depends on how upstream failures are classified. A fail verdict and an error verdict must not be collapsed, because a failed restore is negative evidence (the control was exercised and did not hold) while an errored run is no evidence (the control was never truly tested). Routing those through your error categorization frameworks before they reach the coverage checker keeps a flaky harness from masquerading as a covered control.

Error Classification and Thresholds

Evidence completeness is itself graded. Not every missing record is an audit-blocking emergency, and treating them uniformly produces the same alert fatigue that erodes trust in the telemetry. The tiers below drive whether an export is blocked, annotated, or shipped clean.

Tier Condition Tolerance Export action
BLOCKING Required control has zero fresh passing evidence in the window Zero Fail export (exit 1), name the control, notify compliance owner
DEGRADED Required control covered only by evidence past max_age_days but inside the window Bounded Ship export with a staleness annotation; schedule a refresh drill
ADVISORY Non-required control uncovered, or a fail verdict present alongside a later pass Unbounded Record in the export appendix; no gate

The distinction between BLOCKING and DEGRADED is a policy decision expressed in configuration, not in the checking code — the checker reports the raw coverage facts and the age of the newest passing record, and the tier mapping is applied afterward so freshness thresholds can be tuned per framework without touching the join logic.

Telemetry and Compliance Output

The evidence layer emits its own metrics so that coverage itself is observable, not just the drills underneath it. These export cleanly to a Prometheus-compatible endpoint and let a compliance dashboard show, at any moment, how many controls would pass an audit today.

Metric Type Purpose
compliance_controls_covered_total Gauge Required controls with fresh passing evidence, by framework
compliance_controls_gap_total Gauge Required controls with no fresh passing evidence
compliance_evidence_age_seconds Gauge Age of the newest passing record per control, for staleness alerting
compliance_export_signed_total Counter Signed auditor bundles produced, for chain-of-custody accounting

Each exported bundle carries the control-map version, the audit window bounds, the per-control coverage verdict, and a content hash over the ordered evidence set, so the export aligns with the evidence expectations of NIST SP 800-34 Rev. 1, the SOC 2 Availability criteria, and ISO 22301. The two child guides below implement the framework-specific projections: one for the NIST contingency-planning family and one for the SOC 2 and ISO continuity criteria.

Operational Best Practices

  • Version the control map like code. Stamp its version identifier on every export so evidence is always traceable to the exact mapping in force when it was generated.
  • Hash the source telemetry, not just the record. A record whose source_hash no longer matches its underlying artifact has drifted and must be treated as unverified.
  • Enforce freshness per control, never globally. A backup control and a replication control tolerate very different evidence ages; a single window hides stale coverage.
  • Keep fail and error verdicts distinct through the whole pipeline. Negative evidence and absent evidence lead to opposite remediations.
  • Sign the bundle, not the individual records. A single signature over the ordered, hashed evidence set gives an auditor one thing to verify and detects reordering as readily as tampering.
  • Feed gaps back into scheduling. The set of uncovered controls is the highest-priority drill backlog; closing the loop is what keeps coverage from decaying between audits.

Frequently Asked Questions

Why store evidence as records instead of generating an audit report on demand?

An on-demand report re-derives its conclusions when the auditor asks, which means it reflects the state of the system at report time, not at the time the control was actually exercised. Persisting an immutable EvidenceRecord per run — each with a UTC timestamp and a content hash of its source telemetry — captures the fact when it happened, so the record can be re-verified later and cannot silently change if a pipeline is reconfigured.

What makes a control count as covered?

A required control is covered when at least one evidence record exists with a pass verdict, produced by the component named in the control map, dated inside the audit window, and no older than that control's max_age_days freshness limit. Existence alone is not enough — an ancient passing record proves the control worked once, not that it works within the window under review.

How does this layer relate to the raw drill telemetry?

It is a read-only consumer. The evidence layer never triggers a drill or recomputes a checksum; it normalizes telemetry that orchestration already produced and binds each outcome to the control it satisfies. That constraint is deliberate — an audit trail must reflect what the system actually did, so re-running validation at export time would undermine the evidence rather than strengthen it.

This guide is one component of the broader Recovery Validation Telemetry & Compliance area.