CoursesCompliance as codeContinuous compliance

Continuous compliance

Always-on evaluation vs point-in-time.

Advanced30 min · lesson 8 of 15

On July 1 your SOC 2 auditor (SOC 2 is the security report customers ask for before they trust you with their data) sampled the acme-prod-billing-exports bucket in S3, Amazon's object storage service, saw default encryption switched on, and ticked the encryption-at-rest control green. On July 12 an engineer merged a Terraform change that dropped the encryption block to get around a cross-account replication error. For eleven days customer billing data landed in that bucket in plaintext and nothing told you. Nothing could: the only thing that had ever evaluated the control was a person, once, in the past. That is a smoke alarm somebody tests every January and unplugs on the way out. Eleven blind days is the whole argument for continuous compliance. A control you check once a year is a control you do not have for the other 364 days.

One photo of the door, or a camera pointed at it

Point-in-time assessment answers one question: were we compliant on the day somebody looked? Continuous compliance answers a harder pair: are we compliant right now, and can we prove we were compliant on every day in between? The measurement that matters is drift, the moment a resource that satisfied a control stops satisfying it, because that is where real exposure and real audit findings start. Nobody gets written up for the clean deploy on day one. Three mechanisms give you the always-on signal. AWS Config records the configuration of every resource type it supports and re-runs rules against those records; a Config rule is either change-triggered, so it fires within seconds of a resource changing, or periodic, so it re-scans on a fixed clock such as every 24 hours. AWS Security Hub is a CSPM (Cloud Security Posture Management, tooling that watches your cloud account's settings for weak spots) aggregator: it ingests Config rule results plus its own security-standard checks (CIS AWS Foundations, from the Center for Internet Security, and AWS Foundational Security Best Practices) and normalizes all of it into one finding stream written in ASFF, the AWS Security Finding Format. Prowler, an independent open-source scanner, runs a comparable catalog of checks on a schedule from CI and gives you a second opinion from a tool with no stake in the first one's answer. Each control stops being a checkbox on a form and starts behaving like a sensor that never stops reporting.

Catch the drift in the second it happens

What makes a change-triggered Config rule worth having is latency. It does not wait for a nightly sweep the way a night watchman waits for his next round; it goes off like a doorbell. When the July 12 apply removed encryption, Config wrote a new configuration item for the bucket and re-ran the managed rule s3-bucket-server-side-encryption-enabled against it straight away. You can pull that evaluation out of the CLI (command-line interface, the terminal tool you drive AWS with), and its timestamps are exactly the drift narrative an auditor asks for: when the resource changed, and how fast the control noticed.

query the live compliance state of one control
# Ask AWS Config which resources are currently failing this rule,
# and when the rule last evaluated them.
aws configservice get-compliance-details-by-config-rule \
--config-rule-name s3-bucket-server-side-encryption-enabled \
--compliance-types NON_COMPLIANT \
--query 'EvaluationResults[].{Resource:EvaluationResultIdentifier.EvaluationResultQualifier.ResourceId,Changed:EvaluationResultIdentifier.OrderingTimestamp,Evaluated:ResultRecordedTime,Status:ComplianceType}' \
--output json
expected output — drift caught in seconds, not days
[
{
"Resource": "acme-prod-billing-exports",
"Changed": "2026-07-12T02:14:07Z",
"Evaluated": "2026-07-12T02:14:18Z",
"Status": "NON_COMPLIANT"
}
]

Read the two timestamps together. Changed, which Config calls OrderingTimestamp, is when the bucket's configuration actually mutated. Evaluated, which is ResultRecordedTime, is when the control re-assessed it. Eleven seconds later, not eleven days. That single record is worth more than any screenshot: it proves the control was awake and working on July 12, and it pins the drift window to the second. This is why SOC 2 Type II, the version that certifies controls operated effectively across a whole period rather than on one sampled day, only stands up when the evidence is generated continuously. A change-triggered rule is what makes drift observable at all. A periodic-only posture would not have blinked until its next scheduled scan.

Pull hundreds of sensors onto one panel

One rule is a sensor. A real program has hundreds of controls across dozens of accounts and regions, which is a lot of sensors and no panel to read them on. Security Hub is the panel. Ask that stream what is failing right now and group the answer the way a dashboard would, by severity, account, or control, without leaving your terminal. The filters that carry the weight are ComplianceStatus (set to FAILED, so only currently-failing checks count), RecordState (set to ACTIVE, so findings Security Hub has archived drop out), and SeverityLabel. Because get-findings hands back one page at a time, a production aggregation walks every page; the one-pager below groups the current page by severity to show the shape.

aggregate the current failing findings by severity
aws securityhub get-findings \
--filters '{"ComplianceStatus":[{"Value":"FAILED","Comparison":"EQUALS"}],
"RecordState":[{"Value":"ACTIVE","Comparison":"EQUALS"}]}' \
--max-results 100 \
--query 'Findings[].Severity.Label' --output json \
| jq -r 'group_by(.) | map({sev: .[0], count: length})
| sort_by(-.count)[] | "\(.sev)\t\(.count)"'
expected output — posture as a number you can trend
MEDIUM 31
HIGH 9
LOW 7
CRITICAL 2

That aggregation is your compliance posture written as a number you can trend on a chart, alert on, and put in front of a control owner every morning. The two filters earn their keep in different ways. ComplianceStatus FAILED is what drops a resource once somebody fixes it: when a control re-evaluates to PASSED, Security Hub does not create a second finding, it updates the existing one in place and flips Compliance.Status to PASSED, so the resource quietly falls out of a FAILED query while its finding record lives on. RecordState ACTIVE then removes findings Security Hub has archived, which it does when the underlying resource is deleted, the control is disabled, or the finding goes stale (archiving is best-effort, roughly three to five days after a finding stops being updated). Together the pair pins the count to what is failing on resources that still exist, rather than to accumulated history. Wire the same query into a scheduled job that fails when CRITICAL plus HIGH failures cross an agreed threshold, and 'stay under the bar' stops being a hope. It becomes a posture SLO (service level objective, a number you have promised to stay inside) with an exit code behind it.

Put a second opinion on a nightly clock

Leaning only on the cloud provider's own tooling gives you a single point of view, and a single point of failure if a Config recorder is misconfigured. Two kitchen clocks that disagree are annoying; one clock that has stopped is dangerous, because it is right twice a day and confident all the time. Prowler is an open-source, agentless scanner (nothing to install on the machines it inspects) that runs hundreds of checks mapped to frameworks such as CIS, SOC 2, PCI and ISO, and it can push its results back into Security Hub. Running it on a cron (a job that fires on a fixed schedule) from CI (continuous integration, the system that runs your builds) gives you a provider-independent, versioned, reproducible scan on a fixed cadence, one that runs whether or not anyone shipped code that day. Install it, run a single framework, read the verdict.

install, then run one framework fail-only
# 1) install (Python 3.10+); pipx keeps it isolated from project deps
pipx install prowler
# 2) run just the SOC 2 mapping, show only failures, and
# forward every finding into the same Security Hub stream
prowler aws \
--compliance soc2_aws \
--status FAIL \
--security-hub \
--region eu-west-1
expected output — summary plus the gating exit code
Using compliance framework: soc2_aws
FAIL s3_bucket_default_encryption acme-prod-billing-exports eu-west-1
Bucket has no default server-side encryption configured
...
Overview Results:
Total findings: 214 Pass: 200 Fail: 14 Muted: 0
SOC2 mapping (failing controls):
CC6.1 Encryption of data at rest .......... 1 FAIL
CC6.6 Boundary protection ................. 4 FAIL
CC7.2 Anomaly / change detection .......... 3 FAIL
Sending 14 findings to AWS Security Hub... done.
$ echo $?
3

Two details make this CI-grade. First, --status FAIL narrows the output to what needs attention and --security-hub funnels Prowler's findings into the same aggregated ASFF stream as Config, so your dashboard keeps one source of truth instead of two consoles disagreeing in public. Second, the exit code: Prowler returns 3 when any check fails, and a CI runner treats a non-zero exit as a failed job, so the gate is already built and you write no glue code for it. Put the whole thing on a schedule so the control runs on a clock, not on developer activity.

.github/workflows/compliance-scan.yml
name: continuous-compliance
on:
schedule:
- cron: '0 2 * * *' # 02:00 UTC nightly
workflow_dispatch: {}
permissions:
id-token: write # OIDC assume-role, no long-lived keys
contents: read
jobs:
prowler-scan:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111122223333:role/prowler-scan
aws-region: eu-west-1
- run: pipx install prowler
- name: Scan and gate on failures
run: prowler aws --compliance soc2_aws --status FAIL --security-hub
# non-zero exit (3) fails the job -> the scheduled gate fires
- if: always()
uses: actions/upload-artifact@v4
with:
name: prowler-report
path: output/
Green can mean 'unmeasured', not 'safe'
AWS Config only evaluates the resources its recorder is configured to record. If the recorder excludes a resource type, or was never switched on in a region, those resources produce no findings at all, and the dashboard paints them green. Green there reads as 'compliant' when what it actually means is 'nobody looked'. This is the most dangerous failure mode in continuous compliance, because it wears the face of success. Enable the recorder for all supported resource types in every active region (an org-level recorder plus an aggregator is the clean way to do it), and periodically reconcile your real resource inventory against what Config actually evaluated. Skip that and continuous compliance quietly degrades into continuous-for-the-resources-you-happened-to-watch.
Continuous compliance data flow
always-on sources
AWS Config rules
change-triggered: drift in seconds
Security Hub standards
CIS + FSBP scored continuously
Prowler on a CI cron
independent, versioned, nightly
aggregate & normalize
Security Hub (ASFF)
one finding stream, deduped
FAILED + ACTIVE query
current failing, still-existing resources
act on the signal
alert / ticket over threshold
posture SLO, not a hope
exit-code gate in CI
non-zero fails the job
timestamped evidence
operating effectiveness for Type II
Change-triggered rules catch drift, Prowler adds an independent scan, Security Hub aggregates the stream, and the failing-and-active finding count drives alerts, gates, and audit evidence.
Quick check
01A bucket was encrypted on audit day and had encryption stripped 12 days later. Which model catches that exposure, and what does it hand the SOC 2 Type II audit?
Incorrect — It passed on the day it was sampled and nobody looked again until the next audit, so point-in-time is exactly the model that misses the eleven-day window.
Correct — Change-triggered evaluation fires on the configuration change rather than on a schedule, and the OrderingTimestamp and ResultRecordedTime pair shows the control was operating across the period, which is precisely what Type II certifies.
Incorrect — Change-triggered rules fire on every configuration change, not only at creation, and that is what makes near-real-time drift detection possible. A periodic-only rule would sit quiet until its next interval.
Incorrect — A live dashboard shows current state, but Type II wants a durable timestamped record covering the whole period, so evidence storage matters more under continuous compliance, not less.
02Someone remediates an unencrypted bucket. The next morning it has vanished from your ComplianceStatus FAILED plus RecordState ACTIVE query. What happened to its Security Hub finding?
Incorrect — Nothing is deleted. The finding record survives; only its compliance status changed, which is why the resource stops matching a FAILED filter.
Correct — Re-evaluation rewrites the finding you already have instead of creating a new one, so a remediated resource quietly falls out of a FAILED query and its history stays intact.
Incorrect — Archiving is tied to the underlying resource being deleted, the control being disabled, or the finding going stale for roughly three to five days, not to a control starting to pass.
Incorrect — Security Hub does not spin up a new finding on re-evaluation, which is exactly why the FAILED count moves without any new records appearing.
03You launched twenty new buckets in ap-south-1 yesterday. This morning the severity aggregation for that region comes back with no rows at all, and the nightly workflow is green. What do you do next?
Incorrect — Zero can mean nothing was measured. A region with no Config recorder produces no findings, and the dashboard renders that silence as green.
Correct — Config only evaluates what its recorder records, so an absent or narrowed recorder gives you silence that looks identical to success on a dashboard.
Incorrect — Archived findings are ones whose resources were deleted or that went stale for three to five days. That filter cannot surface resources nothing ever evaluated.
Incorrect — Tightening a threshold against a stream that contains no findings changes nothing. The gap here is missing measurement, not a bar set too high.

Try this

Work through “Put a second opinion on a nightly clock” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: green can mean 'unmeasured', not 'safe'. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related