Compliance as Code: Turning SOC 2 and CIS Evidence Into a Pipeline
Audit season shouldn't mean a month of screenshots. Here's how to express controls as code and generate continuous, queryable compliance evidence for SOC 2 and CIS automatically.
- #security
- #hardening
- #compliance
- #soc2
- #cis
- #policy-as-code
Every engineer who’s been through a SOC 2 audit knows the ritual. Three weeks before the deadline, someone starts a spreadsheet, and the team spends a month taking screenshots: here’s the MFA setting, here’s the encryption config, here’s a screenshot proving the backup ran. The evidence is stale the moment it’s captured, the process is soul-crushing, and the result proves you were compliant on the day you took the screenshot and says nothing about the other 364 days.
Compliance as code fixes the model. Instead of periodically proving a control by hand, you express the control as code, enforce it continuously, and let the system generate evidence as a byproduct. Audit season stops being a sprint and becomes a query.
The shift: controls become testable statements
A compliance control like “production databases must be encrypted at rest” is, underneath the auditor language, just an assertion about your infrastructure. And assertions about infrastructure are things code can check. The move is to translate every control into one or more automated checks that run continuously and emit a pass/fail record.
That record — timestamped, attributed, stored — is your evidence. Not a screenshot, but a continuous log proving the control held every day, queryable on demand. When the auditor asks “show me encryption was enforced for the audit period,” you run a query instead of digging through a folder.
Policy as code at the gate
The first place to enforce controls is before infrastructure ever ships. Open Policy Agent (OPA) and its Rego language let you encode controls as policy that runs in CI:
# CIS-style control: no security group open to the world on SSH
package compliance.network
deny[msg] {
sg := input.resource.aws_security_group[name]
rule := sg.ingress[_]
rule.from_port <= 22
rule.to_port >= 22
rule.cidr_blocks[_] == "0.0.0.0/0"
msg := sprintf("SG %s exposes SSH to the internet (violates CIS 5.2)", [name])
}
Run that against your Terraform plan in the pipeline and a non-compliant change can’t merge. The control isn’t a thing you audit after the fact — it’s a thing the pipeline refuses to violate. Every blocked PR is also a piece of evidence that the control is active.
Continuous scanning of the live environment
Gating new changes isn’t enough; you also have to prove the running environment stays compliant, because things drift. Scanners that map directly to benchmarks do this. For CIS Kubernetes, kube-bench:
kube-bench run --targets master,node --json > evidence/kube-bench-$(date +%F).json
For cloud accounts against multiple frameworks at once, Prowler maps findings straight to SOC 2, CIS, and other standards:
prowler aws --compliance cis_2.0_aws soc2_aws \
--output-formats json-ocsf --output-directory evidence/
Schedule these daily and write the output to immutable, timestamped storage. Now you have a continuous evidence trail — one file per day proving the state of every control — instead of a single audit-day snapshot.
Map evidence to controls, not the other way around
The detail that makes this actually usable at audit time is tagging each check with the control it satisfies. Bury the framework reference in the check itself:
checks:
- id: encryption-at-rest
controls: [SOC2-CC6.1, CIS-AWS-2.1.1]
query: "all RDS instances have StorageEncrypted=true"
- id: mfa-enforced
controls: [SOC2-CC6.6, CIS-AWS-1.10]
query: "all IAM users with console access have MFA"
When the auditor works control-by-control, you filter your evidence store by control ID and hand over a complete, dated history for exactly that requirement. No scramble, no spreadsheet — a query keyed on SOC2-CC6.1 returns every check, every day, that proves it.
The human controls still need a system
Not every SOC 2 control is technical. Access reviews, change-management approvals, and onboarding/offboarding are process controls — but they still produce evidence you can automate the collection of:
- Change management. Your PR history, with required reviews and CI gates, is the change-management evidence. Branch protection rules enforcing review are a control; the merge log is the proof.
- Access reviews. A scheduled job that exports current IAM/role assignments to dated storage turns the quarterly access review into a diff between two snapshots instead of a manual audit.
- Offboarding. A check that cross-references your HR system against active credentials catches the departed employee whose access lingered — a classic audit finding.
The pattern is the same throughout: the system of record already holds the truth; you just automate extracting it into dated, queryable evidence.
Build the dashboard you’ll actually trust
Pull all of it — gate results, scanner output, access snapshots — into one compliance posture view. The goal is that at any moment you can answer “are we compliant right now?” without a project. A simple roll-up works:
# Daily posture summary across all controls
jq -s '[.[] | {control: .controls[], status: .status}]
| group_by(.control)
| map({control: .[0].control,
passing: (map(select(.status=="PASS")) | length),
failing: (map(select(.status=="FAIL")) | length)})' \
evidence/*.json
A control that flips to failing should page like any other reliability regression — because a drifted control is a real exposure, not just an audit risk.
Start with one control, end to end
Don’t try to encode an entire framework at once; you’ll burn out. Pick one painful control — encryption at rest, or no-public-SSH — and take it all the way: a CI gate that blocks violations, a daily scan that proves the live state, evidence written to dated storage, tagged with its SOC 2 and CIS IDs. Once one control works end to end, the rest are copies of the same pattern. Reviewing new policy rules through automated code review keeps a too-loose Rego check from passing things it shouldn’t, and the broader security hardening guides cover the underlying controls these checks verify.
The payoff is real and it compounds: the month of screenshots disappears, your evidence is always current, and “are we compliant?” becomes a dashboard instead of a fire drill.
Policy rules and compliance checks are starting points mapped to common framework versions. Confirm control mappings with your auditor and validate checks against your own environment before relying on them.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.