Audit readiness
Audit prep as a query, not a scramble.
Three weeks before your SOC 2 Type II window closes, the auditor emails a PBC list. SOC 2 (Service Organization Control 2) is the report customers ask for before they trust you with their data, and Type II is the version that tests your controls across months rather than on a single day. PBC stands for provided by client, the auditor's shopping list of evidence they expect you to hand over. Item CC6.1 asks you to prove one thing: logical access to customer data stayed restricted for the whole audit period. In practice that means three files. The MFA (multi-factor authentication, a second login step on top of a password) status of every IAM (identity and access management) principal that can reach the console. The public-access posture of every S3 (Simple Storage Service, the AWS file store) bucket holding customer data. And a record of every change to the production bucket policy during the period. Run a manual program and this kicks off a screenshot marathon across dozens of console pages, half of it rebuilt from memory. The gap between a smooth audit and a miserable one comes down to one question. Does that evidence already exist as data you can query, or only as institutional memory you have to reassemble? In a compliance-as-code program each of those three asks is a query you run in under a minute, and the result of the query is the evidence.
Every control is a question, and the answer sits in one of two places
Walking the building at closing time and rattling every door tells you the building is locked right now. It tells you nothing about the fire exit somebody propped open at 2pm. For that you need the camera footage. Compliance evidence splits along the same line. A control becomes auditable the moment you can phrase it as a question and point that question at something which knows the answer. Steampipe handles the first half. It puts a SQL (Structured Query Language, the standard way of asking a database for rows) face on live cloud APIs, so "which buckets allow public access" turns into a plain SELECT against a table called aws_s3_bucket. No scripting against the AWS SDK (Software Development Kit, the code libraries you would otherwise wire up yourself), no clicking through the console. But Steampipe only ever reads current state, and an auditor tests a period. For who changed what and when, the source of truth is CloudTrail, the account's permanent append-only log of every API call, and you query that with Amazon Athena, which runs SQL straight over the raw log files sitting in S3. Two data planes, two engines. Steampipe for what is true this second, Athena over CloudTrail for what happened across the whole window.
Step 1, install Steampipe and point it at the account
Steampipe runs anywhere you have read-only cloud credentials. Install the binary, add the AWS plugin, and it picks up the AWS CLI profile or environment credentials you already have. Scope those credentials to a read-only audit role, never an admin key. And check the install works now, not in the meeting where an auditor is watching your screen.
# Install Steampipe and the AWS plugin (Linux / macOS)sudo /bin/sh -c "$(curl -fsSL https://steampipe.io/install/steampipe.sh)"steampipe plugin install awssteampipe --version
Installed plugin: aws@latestSteampipe v2.4.4
Steampipe embeds PostgreSQL and presents each cloud service as a foreign table, a table whose rows are fetched live from somewhere else instead of stored on disk. Every SQL feature you already know works on top of that: joins, aggregates, and CTEs (common table expressions, the WITH clauses that let you name a subquery and reuse it), across accounts and regions. It caches API responses for the session, so a broad query over hundreds of buckets costs one sweep of the API rather than one call per row. And because it only ever reads, you can hand it a role limited to Describe, List, and Get. An auditor can sit beside you and watch the query run with zero risk to production.
Step 2, turn the control text into a SELECT
CC6.1, and the PCI DSS (Payment Card Industry Data Security Standard, the rulebook for anyone who touches card data) requirement covering public exposure, both demand the same thing. Storage holding customer data must not be reachable by the general public. The aws_s3_bucket table hands you the columns that settle it: block_public_acls (an ACL is an access control list, the older per-object permission model on S3), block_public_policy, and a computed column called bucket_policy_is_public. A bucket fails the control if any public path is open, so the WHERE clause is the sentence from the control document, translated. Writing the control as SQL buys you something else. The check is reviewable in a pull request, unlike a one-off script living in someone's terminal history. Run it and you get the failing buckets, the exact rows an auditor asks for, with every passing bucket kept out of the way.
steampipe query "select name, region, block_public_acls, block_public_policy, bucket_policy_is_publicfrom aws_s3_bucketwhere not block_public_acls or not block_public_policy or bucket_policy_is_publicorder by name;"
+---------------------+-----------+-------------------+---------------------+-------------------------+| name | region | block_public_acls | block_public_policy | bucket_policy_is_public |+---------------------+-----------+-------------------+---------------------+-------------------------+| acme-legacy-exports | us-east-1 | false | false | true |+---------------------+-----------+-------------------+---------------------+-------------------------+
One row, one finding. acme-legacy-exports is publicly readable, and that same result set doubles as proof the control was tested. Empty output is the happy path: a documented, timestamped no-exceptions result. Save the output as JSON or CSV with a single --output flag, attach it to the control in your evidence store, and move on. The MFA ask has the same shape. Export it straight to CSV, the format auditors drop into their workpapers, which are the working files that back up the final report.
mkdir -p evidencesteampipe query --output csv "select name as user_name, mfa_enabled, password_last_used, create_datefrom aws_iam_userwhere not mfa_enabled and login_profile is not nullorder by name;" > evidence/pci-8.4-console-users-without-mfa.csvcat evidence/pci-8.4-console-users-without-mfa.csv
user_name,mfa_enabled,password_last_used,create_datecontractor-batch,false,2026-06-28T14:02:11Z,2026-03-01T09:15:00Zsvc-legacy-deploy,false,2026-07-01T03:44:56Z,2025-11-12T18:20:00Z
That CSV is the PBC deliverable. Two console users with passwords and no second factor, each one a real access-control gap you can close before the auditor ever opens the file. The query is your finding and your evidence at the same time, which is the whole point. The enforcement check and the audit record are one artifact.
Step 3, prove it held for the whole window
Steampipe told you the bucket policy is safe today. The auditor's harder question is whether it was ever unsafe during the six-month window, and only CloudTrail knows. Save the query as a versioned .sql file so next year's evidence request is a rerun instead of a rewrite, then run it against your Athena CloudTrail table and export the rows to CSV.
SELECT eventtime,useridentity.arn AS actor,eventnameFROM cloudtrail_logsWHERE eventsource = 's3.amazonaws.com'AND eventname IN ('PutBucketPolicy', 'PutBucketAcl', 'DeleteBucketPolicy')AND requestparameters LIKE '%acme-customer-data%'AND eventtime BETWEEN '2026-04-01T00:00:00Z' AND '2026-06-30T23:59:59Z'ORDER BY eventtime;
QID=$(aws athena start-query-execution \--query-string file://athena/soc2-cc6.1-bucket-policy-changes.sql \--work-group primary \--result-configuration OutputLocation=s3://acme-audit-evidence/q2-2026/ \--query QueryExecutionId --output text)# start-query-execution returns instantly; wait for Athena to finish writing the CSVuntil aws athena get-query-execution --query-execution-id "$QID" \--query QueryExecution.Status.State --output text | grep -qx SUCCEEDED; dosleep 5doneaws s3 cp "s3://acme-audit-evidence/q2-2026/${QID}.csv" evidence/soc2-cc6.1-q2-changes.csvcat evidence/soc2-cc6.1-q2-changes.csv
"eventtime","actor","eventname""2026-05-14T22:07:03Z","arn:aws:iam::123456789012:user/contractor-batch","PutBucketPolicy""2026-05-14T22:41:19Z","arn:aws:iam::123456789012:role/sec-remediation","PutBucketPolicy"
Two events, and both have a story. A contractor loosened the production bucket policy at 22:07, and the security-remediation role put it back 34 minutes later. That is a control exception with its own automated correction, recorded somewhere nobody can quietly edit. Auditors want that traceable narrative far more than they want your assurance that nothing happened. One operational detail decides whether the export works at all: start-query-execution returns the instant the query is accepted, so poll get-query-execution until the state reads SUCCEEDED before you fetch the results. In CI (continuous integration, the automated build and test run that fires on every change) you gate on the row count. Zero unexplained changes is a pass. Any unexplained change is an exit-1 finding routed to the control owner.
Making the evidence auditor-ready
Raw query output convinces an engineer. An auditor needs a paper trail. Name each query file after the control it satisfies, such as soc2-cc6.1-bucket-policy-changes.sql, and commit it, so the mapping from written requirement to running check is itself version-controlled evidence. Store each run's output with a timestamp and a content hash (a short fingerprint of the file's bytes that changes if a single character changes) in a write-once location, so nobody can edit a result after the fact. Watch for false positives. A bucket flagged public may be an intentional static-website host, so keep a reviewed exception list with a named owner and a written justification, rather than editing the query until the awkward row disappears. And because controls share evidence, one result set can answer SOC 2, PCI, and ISO at the same time. None of it certifies anything on its own. A passing check is evidence toward a control, and the auditor still writes the narrative and picks the sample.
None of these three queries should first run three weeks before the audit. That is still a scramble, only a quicker one. The next lesson, Compliance in DevSecOps, wires these same SELECTs and CloudTrail checks into the pipeline and into scheduled jobs, so the evidence store fills up week by week and audit prep turns into exporting what the system has already collected.
Try this
Work through “Making the evidence auditor-ready” 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: a live SELECT proves today, not the period. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.