Skip to content

Repository files navigation

StackQL Cloud Audit Action

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.

sample summary

What it finds

ProviderCheckSeverity
GCPSSH (22/tcp) open to the internetHIGH
GCPRDP (3389/tcp) open to the internetHIGH
GCPCloud SQL instances reachable on a public IPHIGH
GCPCompute instances with public IPsMEDIUM
GCPCompute instances using the default service accountMEDIUM
GCPStorage buckets without uniform bucket-level accessMEDIUM
GCPDefault VPC network existsMEDIUM
AWSSSH (22/tcp) open to the internet (security groups)HIGH
AWSRDP (3389/tcp) open to the internet (security groups)HIGH
AWSRDS instances publicly accessibleHIGH
AWSEC2 instances with a public IPMEDIUM
AWSDefault VPC existsLOW
AzureSSH (22/tcp) open to the internet (NSG)HIGH
AzureRDP (3389/tcp) open to the internet (NSG)HIGH
AzureSQL servers with public network accessHIGH
AzureStorage accounts allowing public blob accessMEDIUM

Every check is a single YAML file under queries/<provider>/ — fork, extend, or replace at will.

Quickstart

  1. Set the credentials for the cloud(s) you want to audit (see Credentials & configuration).

  2. Copy a ready-to-use workflow from docs/examples/ into your repo's .github/workflows/:

    ExampleAudits
    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 main
    aws-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.

Credentials & configuration

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.

GCP

Action inputSuggested GitHub homeSensitive
gcp-credentialssecret GCP_SA_JSON (service account JSON key)yes
gcp-project-idvariable / run-time inputno

AWS

Action inputSuggested GitHub homeSensitive
aws-access-key-idsecret AWS_ACCESS_KEY_IDyes
aws-secret-access-keysecret AWS_SECRET_ACCESS_KEYyes
aws-regionvariable / run-time inputno

Azure

Action inputSuggested GitHub homeSensitive
azure-client-secretsecret AZURE_CLIENT_SECRETyes
azure-tenant-idsecret / variable AZURE_TENANT_IDidentifier
azure-client-idsecret / variable AZURE_CLIENT_IDidentifier
azure-subscription-idvariable / run-time inputno

Azure auth uses a service principal (the tenant/client/secret triplet) via stackql's azure_default.

Inputs

NameRequiredDefaultDescription
gcp-project-idfor GCPGCP project ID; substituted as ${PROJECT_ID} in google checks.
gcp-credentialsfor GCPFull contents of a GCP service account JSON key.
google-provider-versionnopinnedstackql Google provider version (blank = latest).
aws-access-key-idfor AWSAWS access key ID.
aws-secret-access-keyfor AWSAWS secret access key.
aws-regionfor AWSAWS region; substituted as ${AWS_REGION} in aws checks.
aws-provider-versionnolateststackql AWS provider version.
azure-subscription-idfor AzureAzure subscription ID; substituted as ${SUBSCRIPTION_ID} in azure checks.
azure-tenant-id / azure-client-id / azure-client-secretfor AzureService principal credentials.
azure-provider-versionnolateststackql Azure provider version.
queries-pathno(built-in)Custom queries dir (must contain per-provider subdirs google/aws/azure/).
fail-on-severitynoHIGHFail the workflow on findings at this severity or above. NONE never fails.
stackql-versionnolateststackql release to install.
upload-logsnofalseUpload per-invocation stackql logs as the stackql-audit-logs artifact.
log-retention-daysno0Retention (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.

Outputs

NameDescription
findings-countTotal findings across all checks.
highest-severityCRITICAL / HIGH / MEDIUM / LOW / NONE.

Required permissions

Read-only, on the audited scope:

  • GCProles/compute.viewer, roles/cloudsql.viewer, roles/storage.objectViewer, roles/iam.securityReviewer (recommended).
  • AWS — the managed SecurityAudit policy (or ReadOnlyAccess) covers the EC2/RDS describe calls the checks make.
  • Azure — the Reader role 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:

  • GCProles/viewer + roles/resourcemanager.folderViewer at the organization node (Viewer covers the resource reads + project listing; folderViewer adds folder descent).
  • AWSSecurityAudit (or ReadOnlyAccess) on the principal; the S3 deep scan additionally needs s3:GetBucket* and, if it routes via Cloud Control, cloudcontrol:GetResource/ListResources (ReadOnlyAccess includes these).
  • AzureReader at the management-group (or tenant root) scope, which inherits to every child subscription and covers the management-group descent.

Custom checks

Point queries-path at your own directory of per-provider subdirs. Each file is one check:

id: my-org-firewall-checkname: Allow only known source rangesseverity: HIGHdescription: 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_portfilter_args:
port: 22protocol: tcp

Logs & debugging

Set 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).

How it works

 ┌───────────────────────────┐
│ 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).

Why stackql

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.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages