AWS security and cost swiss army knife — single binary, zero dependencies.
Audit IAM permissions, spot cost anomalies, find orphaned resources, flag insecure storage, detect overly permissive security groups, monitor TLS certificate expiry, enforce resource tagging, check service quota utilization, save and compare scan baselines, generate HTML reports, and more — across your AWS account, plus a Kubernetes RBAC scanner.
cloudgov is an AWS governance CLI. Every cloud domain is implemented against AWS,
and a Kubernetes RBAC scanner covers the cluster side. Internally each domain is a
capability interface (cloud.IAMProvider, cloud.StorageProvider, …) resolved through
a provider registry, so commands stay decoupled from SDK wiring.
| Domain (command) | AWS | Kubernetes |
|---|---|---|
IAM (iam scan / iam fix) |
✅ | — |
Cost (cost diff) |
✅ | — |
Orphans (orphans) |
✅ | — |
Storage (storage audit) |
✅ | — |
Network (network audit) |
✅ | — |
Certs (certs) |
✅ | — |
Tags (tags) |
✅ | — |
Secrets (secrets scan) |
✅ | — |
Drift (drift) |
✅ | — |
Inventory (inventory) |
✅ | — |
Quota (quota) |
✅ | — |
Consolidated (audit) |
✅ | — |
Lambda policy (lambda audit) |
✅ | — |
RBAC (k8s rbac) |
— | ✅ |
Platform tenant (platform audit) |
✅† | ✅ |
Repository settings (repo audit) |
— | — |
✅ implemented · — not applicable
‡ Repository settings live only in GitHub — branch protection, required checks and
Dependabot state. No gate inside a repository can observe them, so a repo can carry a full CI
matrix, a protection rule that requires none of it, and nothing anywhere will say so. Read
through the gh CLI's existing credential, the same way the AWS commands use the ambient
credential chain. Compared against a committed expected-repo-settings.yaml; reported, never
enforced.
† Platform audit reads AWS IAM roles and EKS Pod Identity associations, plus Platform-tenant
cluster objects (namespace, ResourceQuota, NetworkPolicy, ServiceAccount) — not RBAC,
which is the separate k8s rbac command.
The offline commands — compliance, compare, baseline, report, remediate —
operate on saved scan reports or local state and are cloud-agnostic; mcp exposes the
AWS scanners over MCP/stdio for AI agents.
brew install nanohype/tap/cloudgovgo install github.com/nanohype/cloudgov@latestPre-built binaries for Linux, macOS, and Windows are attached to every GitHub release.
Archive names carry the version, so resolve it first — releases/latest/download/
needs the exact asset filename and has nothing to substitute into it.
# macOS arm64 example
VERSION=$(curl -sSL https://api.github.com/repos/nanohype/cloudgov/releases/latest \
| sed -n 's/.*"tag_name": *"v\{0,1\}\([^"]*\)".*/\1/p')
curl -sSL "https://github.com/nanohype/cloudgov/releases/download/v${VERSION}/cloudgov_${VERSION}_darwin_arm64.tar.gz" \
| tar -xz cloudgov
sudo mv cloudgov /usr/local/bin/Verify the download against the published SHA256 checksums:
curl -sSL "https://github.com/nanohype/cloudgov/releases/download/v${VERSION}/cloudgov_${VERSION}_checksums.txt" \
| sha256sum --check --ignore-missingRequires Go 1.26+ and Task.
git clone https://github.com/nanohype/cloudgov.git
cd cloudgov
task buildcloudgov uses the standard AWS SDK credential chain.
# Option 1 — environment variables
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_REGION=us-east-1
# Option 2 — named profile
export AWS_PROFILE=my-profile
export AWS_REGION=us-east-1
# Option 3 — IAM role / instance metadata (no env vars needed)Required IAM permissions for a read-only audit role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:List*",
"iam:Get*",
"cloudtrail:LookupEvents",
"ce:GetCostAndUsage",
"ec2:Describe*",
"elasticloadbalancing:Describe*",
"s3:ListAllMyBuckets",
"s3:GetBucketAcl",
"s3:GetBucketEncryption",
"s3:GetBucketVersioning",
"s3:GetBucketLogging",
"s3:GetBucketPublicAccessBlock",
"s3:GetBucketTagging",
"acm:ListCertificates",
"acm:DescribeCertificate",
"rds:DescribeDBInstances",
"rds:DescribeDBClusters",
"rds:DescribeDBSnapshots",
"rds:DescribeDBClusterSnapshots",
"lambda:ListFunctions",
"lambda:GetFunction",
"lambda:ListTags",
"lambda:GetAccountSettings",
"ecs:ListClusters",
"ecs:DescribeClusters",
"eks:ListClusters",
"eks:DescribeCluster",
"dynamodb:ListTables",
"dynamodb:DescribeTable",
"dynamodb:ListTagsOfResource",
"sns:ListTopics",
"sns:ListTagsForResource",
"sqs:ListQueues",
"sqs:ListQueueTags",
"iam:GetAccountSummary",
"servicequotas:GetServiceQuota",
"logs:DescribeLogGroups",
"events:ListRules",
"events:ListTagsForResource"
],
"Resource": "*"
}
]
}Compares granted permissions against CloudTrail activity over the lookback window and reports unused, admin, and cross-account risks.
The window it reports is the window it covered. --days is what you ask for;
the audit log behind the scan decides what can be answered. CloudTrail's Event
history holds 90 days of management events and returns those same 90 for a wider
request, so a scan asked for more is narrowed to what the source retains and every
finding states the covered period rather than the requested one. The JSON report
carries both as a window object — requested_days, observed_days, and
limited_by naming the provider that bounded it — so a consumer reads the window
instead of parsing it out of a sentence, and a narrowed window is also recorded as
an incomplete observation.
Retention belongs to the source, not to cloudgov: a provider reading an Athena-backed trail or CloudTrail Lake declares its own bound, and one that declares none is treated as unbounded. Over several providers the report carries the narrowest window any of them covered, because the envelope can only claim what all of them could answer for.
# Scan the current AWS account (90-day lookback)
cloudgov iam scan
# Last 30 days, show CRITICAL and HIGH only
cloudgov iam scan --days 30 --severity HIGH
# Scan a specific principal
cloudgov iam scan --principal arn:aws:iam::123456789012:role/scanner
# JSON output for downstream tooling
cloudgov iam scan --output json --output-file report.json
# SARIF output for GitHub Advanced Security
cloudgov iam scan --output sarif --output-file results.sarif
# Increase parallelism for large accounts
cloudgov iam scan --concurrency 20Flags
| Flag | Default | Description |
|---|---|---|
--days |
90 |
CloudTrail lookback window in days, narrowed to what the audit log retains |
--principal |
Scan a single principal by name or ID | |
--severity |
LOW |
Minimum severity to report: CRITICAL, HIGH, MEDIUM, LOW, INFO |
--output |
table |
Output format: table, json, sarif |
--output-file |
Write output to file instead of stdout | |
--concurrency |
10 |
Maximum parallel goroutines |
--profile |
AWS named profile to use for credentials |
Reads a JSON scan report and generates least-privilege Terraform policy files for each flagged principal.
It refuses a partial scan. A report and a fix do not get the same default. An
iam scan that could not see part of an account prints what it missed and leaves
the exit code to --fail-on, because reporting a partial view costs an operator a
re-read. A fix built from the same report costs them access that is in use: the
removals are computed from the permissions the scan observed, so every
permission it could not observe reads as unused. A short audit-log window is the
same problem — a permission last used before the window looks like one never used
at all.
So iam fix stops when the report's incomplete array is non-empty, writes
nothing, and names what was missed, what window the removals would have rested on,
and the two ways forward. Passing --accept-incomplete-scan generates the fix
anyway, and every generated file records that it was built from a partial scan —
the caveat travels with the artifact, because a fix file is reviewed in a pull
request by someone who never saw the command. Raw-policy output gets the caveat as
a file beside the policies, since a JSON policy has no comment syntax and anything
added to it stops being a policy.
# Generate fixes for all HIGH+ findings
cloudgov iam fix --from report.json
# Write fixes to a custom directory
cloudgov iam fix --from report.json --out ./tf-fixes
# Include MEDIUM severity fixes too
cloudgov iam fix --from report.json --severity MEDIUMWorkflow
cloudgov iam scan --output json --output-file report.json
cloudgov iam fix --from report.json --out ./fixes
ls ./fixes/
# minimal_lambda_executor.tf
# minimal_ci_deploy_role.tfFlags
| Flag | Default | Description |
|---|---|---|
--from |
(required) | Path to JSON report from cloudgov iam scan --output json |
--format |
terraform |
Output format: terraform, json |
--out |
./cloudgov-fixes |
Output directory for generated files |
--severity |
HIGH |
Minimum severity to generate fixes for |
--profile |
(default chain) | AWS named profile to use for credentials (match the profile used for the scan) |
--accept-incomplete-scan |
false |
Generate fixes from a scan that did not see everything it was asked to; the generated files record that they were |
Compares AWS spend between the last N days and the N days before that, surfacing unexpected increases service by service.
# Compare last 30 days vs the 30 days before
cloudgov cost diff
# 7-day comparison
cloudgov cost diff --days 7
# JSON output for alerting pipelines
cloudgov cost diff --output jsonFlags
| Flag | Default | Description |
|---|---|---|
--days |
30 |
Compare last N days vs N days before |
--threshold |
0 |
Only show services with >N% change (e.g. --threshold 20) |
--output |
table |
Output format: table, json |
--output-file |
Write output to file instead of stdout |
Cost increases >10% are shown in red; decreases are shown in green.
Finds unattached disks, reserved IPs with no instance, and idle load balancers (with
estimated monthly cost), plus cluster residue — resources tied to a now-deleted EKS
cluster that the cluster's teardown can't reach: the control-plane log group (which blocks
a same-named re-create) and Karpenter's interruption SQS queue + EventBridge rules. Residue
is matched against eks:ListClusters, so a live cluster's resources are never flagged, and
it's always reported regardless of --min-cost (it's a conflict, not a cost, problem).
It also finds stranded manual RDS snapshots. Deleting a database removes its automated
backups but never its manual snapshots, so a snapshot outlives the instance or cluster it
was taken from and keeps billing for storage with nothing left to restore onto.
CloudFormation and CDK make this routine: RemovalPolicy.SNAPSHOT takes a final manual
snapshot on every stack destroy, so a staging stack torn down ten times leaves ten of them.
Only manual snapshots are examined — automated ones are deleted with their database and
cannot outlive it. Aurora does not report a size for cluster snapshots (its storage is
elastic and billed on actual backup bytes), so those are reported with the size unknown
rather than as an estimate of $0.00.
# Scan the current AWS account
cloudgov orphans
# Only report resources costing more than $5/month
cloudgov orphans --min-cost 5
# JSON for Slack/PagerDuty integration
cloudgov orphans --output jsonFlags
| Flag | Default | Description |
|---|---|---|
--min-cost |
0 |
Only report orphans with monthly cost above this USD threshold |
--output |
table |
Output format: table, json |
--output-file |
Write output to file instead of stdout |
The table includes a TOTAL row summing all monthly costs.
Costs are estimates from on-demand list prices (e.g. ~$0.10/GB-month for gp2 EBS), not your billed actuals — they ignore volume type, region, and discounts. Use them to rank waste, not to reconcile a bill; see AWS Cost Explorer for actuals.
Audits object storage for public access, missing encryption, disabled versioning, and missing access logging.
# Scan the current AWS account
cloudgov storage audit
# HIGH and CRITICAL findings only
cloudgov storage audit --severity HIGH
# JSON for SIEM ingestion
cloudgov storage audit --output json --output-file storage-findings.jsonFlags
| Flag | Default | Description |
|---|---|---|
--severity |
LOW |
Minimum severity to report |
--output |
table |
Output format: table, json, sarif |
--output-file |
Write output to file instead of stdout | |
--fix |
false |
Generate shell remediation scripts for each finding |
--out |
. |
Directory to write fix scripts (used with --fix) |
Checks security groups for rules that expose sensitive ports to the internet.
Severity rules:
- CRITICAL —
0.0.0.0/0on SSH (22), RDP (3389), or database ports (3306, 5432, 1433, 27017, 6379, 9200) - HIGH —
0.0.0.0/0on any non-HTTP/HTTPS port - MEDIUM — unrestricted egress (all traffic to
0.0.0.0/0)
# Scan the current AWS account
cloudgov network audit
# Show CRITICAL findings only
cloudgov network audit --severity CRITICAL
# JSON output
cloudgov network audit --output json --output-file network-findings.json
# Generate shell remediation scripts alongside the table
cloudgov network audit --fix --out fixes/Flags
| Flag | Default | Description |
|---|---|---|
--severity |
LOW |
Minimum severity to report |
--output |
table |
Output format: table, json |
--output-file |
Write output to file instead of stdout | |
--fix |
false |
Generate shell remediation scripts for each finding |
--out |
. |
Directory to write fix scripts (used with --fix) |
Read a previously-saved JSON scan report and emit shell scripts that remediate each finding. The offline equivalent of <domain> audit --fix — useful when you want to review findings first, gate remediation behind code review, or apply a subset by severity.
Supported report types: storage, network, orphans. Reports are read from files written via --output json --output-file <path> on the corresponding scan command.
The report is input, not configuration. Each generated script is named after the provider on the findings it covers, so remediate refuses a report whose provider names a path rather than a file — a separator, a .. segment, or nothing at all — and refuses it before writing anything, naming the entry. It also refuses a name that is already a symlink, which would carry the write wherever the link points.
The scripts are written executable, so where a file lands is contained: no report can put one outside --out.
What is in the file is contained in one place and not the other, and the difference is the report type. For orphans, cloudgov composes every command from an allowlist of resource kinds, and report values reach the file only inside comment lines and quoted arguments — they do not become lines of their own. For storage and network, the report's remediation string is the command by design: the scan writes it, remediate copies it to the script verbatim, and a report that supplies that field supplies the line. Review a storage or network script before running it, and treat one from a source you do not control as you would any other script from that source.
What that does not cover: remediate refuses a symlink at the name it is about to write, and does not re-check the directories above it. Point --out at a directory you control.
# Generate fix scripts from a saved storage scan
cloudgov storage audit --output json --output-file storage.json
cloudgov remediate --type storage --from storage.json --out fixes/
# Same for network, only CRITICAL findings
cloudgov network audit --output json --output-file network.json
cloudgov remediate --type network --from network.json --severity CRITICAL --out fixes/Flags
| Flag | Default | Description |
|---|---|---|
--type |
(required) | Report type: storage, network, or orphans |
--from |
(required) | Path to JSON scan report |
--out |
. |
Directory to write fix scripts |
--severity |
LOW |
Minimum severity to include in fix scripts |
Lists TLS certificates from ACM that are expired or expiring soon.
Severity rules:
- CRITICAL — expired, or expiring within 7 days
- HIGH — expiring within 30 days
- MEDIUM — expiring within 60 days
- LOW — expiring within 90 days (default
--daysthreshold)
# Warn on certs expiring within 90 days (default)
cloudgov certs
# Only show certs expiring within 30 days
cloudgov certs --days 30
# CRITICAL and HIGH only
cloudgov certs --severity HIGH
# JSON output
cloudgov certs --output json --output-file certs.json
# SARIF for CI ingestion
cloudgov certs --output sarif --output-file certs.sarifFlags
| Flag | Default | Description |
|---|---|---|
--days |
90 |
Include certs expiring within this many days |
--severity |
LOW |
Minimum severity to report |
--output |
table |
Output format: table, json, sarif |
--output-file |
Write output to file instead of stdout |
Audits EC2 instances, S3 buckets, RDS databases, Lambda functions, ECS clusters, EKS clusters, DynamoDB tables, SNS topics, and SQS queues for missing required tags.
All findings are MEDIUM severity.
# Require owner, env, and cost-center tags
cloudgov tags --require owner,env,cost-center
# Gate on a published tagging standard's required AWS keys instead of typing them
cloudgov tags --standard-file resource-tagging.json
# Fail CI if anything is missing a required tag
cloudgov tags --standard-file resource-tagging.json --fail-on medium
# JSON output
cloudgov tags --require owner,env --output json --output-file tags.jsonThe tag policy comes from --require (ad-hoc) or --standard-file (a nanohype resource-tagging standard JSON). --require wins when both are set. Pair with the global --fail-on medium to gate CI (all findings are MEDIUM, so --fail-on medium exits non-zero on any missing required tag).
--standard-file reads both tiers the standard declares, and only it can: a rule that applies to some resource kinds and not others is not expressible as a list of keys.
- Required (
content.required_by_surface.aws) — the keys every resource carries. - Conditional (
content.conditional_requirements) — keys required only on certain kinds. The standard declares one:BackupPolicy, required on backup-eligible resources. A resource carrying all ten required keys and noBackupPolicyis never selected by the tag-matching backup plan, and nothing errors until a restore is attempted — so it is flagged here.
A conditional rule naming a kind cloudgov does not enumerate is reported as an incomplete observation rather than dropped, so the run states the coverage it has rather than the coverage the standard asks for. Aurora and EFS are in that position: there is no EFS auditor, and the RDS auditor paginates DescribeDBInstances, which does not return Aurora clusters. Run the command to see which kinds the standard you point it at leave unenforceable.
Flags
| Flag | Default | Description |
|---|---|---|
--require |
Comma-separated tag keys that must be present | |
--standard-file |
Path to a resource-tagging standard JSON; gates on its required keys and conditional rules | |
--severity |
MEDIUM |
Minimum severity to report |
--output |
table |
Output format: table, json |
--output-file |
Write output to file instead of stdout |
One of --require or --standard-file is required.
Inspects each AWS Lambda function's resource-based policy (lambda:GetPolicy) for patterns that grant invoke permission too widely. This is the resource-based counterpart to cloudgov iam scan — that one checks what identities can do from the inside; this one checks who can invoke into the function from the outside.
Severity rules:
- CRITICAL —
Principal: "*"orPrincipal: {"AWS": "*"}(anyone can invoke) - HIGH — cross-account principal in
Principal: {"AWS": "arn:..."}(a different account is allowed to invoke) - HIGH —
Principal: {"Service": "..."}withoutaws:SourceAccountoraws:SourceArncondition (confused-deputy risk) - HIGH —
Action: "*"orAction: "lambda:*"in any allow statement
Functions without a resource policy are silently skipped — they're only reachable via identity-based IAM, which the IAM scan already covers.
# Audit all Lambda resource policies in the current AWS account
cloudgov lambda audit
# CRITICAL only, JSON output
cloudgov lambda audit --severity CRITICAL --output json --output-file lambda.jsonFlags
| Flag | Default | Description |
|---|---|---|
--severity |
LOW |
Minimum severity to report |
--output |
table |
Output format: table, json, sarif |
--output-file |
Write output to file instead of stdout |
Scans cluster-scoped ClusterRoles and ClusterRoleBindings for the patterns that produce real incidents: wildcard verbs/resources, dangerous verbs (create/update/patch/delete) on wildcard resources, and bindings to broad subject groups (system:authenticated, system:unauthenticated, system:masters). Built-in default roles (cluster-admin, admin, edit, view, system:*, kubeadm:*) are skipped so the output focuses on user-introduced risk.
Connection uses the standard kubeconfig chain: --kubeconfig flag → $KUBECONFIG → ~/.kube/config → in-cluster service-account token.
# Scan the cluster of the current kubeconfig context
cloudgov k8s rbac
# Use a specific kubeconfig
cloudgov k8s rbac --kubeconfig /path/to/kubeconfig
# JSON output for CI
cloudgov k8s rbac --output json --output-file rbac.json
# HIGH and above only
cloudgov k8s rbac --severity HIGHFlags
| Flag | Default | Description |
|---|---|---|
--kubeconfig |
(chain) | Path to kubeconfig file |
--severity |
LOW |
Minimum severity to report |
--output |
table |
Output format: table, json, sarif |
--output-file |
Write output to file instead of stdout |
Scans Lambda environment variables, ECS task definitions, EC2 user data, and similar runtime configuration for embedded secrets — AWS keys, Slack tokens, private keys, GitHub tokens, generic high-entropy strings, and third-party cloud credentials (GCP service-account keys, Azure connection strings) leaked into AWS resources.
# Scan the current AWS account
cloudgov secrets scan
# HIGH and above
cloudgov secrets scan --severity HIGH
# SARIF output for GitHub Advanced Security
cloudgov secrets scan --output sarif --output-file secrets.sarifFlags
| Flag | Default | Description |
|---|---|---|
--severity |
LOW |
Minimum severity to report |
--output |
table |
Output format: table, json, sarif |
--output-file |
Write output to file instead of stdout |
Loads JSON scan reports from prior cloudgov runs and maps the findings to controls in a named benchmark, producing a pass/fail evaluation per control.
Available benchmarks: cis-aws-v3, soc2.
# Produce JSON reports first
cloudgov iam scan --output json --output-file iam.json
cloudgov storage audit --output json --output-file storage.json
# Then evaluate against a benchmark
cloudgov compliance cis-aws-v3 --iam-report iam.json --storage-report storage.json
# JSON output for ingest into a dashboard
cloudgov compliance soc2 --iam-report iam.json --output json --output-file soc2.jsonFlags
| Flag | Default | Description |
|---|---|---|
--iam-report |
Path to JSON report from iam scan |
|
--storage-report |
Path to JSON report from storage audit |
|
--network-report |
Path to JSON report from network audit |
|
--certs-report |
Path to JSON report from certs |
|
--tags-report |
Path to JSON report from tags |
|
--output |
table |
Output format: table, json, sarif |
--output-file |
Write output to file instead of stdout |
Reads a terraform.tfstate file and checks each managed resource against the AWS API to detect modifications or deletions outside Terraform. Supports security groups, IAM policies, and S3 buckets.
# Local state file
cloudgov drift terraform.tfstate
# Filter to a single resource type
cloudgov drift terraform.tfstate --resource-type aws_security_group
# Lower concurrency
cloudgov drift terraform.tfstate --concurrency 5
# JSON output
cloudgov drift terraform.tfstate --output json --output-file drift.jsonFlags
| Flag | Default | Description |
|---|---|---|
--resource-type |
Filter to a single Terraform resource type | |
--concurrency |
10 |
Max concurrent API calls |
--output |
table |
Output format: table, json, sarif |
--output-file |
Write output to file instead of stdout |
Runs all security and cost scans (IAM, storage, network, orphans, certs, tags, secrets) in one shot and produces a single combined report. Skip specific domains with --skip.
# Full audit of the current AWS account
cloudgov audit
# Skip IAM and certs domains
cloudgov audit --skip iam,certs
# HIGH and CRITICAL findings only, JSON output
cloudgov audit --severity HIGH --output json --output-file audit.json
# SARIF output for GitHub Advanced Security
cloudgov audit --output sarif --output-file audit.sarif
# Custom thresholds
cloudgov audit --iam-days 30 --cert-days 60 --require-tags owner,envFlags
| Flag | Default | Description |
|---|---|---|
--skip |
Domains to skip: iam, storage, network, orphans, certs, tags, secrets |
|
--severity |
LOW |
Minimum severity to report |
--output |
table |
Output format: table, json, sarif |
--output-file |
Write output to file instead of stdout | |
--iam-days |
90 |
IAM audit log lookback period in days |
--cert-days |
90 |
Certificate expiry warning threshold in days |
--require-tags |
Required tags for tag audit (comma-separated) | |
--concurrency |
10 |
Max parallel goroutines for IAM scanning |
--sink |
Notification sink (repeatable). See Notification sinks below. | |
--report-url |
URL embedded in sink notifications (link to full report) |
cloudgov audit --sink <spec> posts a digest of the run to an external system after the scan completes. Sinks fire on a best-effort basis — one bad sink does not block the others, and audit exit code is unaffected. The flag is repeatable, so you can deliver to several destinations at once.
| Spec form | What it does |
|---|---|
slack:<webhook-url> |
Block Kit message with severity-coded header, per-domain summary, top 10 findings, and optional report link |
webhook:<url> |
POSTs the raw JSON digest to any URL; receivers parse it however they like |
pagerduty:<routing-key> |
PagerDuty Events API v2 trigger — only fires when the digest contains at least one critical or high finding (avoids alert fatigue) |
# Post a Slack notification on every audit run
cloudgov audit --sink slack:https://hooks.slack.com/services/T00/B00/XXX
# Page on-call AND notify Slack AND forward to a custom collector
cloudgov audit \
--sink slack:https://hooks.slack.com/services/T00/B00/XXX \
--sink pagerduty:my-pd-routing-key \
--sink webhook:https://collector.example.com/cloudgov \
--report-url https://reports.example.com/audit-$(date +%F).htmlLists all AWS resources with type, region, tags, and creation date. Groups by type and region for a complete asset overview.
# List all resources in the current AWS account
cloudgov inventory
# Filter to specific resource types
cloudgov inventory --type ec2,s3,lambda
# JSON output
cloudgov inventory --output json --output-file inventory.jsonFlags
| Flag | Default | Description |
|---|---|---|
--type |
all | Resource types to list (e.g. ec2, s3, lambda) |
--output |
table |
Output format: table, json |
--output-file |
Write output to file instead of stdout |
Checks AWS service quota usage to prevent outages from silently hitting limits. Reports IAM, EC2, S3, Lambda, and RDS quotas.
# All quotas
cloudgov quota
# Only quotas above 50% utilization
cloudgov quota --threshold 50
# JSON output
cloudgov quota --output json --output-file quotas.jsonFlags
| Flag | Default | Description |
|---|---|---|
--threshold |
0 |
Minimum utilization percentage to report |
--output |
table |
Output format: table, json |
--output-file |
Write output to file instead of stdout |
Utilization is color-coded: green (<50%), yellow (50-80%), red (>80%).
Save any scan report as a named baseline for later comparison with cloudgov compare.
# Save a baseline from a scan report
cloudgov iam scan --output json --output-file scan.json
cloudgov baseline save --from scan.json --name pre-deploy
# List saved baselines
cloudgov baseline list
# Delete a baseline
cloudgov baseline delete --name old-scanBaselines are stored in ~/.cloudgov/baselines/.
Subcommands
| Subcommand | Description |
|---|---|
baseline save --from <file> --name <name> |
Save a report as a named baseline |
baseline list |
List all saved baselines with dates |
baseline delete --name <name> |
Delete a saved baseline |
Compares two scan reports (or a saved baseline against a current report) and classifies each finding as new, resolved, or unchanged.
# Compare a saved baseline against a new scan
cloudgov compare --baseline pre-deploy --current scan-after.json
# Compare two report files directly
cloudgov compare --from old-report.json --to new-report.json
# JSON output
cloudgov compare --from old.json --to new.json --output jsonFlags
| Flag | Default | Description |
|---|---|---|
--baseline |
Name of saved baseline to compare against | |
--current |
Path to current report JSON file | |
--from |
Path to older report JSON file | |
--to |
Path to newer report JSON file | |
--output |
table |
Output format: table, json |
--output-file |
Write output to file instead of stdout |
Use --baseline + --current or --from + --to (not both). Supports all report types: audit, IAM, storage, network, orphans, certs, tags, secrets, quotas.
End-to-end workflow
# Before a deploy: scan and save a baseline
cloudgov audit --output json --output-file audit.json
cloudgov baseline save --from audit.json --name pre-deploy-v2
# After the deploy: scan again and compare
cloudgov audit --output json --output-file audit-after.json
cloudgov compare --baseline pre-deploy-v2 --current audit-after.json
# Output shows:
# +NEW findings introduced since the baseline
# -RESOLVED findings that no longer appear
# =UNCHANGED findings present in bothYou can also skip baselines and compare any two JSON files directly:
cloudgov compare --from monday-scan.json --to friday-scan.json --output jsonAudits every Platform CR in a cluster against the eks-agent-platform contract:
the tenant namespace and its restricted Pod Security Standards label, the
ResourceQuota and LimitRange, the default-deny tenant-egress policy (as a
NetworkPolicy or a CiliumNetworkPolicy, whichever the cluster's network engine
uses), the tenant ServiceAccount and the EKS Pod Identity association that binds
it, and the tenant IAM role's trust policy, generated inline policies and
permissions boundary.
The boundary is checked in two parts, because only one of them can always be
answered. A tenant role carrying no boundary is reported at CRITICAL on its own
evidence: the boundary is the ceiling the rest of the tenant identity model rests
on, and without it the next policy attached to the role is bounded by nothing. A
boundary that is present is compared against the ARN the Platform publishes on
status.permissionsBoundaryArn; where the Platform publishes none, there is
nothing to compare against and the run records the boundary as unchecked rather
than reporting the tenant conformant — a different boundary is a different
ceiling, and presence alone does not tell the two apart.
The cluster half needs a kubeconfig; the tenant-role and Pod Identity half needs AWS credentials. Absent AWS credentials do not fail the run — the cluster-side checks still execute — but they are recorded as an incomplete observation, because skipping a class of conformance checks is not the same as passing them. Any check the run could not perform lands the same way, so a tenant nobody was allowed to inspect never reads as a tenant that conforms.
# Audit every Platform in the current context
cloudgov platform audit
# A specific cluster, gating a merge on real breaches
cloudgov platform audit --kubeconfig /path/to/kubeconfig --fail-on HIGH
# SARIF for GitHub Advanced Security
cloudgov platform audit --output sarif --output-file platform.sarifFlags
| Flag | Default | Description |
|---|---|---|
--kubeconfig |
(chain) | Path to kubeconfig file |
--severity |
LOW |
Minimum severity to report |
--output |
table |
Output format: table, json, sarif |
--output-file |
Write output to file instead of stdout |
Branch protection, required status checks and Dependabot state live only in
GitHub, so no gate inside a repository can observe them: a repository can carry a
full CI matrix, a protection rule that requires none of it, and nothing anywhere
says so. This compares an organization's repositories against the committed
expected-repo-settings.yaml and reports the differences. It never changes a
setting.
Reads through the gh CLI, so it uses whatever credentials gh auth status
reports. --org is required: a default would not save a keystroke, it would pick
a target.
# Audit an organization against the committed expected shape
cloudgov repo audit --org my-org
# A different expected shape
cloudgov repo audit --org my-org --expected ./ci/repo-settings.yaml
# JSON for a dashboard
cloudgov repo audit --org my-org --output json --output-file repos.jsonFlags
| Flag | Default | Description |
|---|---|---|
--org |
(required) | GitHub organization to audit |
--expected |
expected-repo-settings.yaml |
Path to the committed expected settings |
--severity |
LOW |
Minimum severity to report |
--output |
table |
Output format: table, json |
--output-file |
Write output to file instead of stdout |
Runs cloudgov as a Model Context Protocol server over stdio, exposing each
scanner as a tool that returns the same JSON report the CLI emits with
--output json. Register it once:
claude mcp add --transport stdio cloudgov -- cloudgov mcpThe server is read-only and takes no flags — credentials resolve through the same
AWS SDK and kubeconfig chains the CLI uses. There is no exit code over MCP, so
the incomplete array in each response is the only carrier of "this scan could
not see everything it was asked to".
One parameter is narrower than its CLI equivalent: a kubeconfig path supplied
through a tool call may not authenticate by running an exec credential plugin,
because the plugin's command line comes from the file and a tool argument must
not choose which binary runs. Omit it to use the server's own kubeconfig chain.
See AGENTS.md for the full tool table and parameters.
Generates a standalone, self-contained HTML report from any JSON scan output. Includes summary cards, severity breakdown, domain-specific tables, and client-side table sorting. Supports light and dark mode via prefers-color-scheme.
# Generate from an audit report
cloudgov audit --output json --output-file audit.json
cloudgov report --from audit.json --out report.html --open
# Generate from any scan report
cloudgov report --from iam-scan.json --out iam-report.html
# Explicit type override
cloudgov report --from data.json --type orphans --out orphans.htmlFlags
| Flag | Default | Description |
|---|---|---|
--from |
(required) | Path to scan report JSON file |
--out |
report.html |
Output HTML file path |
--type |
auto |
Report type: auto, audit, iam, storage, network, orphans, certs, tags, secrets, cost, quotas |
--open |
false |
Open the report in the default browser after generation |
| Flag | Description |
|---|---|
--regions |
Regions to scan for regional resources. Defaults to every region enabled for the account |
--quiet, -q |
Suppress all progress and summary output on stderr (for scripts) |
--fail-on |
Exit 2 if any finding is at or above this severity |
--version |
Print version, commit hash, and build date |
Regional scans cover every region enabled for the account. AWS regional APIs answer only for the region their client is bound to, so a scan run against one region describes one region — and nothing in a report drawn that way says so. An empty result would read as "the account is clean" when it means "the configured region is clean".
Narrow it when you know where to look:
cloudgov orphans --regions us-west-2,us-east-1Global services — IAM, Cost Explorer, the S3 bucket list — are scanned once
regardless, and their findings are stamped global. A bucket's own region comes
from the bucket, since ListBuckets is account-global and returns no location.
If the region set cannot be discovered, the scan narrows to the configured
region and records that as an incomplete observation, so a one-region result is
never presented as an account sweep. With --fail-on set, that run exits 3.
drift is not fanned out. It compares named resources against a Terraform state
file you supply, so its scope is that file's, and querying every region for one
resource ID would return not-found from all the others.
| Code | Meaning |
|---|---|
0 |
Clean — the scan saw everything it was asked to and found nothing at or above --fail-on |
1 |
Command error — bad flags, unreadable file, no credentials, an API call that failed outright |
2 |
A finding met or exceeded --fail-on |
3 |
The scan could not observe everything it was asked to, so the result is not evidence either way |
Exit 2 and 3 require --fail-on. Passing it is what declares the run a gate;
without it a run is informational and exits 0 or 1 only. Incomplete
observations are still reported on stderr and in the JSON incomplete array
either way — --quiet silences the stderr copy, not the record.
Exit 3 exists because 0 gets read as evidence for approval. A denied
GetBucketVersioning looks exactly like a versioned bucket if the error is
dropped, so a scan run with partial permissions has to be distinguishable from
one that found nothing.
Every command that can observe less than it was asked to honours this —
audit, iam scan, storage audit, network audit, certs, tags,
secrets scan, orphans, quota, inventory, cost diff, drift,
lambda audit, platform audit, repo audit and compliance. The last two
read no cloud account and still qualify: repo audit reports a GitHub probe that
did not answer, and compliance carries forward whatever the scan reports it
loads could not read, because a control evaluated over a partial account is not
an evaluated control.
Commands that read no cloud account (report, baseline, remediate) exit
0/1/2 only, as does k8s rbac — its two reads return an error rather than
a short list, so there is no partial state to report.
compare reads no account either and still honours the contract, because its
inputs carry one. A finding the baseline saw and the current run could not read
is RESOLVED by arithmetic and unobserved in fact, and RESOLVED is the answer
an operator acts on by closing the ticket. Both inputs' incomplete arrays travel
with the result, labelled by which side they came from.
Upload IAM findings to GitHub Advanced Security (requires security-events: write permission):
name: cloudgov security scan
on:
schedule:
- cron: '0 6 * * 1' # every Monday at 06:00 UTC
workflow_dispatch:
permissions:
security-events: write
jobs:
iam-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cloudgov
env:
# Pinned rather than floating to latest: a scan that changes verdict
# because a release landed mid-week is a scan you cannot act on.
CLOUDGOV_VERSION: 2.0.0
run: |
curl -sSL "https://github.com/nanohype/cloudgov/releases/download/v${CLOUDGOV_VERSION}/cloudgov_${CLOUDGOV_VERSION}_linux_amd64.tar.gz" \
| tar -xz cloudgov
sudo mv cloudgov /usr/local/bin/
- name: Run IAM scan
env:
AWS_ROLE_ARN: ${{ secrets.CLOUDGOV_ROLE_ARN }}
AWS_REGION: us-east-1
run: |
cloudgov iam scan \
--severity HIGH \
--output sarif \
--output-file results.sarif \
--quiet
- name: Upload SARIF to GitHub Security
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarifRun a unified audit across all domains in CI:
name: cloudgov full audit
on:
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:
permissions:
security-events: write
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cloudgov
env:
# Pinned rather than floating to latest: a scan that changes verdict
# because a release landed mid-week is a scan you cannot act on.
CLOUDGOV_VERSION: 2.0.0
run: |
curl -sSL "https://github.com/nanohype/cloudgov/releases/download/v${CLOUDGOV_VERSION}/cloudgov_${CLOUDGOV_VERSION}_linux_amd64.tar.gz" \
| tar -xz cloudgov
sudo mv cloudgov /usr/local/bin/
- name: Run full audit
env:
AWS_ROLE_ARN: ${{ secrets.CLOUDGOV_ROLE_ARN }}
AWS_REGION: us-east-1
run: |
cloudgov audit \
--severity HIGH \
--output sarif \
--output-file audit.sarif \
--quiet
- name: Upload SARIF to GitHub Security
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: audit.sarifcloudgov:
image: ubuntu:24.04
variables:
# Pinned rather than floating to latest: a scan that changes verdict
# because a release landed mid-week is a scan you cannot act on.
CLOUDGOV_VERSION: "2.0.0"
before_script:
- curl -sSL "https://github.com/nanohype/cloudgov/releases/download/v${CLOUDGOV_VERSION}/cloudgov_${CLOUDGOV_VERSION}_linux_amd64.tar.gz"
| tar -xz cloudgov
- mv cloudgov /usr/local/bin/
script:
- cloudgov iam scan --output json --output-file report.json --quiet
- cloudgov storage audit --severity HIGH --output json --output-file storage.json --quiet
artifacts:
paths:
- report.json
- storage.json
expire_in: 30 days# Exit non-zero if any CRITICAL findings exist
cloudgov iam scan --severity CRITICAL --output json --quiet | \
jq -e '.findings | length == 0'| Format | Flag | Use case |
|---|---|---|
| Table | --output table |
Human-readable terminal output with colors |
| JSON | --output json |
Scripts, alerting, dashboards |
| SARIF | --output sarif |
GitHub Advanced Security, IDE integrations |
All formats can be written to a file with --output-file path/to/file.
cloudgov --version
# cloudgov version v0.1.0 (commit abc1234, built 2026-03-01T12:00:00Z)