Live-audit GCP, AWS, and Azure with SQL — no agents, no pipelines, no ingestion.
A GitHub Action that runs an opinionated set of security checks against your cloud accounts using stackql. Findings render as a markdown table on the workflow run page. Copy-paste setup, first results in under two minutes.
A provider is audited only if its credentials are supplied — start with one cloud, add the others later just by adding their secrets.
Auditing a whole org (every project / region / subscription, not a single scope)? See Deep audits.
| Provider | Check | Severity |
|---|---|---|
| GCP | SSH (22/tcp) open to the internet | HIGH |
| GCP | RDP (3389/tcp) open to the internet | HIGH |
| GCP | Cloud SQL instances reachable on a public IP | HIGH |
| GCP | Compute instances with public IPs | MEDIUM |
| GCP | Compute instances using the default service account | MEDIUM |
| GCP | Storage buckets without uniform bucket-level access | MEDIUM |
| GCP | Default VPC network exists | MEDIUM |
| AWS | SSH (22/tcp) open to the internet (security groups) | HIGH |
| AWS | RDP (3389/tcp) open to the internet (security groups) | HIGH |
| AWS | RDS instances publicly accessible | HIGH |
| AWS | EC2 instances with a public IP | MEDIUM |
| AWS | Default VPC exists | LOW |
| Azure | SSH (22/tcp) open to the internet (NSG) | HIGH |
| Azure | RDP (3389/tcp) open to the internet (NSG) | HIGH |
| Azure | SQL servers with public network access | HIGH |
| Azure | Storage accounts allowing public blob access | MEDIUM |
Every check is a single YAML file under queries/<provider>/ —
fork, extend, or replace at will.
-
Set the credentials for the cloud(s) you want to audit (see Credentials & configuration).
-
Copy a ready-to-use workflow from
docs/examples/into your repo's.github/workflows/:Example Audits all-clouds-audit-workflow-dispatch.ymlGCP + AWS + Azure in one run google-only-audit-workflow-dispatch.ymlGCP, manual trigger google-only-audit-pull-request.ymlGCP, on every PR to mainaws-only-audit-workflow-dispatch.ymlAWS, manual trigger azure-only-audit-workflow-dispatch.ymlAzure, manual trigger
Open the workflow run → the audit summary renders inline on the run page.
Store sensitive values as repo secrets (Settings → Secrets and variables → Actions → Secrets). Non-sensitive identifiers (project ID, region, subscription ID) can be variables, or just typed in at run time.
| Action input | Suggested GitHub home | Sensitive |
|---|---|---|
gcp-credentials |
secret GCP_SA_JSON (service account JSON key) |
yes |
gcp-project-id |
variable / run-time input | no |
| Action input | Suggested GitHub home | Sensitive |
|---|---|---|
aws-access-key-id |
secret AWS_ACCESS_KEY_ID |
yes |
aws-secret-access-key |
secret AWS_SECRET_ACCESS_KEY |
yes |
aws-region |
variable / run-time input | no |
| Action input | Suggested GitHub home | Sensitive |
|---|---|---|
azure-client-secret |
secret AZURE_CLIENT_SECRET |
yes |
azure-tenant-id |
secret / variable AZURE_TENANT_ID |
identifier |
azure-client-id |
secret / variable AZURE_CLIENT_ID |
identifier |
azure-subscription-id |
variable / run-time input | no |
Azure auth uses a service principal (the tenant/client/secret triplet) via
stackql's azure_default.
| Name | Required | Default | Description |
|---|---|---|---|
gcp-project-id |
for GCP | — | GCP project ID; substituted as ${PROJECT_ID} in google checks. |
gcp-credentials |
for GCP | — | Full contents of a GCP service account JSON key. |
google-provider-version |
no | pinned | stackql Google provider version (blank = latest). |
aws-access-key-id |
for AWS | — | AWS access key ID. |
aws-secret-access-key |
for AWS | — | AWS secret access key. |
aws-region |
for AWS | — | AWS region; substituted as ${AWS_REGION} in aws checks. |
aws-provider-version |
no | latest | stackql AWS provider version. |
azure-subscription-id |
for Azure | — | Azure subscription ID; substituted as ${SUBSCRIPTION_ID} in azure checks. |
azure-tenant-id / azure-client-id / azure-client-secret |
for Azure | — | Service principal credentials. |
azure-provider-version |
no | latest | stackql Azure provider version. |
queries-path |
no | (built-in) | Custom queries dir (must contain per-provider subdirs google/ aws/ azure/). |
fail-on-severity |
no | HIGH |
Fail the workflow on findings at this severity or above. NONE never fails. |
stackql-version |
no | latest |
stackql release to install. |
upload-logs |
no | false |
Upload per-invocation stackql logs as the stackql-audit-logs artifact. |
log-retention-days |
no | 0 |
Retention (days) for that artifact. 0 = repo default; integer in [0, 90]. |
A provider's checks run iff its credentials are present, so unused provider inputs can be left unset.
| Name | Description |
|---|---|
findings-count |
Total findings across all checks. |
highest-severity |
CRITICAL / HIGH / MEDIUM / LOW / NONE. |
Read-only, on the audited scope:
- GCP —
roles/compute.viewer,roles/cloudsql.viewer,roles/storage.objectViewer,roles/iam.securityReviewer(recommended). - AWS — the managed
SecurityAuditpolicy (orReadOnlyAccess) covers the EC2/RDS describe calls the checks make. - Azure — the
Readerrole on the subscription.
For the org-wide deep audits (scripts/discover.py), grant the same read
roles one scope up so they inherit, plus enumeration rights:
- GCP —
roles/viewer+roles/resourcemanager.folderViewerat the organization node (Viewer covers the resource reads + project listing; folderViewer adds folder descent). - AWS —
SecurityAudit(orReadOnlyAccess) on the principal; the S3 deep scan additionally needss3:GetBucket*and, if it routes via Cloud Control,cloudcontrol:GetResource/ListResources(ReadOnlyAccessincludes these). - Azure —
Readerat the management-group (or tenant root) scope, which inherits to every child subscription and covers the management-group descent.
Point queries-path at your own directory of per-provider subdirs. Each file
is one check:
id: my-org-firewall-check
name: Allow only known source ranges
severity: HIGH
description: Catch firewall rules with sourceRanges outside our corp CIDRs.
remediation: Restrict to known CIDRs or migrate to IAP.
query: |
SELECT name, network, sourceRanges
FROM google.compute.firewalls
WHERE project = '${PROJECT_ID}'
AND direction = 'INGRESS'
columns: [name, network, sourceRanges]A query returning zero rows = no findings. Any rows returned become finding
rows. For checks where SQL alone can't express the audit logic (e.g. filtering
by structure inside a nested column), reference a function in
scripts/filters.py:
filter: firewall_allows_port
filter_args:
port: 22
protocol: tcpSet upload-logs: true to capture a per-invocation log for every stackql exec
(the query, exit code, and stderr) and upload it as the stackql-audit-logs
artifact. This is the fastest way to diagnose a check that returns nothing
unexpectedly — stderr is recorded even when the query exits 0. Tune retention
with log-retention-days (0 = repo default, max 90).
┌───────────────────────────┐
│ queries/<provider>/*.yaml │ (one file per check, per cloud)
└────────────┬──────────────┘
│
▼
┌───────────────────────────┐
│ audit.py │ fans out N parallel
│ └─ ThreadPoolExecutor │ `stackql exec` subprocesses
└────────────┬──────────────┘
│
▼
┌────────────────────────────────────┐
│ stackql exec --output json │
│ SELECT ... FROM google.*/aws.*/ │ <─── live API calls
│ azure.* │
└────────────┬────────────────────────┘
│
▼
┌───────────────────────────┐
│ optional filter (Python) │ for checks SQL can't express
└────────────┬──────────────┘
│
▼
┌───────────────────────────┐
│ $GITHUB_STEP_SUMMARY │ markdown tables, severity badges
└───────────────────────────┘
One combined --auth object carries every supplied provider, so a single fan-out
covers all three clouds. Each check runs in its own short-lived stackql process —
cleanup is automatic and checks run concurrently (8 at a time by default;
override with STACKQL_AUDIT_PARALLEL).
stackql queries cloud control planes live. No resource crawl, no inventory
database, no daily sync — every SELECT hits the cloud API at query time. You
see what the cloud sees right now.
MIT
