Every cloud environment I've touched has the same two storage problems: a bucket someone made public and forgot, and a storage account that should be behind a private endpoint but isn't. The checks differ slightly per provider, but the question is always the same — "is this storage reachable by someone who shouldn't see it?"
I got tired of answering that question with ad-hoc scripts that died in one environment and were rewritten in the next. So I built one suite that covers all three clouds and writes every result to the same CSV/JSON shape.
What it checks
AWS (S3) — the three ways a bucket becomes public:
- Public Access Block fully enabled
- Bucket policy reported as
IsPublic - ACL grants to
AllUsers/AuthenticatedUsers
Plus a security-config audit for default encryption (SSE-S3/SSE-KMS) and versioning.
GCP (Cloud Storage) — IAM bindings to allUsers / allAuthenticatedUsers, uniform bucket-level access, and public access prevention.
Azure (Storage Accounts) — accounts with and without Private Endpoints, plus TLS version, secure transfer, and public network access posture.
Everything is read-only. No module creates, modifies, or deletes anything — you run it, get a report, fix what it flags.
Running it
# AWS: only the public buckets, as JSON
python aws/s3/audit-public-buckets/audit_public_buckets.py --only-public --format json --output-file public.json
# GCP: security configuration matrix
python gcp/storage/audit-security-config/audit_bucket_security.py --project my-project --output-file compliance.csv
# Azure: storage accounts with no private endpoint
./azure/storage/audit-missing-private-endpoints/audit-storage-without-pe.ps1 -OutputPath exposed.csv
The Azure side also ships Bash and KQL variants so you can run the same check from Resource Graph in the portal.
The report contract
Every module writes the same shape — CSV or JSON — with a boolean "finding" column you actually act on:
| Provider | Finding column |
|---|---|
| AWS public buckets | is_public |
| AWS / GCP security config | hardened |
| GCP public IAM | public |
| Azure (no PE) | PrivateEndpointCount == 0 |
Summaries go to stderr, so stdout stays clean for automation. Redacted example outputs for every module are in docs/samples/, and the full field contract is in docs/report-format.md.
The bug that cost me half a day
The AWS and GCP scripts in each provider are named the same — audit_public_buckets.py, audit_bucket_security.py. That's fine until your test files import both. Python imports by module name, so once pytest cached the AWS version, the GCP tests silently imported the wrong code and failed with the most confusing TypeError.
The fix was loading the modules by explicit file path instead of leaning on sys.path:
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
Half a day, gone, over a naming collision.
Keeping it honest
- Pester validates the Azure modules (parse, parameters, read-only guarantees)
- pytest (mocked cloud clients) covers the AWS/GCP modules
- CI runs PSScriptAnalyzer, ShellCheck, ruff, markdownlint, and yamllint on every push
- Test coverage sits at 85% and is published via Codecov
The first CI run failed in four jobs at once — the lesson being that lint rules move under you (a newer PSScriptAnalyzer started flagging Write-Host as a warning I'd never seen before).
Where it lives
The whole thing is free and open source:
https://github.com/priyaranjan-sahu/multi-cloud-automation-scripts
If you manage storage across more than one cloud, it's probably a better starting point than whatever script you were about to rewrite for the fifth time. PRs and issues welcome.
Top comments (0)