Continuous compliance
Always-on evaluation vs point-in-time.
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.
# 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
[{"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.
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)"'
MEDIUM 31HIGH 9LOW 7CRITICAL 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.
# 1) install (Python 3.10+); pipx keeps it isolated from project depspipx install prowler# 2) run just the SOC 2 mapping, show only failures, and# forward every finding into the same Security Hub streamprowler aws \--compliance soc2_aws \--status FAIL \--security-hub \--region eu-west-1
Using compliance framework: soc2_awsFAIL s3_bucket_default_encryption acme-prod-billing-exports eu-west-1Bucket has no default server-side encryption configured...Overview Results:Total findings: 214 Pass: 200 Fail: 14 Muted: 0SOC2 mapping (failing controls):CC6.1 Encryption of data at rest .......... 1 FAILCC6.6 Boundary protection ................. 4 FAILCC7.2 Anomaly / change detection .......... 3 FAILSending 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.
name: continuous-complianceon:schedule:- cron: '0 2 * * *' # 02:00 UTC nightlyworkflow_dispatch: {}permissions:id-token: write # OIDC assume-role, no long-lived keyscontents: readjobs:prowler-scan:runs-on: ubuntu-lateststeps:- uses: aws-actions/configure-aws-credentials@v4with:role-to-assume: arn:aws:iam::111122223333:role/prowler-scanaws-region: eu-west-1- run: pipx install prowler- name: Scan and gate on failuresrun: 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@v4with:name: prowler-reportpath: output/
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.