# greenops-scan

Cloud Carbon & Cost Scanner — identify waste, cut carbon, save money.

Scans your AWS infrastructure using read-only APIs and generates actionable reports with cost and carbon savings estimates.

[📚 Full documentation & GreenOps Cloud](https://greenops.cloud/scan?utm_source=github&utm_medium=readme&utm_campaign=greenops_launch_2026q3&utm_content=cli_readme_top)

<a href="https://asciinema.org/a/yGvr3oJWCO66yf1r" target="_blank" rel="noreferrer">
  <img src="https://asciinema.org/a/yGvr3oJWCO66yf1r.png" alt="GreenOps Scan demo" width="100%">
</a>

## Quick Start

```bash
npx greenops-scan
```

> 🤖 **Using this from an AI agent?** See [`SKILL.md`](./SKILL.md) for the full
> non-interactive invocation guide, every flag/env var, the `--json` schema field-by-field,
> and an algorithm for ranking findings by dollar/carbon impact.

### Run with Docker

No local Node.js install needed — pull the image and mount your AWS credentials:

```bash
docker run --rm -it \
  -v ~/.aws:/home/greenops/.aws:ro \
  -v "$(pwd)/reports:/reports" \
  -e AWS_PROFILE=default \
  ghcr.io/spidgorny/greenops-scan:latest --region us-east-1
```

- `-v ~/.aws:/home/greenops/.aws:ro` shares your AWS CLI credentials/config read-only (the
  container runs as a non-root `greenops` user, so the home directory is `/home/greenops`, not
  `/root`).
- `-v "$(pwd)/reports:/reports"` maps the container's working directory to a host folder so
  `--save-report` (JSON) and PDF report files land where you can see them.
- Swap `AWS_PROFILE` for the profile you want to scan, or use `-e AWS_ACCESS_KEY_ID=... -e AWS_SECRET_ACCESS_KEY=...` instead.

Non-interactive/CI example, matching the [scripting conventions](#scripting-json-on-stdout-diagnostics-on-stderr) below:

```bash
docker run --rm \
  -v ~/.aws:/home/greenops/.aws:ro \
  -v "$(pwd)/reports:/reports" \
  -e AWS_PROFILE=default \
  ghcr.io/spidgorny/greenops-scan:latest \
  --region us-east-1 --format json --quiet --save-report
```

Pin a specific release instead of `latest` with `ghcr.io/spidgorny/greenops-scan:0.1.33`.

## What It Scans

| Module | What it detects |
|--------|----------------|
| **EC2** | Idle instances, old-gen Intel → Graviton upgrades, unattached EBS volumes |
| **S3** | Missing lifecycle policies, version accumulation, large un-tiered buckets |
| **ECS** | Empty clusters, over-provisioned Fargate (validated via 30-day CloudWatch metrics), optional weekend scale-to-zero for non-prod services |
| **EKS** | Outdated K8s versions, non-Graviton node groups, no autoscaler, versions nearing/in paid Extended Support |
| **Lambda** | Zombie functions, legacy runtimes, x86→ARM, over-provisioned memory, runtimes nearing/past end of security-patch support |
| **RDS** | Idle databases, Graviton upgrades, Multi-AZ on non-prod, old snapshots, engine versions nearing/in paid Extended Support (MySQL, PostgreSQL, Aurora, DocumentDB) |
| **ElastiCache** | Idle caches, Graviton upgrades, Redis versions nearing/in paid Extended Support |
| **CloudFront** | Unused distributions, missing HTTPS redirect, outdated TLS |
| **EBS Snapshots** | Old snapshots, unused AMIs |
| **Bedrock** | Idle provisioned throughput, unused custom models, stale agents, empty knowledge bases |
| **VPC Endpoints** | NAT Gateways paying data processing fees for S3/DynamoDB traffic that should use a free Gateway Endpoint |
| **OpenSearch** | Domain engine versions nearing/in paid Extended Support (legacy Elasticsearch/OpenSearch versions) |

For the architecture of scanner modules and a step-by-step guide to adding a new module, see 
[`docs/scanner-modules.md`](https://github.com/spidgorny/greenops-scan/docs/scanner-modules.md).

## Features

- Interactive TUI with arrow-key profile and region selection
- Non-interactive, pipe-friendly JSON output for scripts and CI
- IAM permission checking per module
- Parallel scanning with live progress display
- Multi-region scanning in a single run
- AWS Cost Explorer context and realism filters for savings estimates
- Carbon calculations using Green Software Foundation methodology
- Date-aware deprecation detection — flags Lambda runtimes, RDS/Aurora/DocumentDB engine versions, EKS versions, ElastiCache Redis versions, and OpenSearch/Elasticsearch domain versions that are nearing or already past AWS's standard support end date, with real Extended Support surcharge cost estimates
- JSON report export
- Branded PDF report generation
- Modular architecture — ready for GCP/Azure providers

## CLI Options

```
npx greenops-scan [options]

Selection (skip interactive prompts):
  -p, --provider <name>    Cloud provider: aws, gcp                 [GREENOPS_PROVIDER]
      --profile <name>     Cloud profile/project name               [GREENOPS_PROFILE, AWS_PROFILE]
  -r, --region <codes>     Region code(s), comma-separated          [GREENOPS_REGION, AWS_REGION]
  -m, --modules <names>    Scanner modules to run, comma-separated  [GREENOPS_MODULES]
      --non-interactive    Never prompt; fail if a value is missing [GREENOPS_NON_INTERACTIVE]

Output:
      --format <format>    human | json | fix-commands              [GREENOPS_FORMAT]
      --json               Shorthand for --format json
      --fix-commands       Shorthand for --format fix-commands
  -q, --quiet              Suppress non-essential stderr output     [GREENOPS_QUIET]
  -v, --verbose            Extra diagnostics on stderr              [GREENOPS_VERBOSE]
      --save-report        Also write the JSON report file (machine formats)
      --no-pdf             Skip PDF report generation
      --llm-prompts        Print a copy/paste LLM prompt under each finding (human format only) [GREENOPS_LLM_PROMPTS]
                           asks your coding agent to fix it in your IaC (Terraform/CDK/CloudFormation/etc.) instead of the live resource

Filters:
  -s, --severity <levels>  Comma-separated: critical,high,medium,low [GREENOPS_SEVERITY]
      --min-cost <amount>  Minimum monthly savings (USD) to include  [GREENOPS_MIN_COST]

Cached mode:
      --cached             Skip cloud introspection; reuse the latest saved JSON report from the current folder [GREENOPS_CACHED]
                           (in this mode, --modules also filters findings by module, e.g. --cached --modules rds,ec2)

Other:
      --weekend-scale-down Suggest scaling non-prod ECS/Fargate services to zero on weekends [GREENOPS_WEEKEND_SCALE_DOWN]
      --endpoint-url <url> Custom API endpoint (e.g. LocalStack)     [GREENOPS_ENDPOINT_URL]
  -h, --help               Show help
```

Every flag has an environment variable equivalent (shown in brackets), so scans can
be configured entirely from CI settings. Module names: `ec2`, `s3`, `ecs`, `eks`,
`lambda`, `rds`, `elasticache`, `cloudfront`, `ebs-snapshots`, `bedrock`.

**Examples:**
```bash
# Fully non-interactive scan
npx greenops-scan --provider aws --profile production --region eu-west-1

# Two regions, EC2 and RDS only
npx greenops-scan --profile production --region eu-west-1,us-east-1 --modules ec2,rds

# Show only high-impact findings
npx greenops-scan --severity high,critical

# Show findings with at least $100 in estimated monthly savings
npx greenops-scan --min-cost 100

# Combine options
npx greenops-scan --profile dev --region us-east-1 --severity medium,high,critical

# Suggest weekend scale-to-zero for non-prod ECS/Fargate services
npx greenops-scan --profile dev --region eu-west-1 --weekend-scale-down

# Re-filter a previous scan without touching AWS again
npx greenops-scan --profile dev --save-report --severity critical
npx greenops-scan --cached --severity critical --min-cost 50
```

## Cached mode: filter without re-scanning

`--cached` skips cloud introspection entirely and re-applies `--severity`/`--min-cost`
filters against the most recently saved JSON report (`greenops-*.json`) in the current
folder. This is handy when you want to slice the same scan several different ways —
by severity, by minimum monthly savings, or by output format — without waiting on AWS
API calls or spending Cost Explorer requests again.

- Requires a report file already saved via `--save-report` (the default for `human`
  format runs). If none is found, the scan exits with a clear error (code `3`).
- The most recently modified `greenops-*.json` file in the working directory is used.
- In cached mode, `--modules` filters findings by module instead of selecting which
  scanners run (there's no live scan to select modules for).
- Billing percentages are recomputed against the cached report's own stored monthly/
  annual bill total, so the percentages stay meaningful even for a filtered subset.
- Works with every output format (`human`, `json`, `fix-commands`) and honors `--no-pdf`
  the same way a normal scan does.

```bash
npx greenops-scan --cached --severity critical
npx greenops-scan --cached --format json --min-cost 100
npx greenops-scan --cached --modules rds,elasticache
```

## Scripting: JSON on stdout, diagnostics on stderr

The scanner is safe to use in a pipeline:

- `--json` (or `--format json`) prints the **full report as a single JSON document to
  stdout and nothing else** — no banner, no table, no prompts.
- Progress spinners, the live module progress display, filter notes, warnings and all
  other human-facing text go to **stderr**, so they never contaminate the data stream.
  Capture them with `2>` or silence them with `--quiet`.
- When stdout is **not a TTY** (i.e. you piped or redirected it), `--format json` is the
  default and interactive prompts are disabled automatically.
- In non-interactive mode the profile/region are resolved from flags, then environment
  variables, then the values remembered from your last interactive run. If none of them
  resolve, the scan exits with code `2` instead of prompting.
- Machine formats do not write report files or open a PDF. Add `--save-report` if you
  also want the JSON report file on disk.
- This applies to the default `human` format too: the banner, tables and summary are
  written to stderr, so `greenops-scan > log.txt` captures nothing — use `2> log.txt`
  (or `&> log.txt`) to keep a transcript.

```bash
# Findings as JSON, progress on the terminal, data into jq
npx greenops-scan --profile production --region eu-west-1 --json | jq '.summary'

# Fully silent except the JSON document
npx greenops-scan --profile production --region eu-west-1 --json --quiet > report.json
```

### JSON schema

The document is versioned via `schemaVersion` (currently `"1.0"`); it is only bumped on
breaking changes, so automation can pin against it.

```jsonc
{
  "schemaVersion": "1.0",
  "timestamp": "2026-08-14T05:13:00.000Z", // ISO-8601 scan start
  "provider": "aws",
  "account": "123456789012",               // account ID
  "accountAlias": "acme-production",       // IAM account alias, or profile name
  "region": "eu-west-1",                   // primary region (first of `regions`)
  "regions": ["eu-west-1", "us-east-1"],   // all regions covered by the scan
  "findings": [
    {
      "module": "ec2",                     // scanner module / service
      "resourceId": "i-0abc123",           // resource identifier or ARN
      "resourceType": "EC2 Instance",
      "region": "eu-west-1",               // region the resource lives in
      "issue": "Idle instance — avg CPU 0.4% over 30 days",
      "severity": "critical",              // critical | high | medium | low
      "monthlySavingsUsd": 62.05,          // estimated monthly cost saving
      "monthlyCarbonSavingsKg": 8.31,      // estimated monthly CO2 saving
      "savingsConfidence": "high",         // high | medium | low
      "recommendation": "Consider stopping/terminating or right-sizing",
      "metadata": { "instanceType": "m5.large", "avgCpuPercent": 0.4 },
      "tags": { "Project": "checkout", "Environment": "production" } // best-effort, AWS only — see below
    }
  ],
  "summary": {
    "totalFindings": 12,
    "totalMonthlySavingsUsd": 412.9,
    "totalMonthlyCarbonSavingsKg": 55.1,
    "totalAnnualSavingsUsd": 4954.8,
    "totalAnnualCarbonSavingsKg": 661.2,
    "moduleBreakdown": [
      { "module": "ec2", "findingCount": 4, "monthlySavingsUsd": 190.2, "monthlyCarbonSavingsKg": 21.4 }
    ]
  },
  "billing": {                             // AWS Cost Explorer context (AWS only)
    "periodStart": "2026-07-01",
    "periodEnd": "2026-07-31",
    "monthlyCostUsd": 8231.44,
    "annualCostUsd": 98777.28,
    "monthlySavingsPercentage": 5.0,
    "annualSavingsPercentage": 5.0,
    "serviceCosts": [{ "service": "Amazon EC2", "monthlyCostUsd": 3120.5 }],
    "moduleCosts": [{ "module": "ec2", "monthlyCostUsd": 3120.5, "confidence": "high" }]
  }
}
```

Notes:

- The JSON document does not carry remediation commands; it includes top-level
  `remediationHint` and `upgradeUrl` fields instead. Use `--fix-commands` to get the
  commands themselves.
- `metadata` is module-specific and intentionally unstable — do not rely on its keys.
- `tags` is a best-effort map of the underlying AWS resource's tags (e.g. `Project`,
  `Environment`, `Team`, or `aws:cloudformation:stack-name` for CloudFormation-managed
  resources). It's populated via the `ResourceGroupsTaggingAPI` — already covered by the
  standard `ReadOnlyAccess` policy, so no extra IAM permissions are needed. It's omitted
  when the resource has no tags, tag fetching fails for any reason, or the resource type
  isn't supported yet (currently: EC2 instances/volumes, RDS instances, ElastiCache
  clusters, Lambda functions, EKS clusters, OpenSearch domains, S3 buckets, CloudFront
  distributions). Never blocks or fails the scan.
- All monetary values are USD, all carbon values are kg CO₂e.

### Fix commands

`--fix-commands` skips the report entirely and prints the remediation `aws` CLI
commands, one per line, with context as shell comments:

```bash
npx greenops-scan --profile production --region eu-west-1 --fix-commands > fixes.sh
```

```sh
# [critical] ec2/vol-0abc123 (eu-west-1): Unattached gp2 volume (100 GB) — pure waste
#   est. $10.00/mo, 0.02 kg CO2/mo (confidence: medium)
#   DESTRUCTIVE — review before running: This will permanently delete the volume and all data on it.
#   backup: aws ec2 create-snapshot --volume-id vol-0abc123 --description "Backup before deletion"
aws ec2 delete-volume --volume-id vol-0abc123
```

The scanner **never executes remediation commands** — savings are estimates and the
commands can be destructive, so they are emitted for human review only.

### LLM fix prompts (for teams on Terraform/CDK/CloudFormation/etc.)

Running `--fix-commands` against a live resource works, but if that resource is
managed by Terraform, CDK, CloudFormation, Pulumi, or any other IaC, changing it
directly with the AWS CLI causes drift — the next `terraform apply`/`cdk deploy`
either reverts your fix or fights with it.

`--llm-prompts` solves this differently: it adds a copy/paste-ready prompt under
each finding in the default `human` output, telling your coding agent (GitHub
Copilot, Claude, Cursor, or any tool-calling LLM with repo access) to find the
resource in your actual IaC source and make the equivalent change there — showing
you a diff before anything gets applied.

```bash
npx greenops-scan --profile production --region eu-west-1 --severity critical --llm-prompts
```

```
  - severity: CRITICAL
    module: RDS
    resource: prod-orders-db
    resourceType: RDS Instance
    region: eu-west-1
    tags:
      Project: checkout
      Environment: production
      aws:cloudformation:stack-name: OrdersDbStack
    issue: Idle database — avg CPU 4.3% over 14 days
    costSavings: $49.64/mo
    confidence: MED
    co2Savings: 1.48 kg/mo
    recommendation: Consider stopping, downsizing, or consolidating this database
    ----- COPY BELOW: LLM fix prompt -----
    Fix this AWS cost/carbon finding in my infrastructure as code (Terraform, AWS CDK,
    CloudFormation, Pulumi, Serverless Framework, or whatever this repo uses) — do not
    change the live resource via the AWS CLI or console, make the change in code so it
    stays in sync with the rest of the stack:

    Resource: RDS Instance "prod-orders-db" (region: eu-west-1)
    Module: rds
    Issue: Idle database — avg CPU 4.3% over 14 days
    Recommendation: Consider stopping, downsizing, or consolidating this database
    Estimated savings: $49.64/mo ($595.68/yr), 1.48 kg CO2/mo
    Context:
      - avgCpuPercent: 4.3
      - instanceClass: "db.t3.medium"
      - engine: "postgres"
    Resource tags (use these to help locate the right stack/module):
      - Project: checkout
      - Environment: production
      - aws:cloudformation:stack-name: OrdersDbStack

    Please:
    1. Search this repository for where "prod-orders-db" (or a matching tag/logical
       name) is defined in infrastructure-as-code.
    2. Apply the change described in the recommendation above, keeping all other
       configuration unchanged.
    3. Show me the diff before applying/deploying anything — do not run
       `terraform apply`, `cdk deploy`, or equivalent without my review.
    4. If you can't find this resource in the codebase, tell me instead of guessing.
    ----- COPY ABOVE -----
```

Off by default to keep normal output compact — enable it whenever you're about to sit
down and fix findings rather than just triage them. Works alongside `--cached`,
`--severity`, `--min-cost`, and `--modules`, so you can filter down to exactly the
findings you want prompts for.

### Exit codes

| Code | Meaning |
|------|---------|
| `0` | Scan completed, no findings (or an interactive run finished successfully) |
| `1` | Scan completed and findings are present (non-interactive runs only) |
| `2` | Usage/configuration error (bad flag, unknown region/module, unresolvable profile) |
| `3` | Runtime error (invalid credentials, no accessible modules, unexpected failure) |

Interactive TTY runs always exit `0` on success so the TUI stays friendly; scripts get the
findings-based code because piping/redirecting stdout (or `--non-interactive`) turns the
findings signal on.

### Example: review and remediate with `jq` + `aws`

> ⚠️ **Review before you remediate.** Savings and carbon numbers are *estimates* derived
> from CloudWatch metrics and public pricing, and some findings may be false positives
> (e.g. a deliberately idle DR instance). Never pipe these commands straight into a shell
> — inspect each resource, take backups, and apply them yourself.

```bash
#!/usr/bin/env bash
set -uo pipefail

# 1. Scan non-interactively; keep progress output on the terminal, data in a file.
#    Exit code 1 just means "findings present", so don't treat it as a failure.
status=0
npx greenops-scan \
  --profile production \
  --region eu-west-1,us-east-1 \
  --json > findings.json || status=$?
if [ "$status" -gt 1 ]; then
  echo "scan failed (exit $status)" >&2
  exit "$status"
fi

# 2. Nothing to do?
if [ "$(jq '.findings | length' findings.json)" -eq 0 ]; then
  echo "No findings — infrastructure is clean."
  exit 0
fi

# 3. Triage: highest-value, non-destructive findings first.
jq -r '
  .findings
  | map(select(.severity == "critical" and .monthlySavingsUsd >= 50))
  | sort_by(-.monthlySavingsUsd)[]
  | "\(.region)\t\(.module)\t\(.resourceId)\t$\(.monthlySavingsUsd)/mo\t\(.monthlyCarbonSavingsKg)kg CO2/mo\t\(.issue)"
' findings.json | column -t -s $'\t'

# 4. Inspect a resource before touching it.
jq -r '.findings[] | select(.resourceId == "vol-0abc123")' findings.json
aws ec2 describe-volumes --volume-ids vol-0abc123 --region eu-west-1

# 5. Emit the remediation commands for review (destructive ones are flagged in comments).
npx greenops-scan \
  --profile production \
  --region eu-west-1,us-east-1 \
  --fix-commands --quiet > remediation-plan.sh || [ $? -le 1 ]

# 6. Read remediation-plan.sh, take backups, then run the commands you approve — one by one.
```

## Using with AI coding agents / LLMs

`greenops-scan` is designed to be driven by an AI agent (GitHub Copilot, Claude, Cursor,
or any tool-calling LLM) just as easily as by a human at a terminal — every interactive
prompt has a non-interactive flag or environment variable, and `--json` produces a
stable, documented schema instead of a table meant for a human eye.

See **[`SKILL.md`](./SKILL.md)** ([view on GitHub](https://github.com/spidgorny/greenops-scan/blob/main/SKILL.md))
for the full agent guide, including:

- The minimal non-interactive invocation an agent should use (`--non-interactive --json --profile ... --region ...`) and what each exit code means
- Every CLI flag and its environment-variable equivalent, and all `--modules` names
- The `--json` output schema, field by field — including confidence levels and how `metadata` varies per module
- A concrete **ranking algorithm** for surfacing the biggest-impact findings first (sort by `monthlySavingsUsd`, tie-break by `savingsConfidence`/`severity`/carbon, express findings as `% of monthlyCostUsd` when Cost Explorer billing data is present)
- How to safely use `--fix-commands` output — and the hard rule that destructive remediations must get human sign-off before running

Example: ask your agent something like *"Run greenops-scan against my `prod` AWS
profile and show me the top 3 cost-saving opportunities"* — pointing it at `SKILLS.md`
(or just letting it discover the CLI's `--help`/`--json` output) is enough for it to run
the scan, parse the findings, and come back with a prioritized, dollar-ranked list
instead of a raw dump of every finding.

```bash
# What an agent typically runs under the hood:
npx greenops-scan --non-interactive --json --profile prod --region eu-west-1 \
  | jq '.findings | sort_by(-.monthlySavingsUsd) | .[:3]'
```

## Security

This scanner runs locally and uses **read-only** AWS APIs. Your cloud credentials never leave your machine.

## Feedback and issue reporting

Found a bug, false positive, or missing check? Please open an issue:

https://github.com/spidgorny/greenops-scan

## Need automated reporting across teams?

The free CLI is great for one-off and ad hoc scans. For continuous coverage and team workflows, use GreenOps Cloud:

https://greenops.cloud/scan?utm_source=github&utm_medium=readme&utm_campaign=greenops_launch_2026q3&utm_content=cli_readme_upsell

GreenOps Cloud adds:

- Automated recurring scans (no manual re-runs)
- Shared dashboard and findings history across accounts
- Trend tracking for cost and carbon opportunities over time
- Team collaboration workflows for triage and follow-up
- Centralized visibility for engineering and FinOps stakeholders

## Requirements

- Node.js 18+
- AWS credentials configured in `~/.aws/credentials` or via environment variables
