CoursesAdvanced cloud securityCSPM & continuous compliance

CSPM & continuous compliance

Config rules, CIS benchmarks, and drift as a security signal.

Advanced30 min · lesson 13 of 15

A yearly compliance audit is a photograph of a river. It captures the water on one day and tells you nothing about the current for the other 364. Your cloud estate is that river: thousands of resources changing every day, and any single change can carve a channel straight to your data. Cloud Security Posture Management (CSPM), which means watching the configuration of every resource around the clock and flagging the unsafe ones, swaps the photograph for a gauge fixed to the bank. A public storage bucket or a wide-open firewall shows up minutes after someone creates it, not at next year's review.

The loop every CSPM tool runs

Underneath the branding, every CSPM tool runs the same short loop. You assert a rule, which is one yes-or-no question you can ask of a resource. The tool evaluates every matching resource against it. Then it surfaces the failures as findings, and a finding is something a person or a script now has to close. The rules come from four places: the cloud provider's own best practices, industry benchmarks like the CIS (Center for Internet Security) Foundations Benchmarks, regulatory and audit frameworks such as PCI-DSS (the Payment Card Industry Data Security Standard), NIST 800-53 (a US government catalog of security controls), and SOC 2 (a common trust and security audit report), and your own house policy. Each rule is deliberately small. Storage buckets must block public reads. Virtual machines must not carry a public IP address. Key policies must not grant access to everyone. Small rules are easy to test, easy to defend in a review, and easy to run in more than one place, which turns out to be the whole game.

A lock on the door versus a camera in the hall

Two kinds of control enforce those rules, and the difference decides how much damage a slip can do. A preventive control is a lock on the door. It refuses the bad configuration before it can exist: an AWS service control policy (SCP, an organization-wide rule that no account beneath it can escape, admins included), a Google Cloud organization policy constraint, an Azure Policy 'deny' effect, or a scan in your build pipeline (CI, short for continuous integration, the automated checks that run on every change) that fails the merge. A detective control is a camera in the hallway. It watches resources that already exist and raises a finding after the fact. Neither one replaces the other. The gate blocks the obvious mistakes cheaply, and continuous evaluation at runtime catches everything that still gets past it: drift, someone clicking around in the console, or a class of resource your pipeline never scanned. Write the same intent in both places and you are covered coming and going.

Block it in the pipeline

Start on the cheap side, before anything is deployed. Checkov, an open-source scanner that reads your infrastructure-as-code (the text files that define your cloud, kept in version control) and checks it against hundreds of built-in rules, can read the very same Terraform that builds all three clouds and fail the build when a resource is wrong. One tool, one command, every provider. Each rule has an identifier. CKV_AWS_53 checks that an S3 bucket blocks public access control lists (ACLs, the older per-object permission model). CKV_GCP_29 checks that a Google Cloud Storage bucket has uniform bucket-level access turned on. CKV_AZURE_59 checks that an Azure storage account disallows public access. One idea, three spellings.

terminal
# One scanner, all three clouds' Terraform, before anything is applied.
checkov -d ./infra --compact --quiet \
--check CKV_AWS_53,CKV_GCP_29,CKV_AZURE_59
echo "exit=$?"
output
_ _
___| |__ ___ ___| | _______ __
/ __| '_ \ / _ \/ __| |/ / _ \ \ / /
| (__| | | | __/ (__| < (_) \ V /
\___|_| |_|\___|\___|_|\_\___/ \_/
by Prisma Cloud | version: 3.2.334
Passed checks: 41, Failed checks: 2, Skipped checks: 0
Check: CKV_AWS_53: "Ensure S3 bucket has block public ACLs enabled"
FAILED for resource: aws_s3_bucket.exports
File: /storage.tf:12-19
Check: CKV_AZURE_59: "Ensure that Storage accounts disallow public access"
FAILED for resource: azurerm_storage_account.logs
File: /azure_storage.tf:4-14
exit=1 # non-zero exit fails the merge request

CKV_GCP_29 passed, so it is not in the list. The two failures are, and the exit code 1 is the only part the pipeline actually reads: non-zero, so the merge request stops here. Nobody had to spot the public bucket in a review, because the machine refused to let it land.

The same question on three control planes

Once resources are live, each cloud runs its own CSPM engine, and in a multi-cloud shop you operate all three. AWS pairs AWS Config, which records every configuration change as a dated item and evaluates Config rules against it (change-triggered or on a schedule), with Security Hub, which collects the findings and scores them against packaged standards. Google Cloud's Security Command Center (SCC) runs Security Health Analytics (SHA), a built-in detector set that scans on a fixed cadence and writes findings you can query for the whole organization, a folder, or one project. Azure's Microsoft Defender for Cloud grades resources against Azure Policy initiatives and rolls the result into a Secure Score. Different nouns, identical loop. Here is each one answering the same question: which resources are non-compliant right now?

terminal
# AWS: a managed Config rule flags any bucket that turns public.
aws configservice put-config-rule --config-rule '{
"ConfigRuleName": "s3-bucket-public-read-prohibited",
"Source": { "Owner": "AWS",
"SourceIdentifier": "S3_BUCKET_PUBLIC_READ_PROHIBITED" }
}'
# Which buckets fail right now? (put-config-rule prints nothing on success)
aws configservice get-compliance-details-by-config-rule \
--config-rule-name s3-bucket-public-read-prohibited \
--compliance-types NON_COMPLIANT \
--query 'EvaluationResults[].EvaluationResultIdentifier.EvaluationResultQualifier.ResourceId' \
--output json
output
[
"prod-analytics-exports",
"legacy-cf-logs-2019"
]
terminal
# Google Cloud: Security Health Analytics writes findings into SCC on its own.
# List active HIGH-severity public-bucket findings across the whole org.
gcloud scc findings list organizations/846251066572 \
--source=- \
--filter='state="ACTIVE" AND severity="HIGH" AND category="PUBLIC_BUCKET_ACL"' \
--format='table(category, resourceName.basename(), eventTime)'
output
CATEGORY RESOURCE_NAME EVENT_TIME
PUBLIC_BUCKET_ACL prod-analytics-exports 2026-07-14T09:12:03.774Z
PUBLIC_BUCKET_ACL ml-datasets-public 2026-07-19T22:41:10.208Z
terminal
# Azure: the subscription's Secure Score, then the Policy compliance rollup.
az security secure-scores show --name ascScore \
--query '{current:properties.score.current, max:properties.score.max, pct:properties.score.percentage}'
az policy state summarize --management-group mg-prod \
--query 'value[0].results.{policies:nonCompliantPolicies, resources:nonCompliantResources}'
output
{
"current": 38.0,
"max": 58.0,
"pct": 0.6552
}
{
"policies": 9,
"resources": 47
}

Read the three outputs side by side and the trade-off jumps out. AWS names the exact buckets that fail one rule. Google Cloud hands you the same failure as a finding with a timestamp, so you know when the exposure started. Azure gives you the aggregate instead: a Secure Score of 38 out of 58, about 66 percent, and 47 resources failing 9 policies. Same reality, three shapes. Pulling those shapes back together is a real cost, and we will come back to it.

A benchmark is a checklist a whole industry agreed on

A CIS Benchmark is a health-inspection checklist for a cloud that a whole industry agreed on, so you are not inventing your own rules or defending them in a meeting. The AWS version, the CIS AWS Foundations Benchmark, spells out concrete controls: '[S3.8] S3 general purpose buckets should block public access', '[EC2.2] VPC default security groups should not allow inbound or outbound traffic', '[IAM.4] IAM root user access key should not exist', '[KMS.4] AWS KMS key rotation should be enabled'. You subscribe to the benchmark, the cloud scores every control, and you watch the percentage move over time the way you watch a credit score. That percentage, tracked continuously instead of once a year, is what 'continuous compliance' actually means.

Every cloud packages the benchmark, but the command line tells a different story on each. On AWS you turn the standard on and the findings start flowing. On Azure the CLI hands you the pass and fail counts directly. On Google Cloud the single percentage lives in the console, while the CLI gives you the failing controls one category at a time, because in SCC each Security Health Analytics detector is a CIS control.

terminal
# AWS: subscribe the account to CIS AWS Foundations Benchmark v5.0.0
# (the current version AWS recommends).
aws securityhub batch-enable-standards \
--standards-subscription-requests \
'StandardsArn=arn:aws:securityhub:us-east-1::standards/cis-aws-foundations-benchmark/v/5.0.0'
# Confirm which standards are enabled, and their status.
aws securityhub get-enabled-standards
output
{
"StandardsSubscriptions": [
{
"StandardsSubscriptionArn": "arn:aws:securityhub:us-east-1:123456789012:subscription/aws-foundational-security-best-practices/v/1.0.0",
"StandardsArn": "arn:aws:securityhub:us-east-1::standards/aws-foundational-security-best-practices/v/1.0.0",
"StandardsInput": {},
"StandardsStatus": "READY"
},
{
"StandardsSubscriptionArn": "arn:aws:securityhub:us-east-1:123456789012:subscription/cis-aws-foundations-benchmark/v/5.0.0",
"StandardsArn": "arn:aws:securityhub:us-east-1::standards/cis-aws-foundations-benchmark/v/5.0.0",
"StandardsInput": {},
"StandardsStatus": "PENDING"
}
]
}
terminal
# Azure: compliance state per regulatory standard in Defender for Cloud.
az security regulatory-compliance-standards list \
--query '[].{Standard:name, State:properties.state, Pass:properties.passedControls, Fail:properties.failedControls, Skip:properties.skippedControls}' \
-o table
output
Standard State Pass Fail Skip
--------------- ------- ------ ------ ------
Azure-CIS-1.1.0 Failed 41 12 7
Azure-CIS-1.3.0 Failed 58 19 9
PCI-DSS-3.2.1 Failed 33 16 5
ISO-27001 Failed 12 8 2
SOC-TSP Failed 9 6 3
terminal
# Google Cloud: no single CIS percentage from the CLI, so count the failing
# detectors by category. Each category maps to a CIS control.
gcloud scc findings list organizations/846251066572 --source=- \
--filter='state="ACTIVE"' \
--format='value(category)' | sort | uniq -c | sort -rn | head
output
9 BUCKET_POLICY_ONLY_DISABLED
6 MFA_NOT_ENFORCED
5 OPEN_FIREWALL
3 NON_ORG_IAM_MEMBER
2 PUBLIC_BUCKET_ACL
2 KMS_PROJECT_HAS_OWNER

Look at the AWS status. It reads PENDING because you turned the standard on moments ago. Security Hub can take up to 18 hours to generate findings for a control that shares an underlying Config rule with a standard you already had enabled (Foundational Security Best Practices, or FSBP, the one already showing READY), so a fresh benchmark is not a same-minute answer. Two more things bite. The control identifiers never line up across clouds: the public-bucket rule is '[S3.8]' on AWS, 'PUBLIC_BUCKET_ACL' on Google Cloud, and a policy named 'Storage accounts should disallow public access' on Azure. And CIS ships new versions over time, so v1.2.0, v1.4.0, v3.0.0, and v5.0.0 all sit side by side in Security Hub. v5.0.0 is the current one, which is why you enabled it above. When the next version lands, turn it on before you retire the old one, or you leave a gap in your checks during the switch.

Drift is the signal, not the noise

A tamper-evident seal on a medicine bottle does not stop anyone opening it. It tells you that someone did. Drift is that seal for your cloud. When the live configuration stops matching the approved, version-controlled baseline, that gap is itself a security event. Maybe a teammate loosened a control by hand under a deadline. Maybe an attacker who already has a foothold is quietly widening one: a security group opened to the internet, flow logs switched off, a Key Management Service (KMS) key policy broadened, public-access-prevention peeled off a bucket. You cannot tell which from a number on a dashboard, so you treat every out-of-band change to a security-relevant resource as something to investigate, not a metric to admire.

The cheapest drift detector is the infrastructure-as-code tool you already run. Keep the desired state in Terraform, and a plan that wants to change something you did not change means the world moved underneath you.

terminal
# The baseline lives in Terraform. Does the live estate still match it?
# -detailed-exitcode returns: 0 = in sync, 1 = error, 2 = drift found.
terraform plan -detailed-exitcode
echo "exit=$?"
output
Note: Objects have changed outside of Terraform
Terraform detected the following changes made outside of Terraform since the
last "terraform apply" which may have affected this plan:
# aws_security_group.db has changed
~ resource "aws_security_group" "db" {
id = "sg-0ab12cd34ef56789a"
name = "db-tier"
~ ingress = [
+ {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ description = ""
+ from_port = 5432
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "tcp"
+ security_groups = []
+ self = false
+ to_port = 5432
},
# (1 unchanged element hidden)
]
# (8 unchanged attributes hidden)
}
Unless you have made equivalent changes to your configuration, or ignored the
relevant attributes using ignore_changes, the following plan may include
actions to undo or respond to these changes.
Terraform will perform the following actions:
# aws_security_group.db will be updated in-place
~ resource "aws_security_group" "db" {
id = "sg-0ab12cd34ef56789a"
~ ingress = [
- {
- cidr_blocks = [
- "0.0.0.0/0",
]
- description = ""
- from_port = 5432
- ipv6_cidr_blocks = []
- prefix_list_ids = []
- protocol = "tcp"
- security_groups = []
- self = false
- to_port = 5432
},
# (1 unchanged element hidden)
]
# (8 unchanged attributes hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
exit=2

Someone opened the database security group to 0.0.0.0/0 on port 5432, the PostgreSQL port, straight to the open internet. Terraform did not do that, so it reports the object changed outside Terraform and offers to put it back by removing the rule. The exit code 2 is the machine-readable half of the signal, and it is what you wire into a scheduled job: exit 2 means open a ticket and page whoever is on call, then decide whether the change was legitimate (in which case it belongs in the code) or hostile (in which case it belongs in your incident queue).

Auto-remediation can fight your own code
Wiring findings straight to auto-remediation with no guardrails causes flapping. The fixer 'corrects' a resource, your next Terraform apply reasserts the tracked-but-drifted state, and now you have an alert storm. Worse, a blind fixer can delete a resource that carried a documented, approved exception. Remediate back to the source of truth, honor your suppressions, and keep high-blast-radius fixes (deleting resources, rewriting key policies, closing production access) behind human approval.

The honest costs: latency, money, parity

Detective CSPM buys you time-to-detection and charges for it in three currencies. The first is latency. Evaluation is periodic, not instant. AWS change-triggered Config rules fire within minutes, but Security Health Analytics batches its scans on a multi-hour cadence and Defender re-assesses on its own clock. Every one of those windows is time the exposure is live and unwatched, which is the entire argument for the preventive controls that shrink the window to zero.

The second currency is money, and it grows with the estate. AWS Config bills per configuration item it records and per rule evaluation. Security Hub bills per finding and per check. Defender for Cloud bills per protected resource per hour. Security Command Center's paid tiers carry a subscription cost of their own. Across thousands of resources in dozens of accounts, projects, and subscriptions, this becomes a real line item, and a 'record everything, everywhere' default can cost more than the workloads it is watching. Scope what you record on purpose.

The third is parity, the problem you already saw in those three outputs. One 'public storage' fact carries a different identifier and severity in every cloud. Teams either normalize inside one provider (Security Hub's AWS Security Finding Format, or ASFF, gives every finding a common shape) or buy a third-party cloud-native application protection platform (CNAPP) such as Wiz, Prisma Cloud, or Orca that pulls all three into a single posture view. Whichever you choose, aggregate at the top of the org tree: a delegated administrator account in AWS, organization-level SCC in Google Cloud, a management-group Azure Policy assignment in Azure. Do it there and no single account can quietly switch its own evaluation off.

One rule, enforced in three places, across three clouds
Prevent (before it lands)
IaC scan in CI
Checkov, fail the merge
Guardrail policy
AWS SCP / GCP Org Policy / Azure deny
Detect (continuous)
AWS Config + Security Hub
config items, CIS + FSBP
GCP SCC + Health Analytics
scheduled scans, org findings
Azure Defender for Cloud
Azure Policy, Secure Score
Respond (close the loop)
Normalize findings
ASFF / SCC / CNAPP
Drift = investigate
out-of-band change is an event
Remediate to source
fix the Terraform, not the resource
Write the same intent as a preventive gate and a detective check, then treat any drift between them as an event.
Quick check
01AWS Config's s3-bucket-public-read-prohibited rule is change-triggered and pages the owner within minutes of a bucket becoming public. Why is that alone still an incomplete control?
Incorrect — The bucket was world-readable for the whole window between the change and the fix, and detecting the exposure does not undo it.
Correct — detective and preventive controls are complementary, and only the preventive one shrinks the exposure window to zero.
Incorrect — False: S3_BUCKET_PUBLIC_READ_PROHIBITED is a native AWS Config managed rule that evaluates buckets directly.
Incorrect — False: a managed rule can be change-triggered by a configuration event and fire in near real time.
02You run batch-enable-standards for CIS v5.0.0, then get-enabled-standards, and the new standard shows StandardsStatus: PENDING with no findings yet. What is the right read?
Incorrect — Re-running does nothing useful; PENDING right after enabling is expected, not a failed call.
Incorrect — a freshly enabled standard has not finished its first evaluation, so empty means 'not scored yet', not 'clean'.
Correct — benchmark scoring is not a same-minute answer, so plan for the delay before trusting the number.
Incorrect — v1.2.0, v1.4.0, v3.0.0, and v5.0.0 are all supported, and PENDING is a normal transitional state.
03A nightly job runs terraform plan -detailed-exitcode against production. Tonight it prints 'aws_security_group.db has changed' with a new 0.0.0.0/0 ingress on port 5432 and exits 2, though nobody merged a change. What should the job do?
Incorrect — 0 means in sync and 2 means changes or drift were found, which is the opposite of empty.
Incorrect — Dangerous: auto-applying blindly could revert a legitimate emergency change or erase evidence that an intruder made it, so a human decides first.
Incorrect — exit 2 is Terraform's defined 'changes present' code, not a timeout or a concurrency problem.
Correct — unexplained drift on a security-relevant resource is an event to investigate, and this one opened PostgreSQL to the world.

The next lesson keeps this continuous-evaluation habit and moves the target inward. Instead of the whole estate, it looks inside the workloads: for containers and serverless functions, posture stops being about bucket permissions and becomes image provenance, runtime privileges, and exactly what a function is allowed to call. Same loop, a different rule library, evaluated one layer down the stack.

Try this

Run echo "exit=$?" on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.

Takeaway

The trap worth remembering here: auto-remediation can fight your own code. 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