Built, not hosted The approval flow runs in the self-hosted application and is tested against mocked AWS. In the hosted service remediation is switched off, and no approved action has run against a live AWS account yet. Hosted findings show the tier and the commands as text.
Risk tiers
| Tier | Waste types | What happens |
|---|---|---|
| Low risk | unassociated_eip, log_group_no_retention, gp2_volume | Runs after approval |
| Medium risk | unattached_volume, orphaned_snapshot, stale_ami | Runs after approval. A volume is snapshotted first; an AMI's snapshots are kept; deleting a snapshot has no backup and needs an acknowledgement |
| High risk | idle_nat_gateway, idle_load_balancer, idle_rds, stopped_instance_ebs | Recommendation only. The command is shown for reference and cannot be executed |
| Advisory | lambda_x86 | Guidance only. Moving to arm64 needs a rebuilt deployment package, so it goes through your build or a pull request |
The tiers are fixed in code, not in the templates. Any waste type without a tier is treated as high risk.
Routing
| Owner | Route |
|---|---|
| Terraform | A pull request, when a safe edit exists. Otherwise a recommendation |
| CloudFormation | Recommendation. Change it in the stack |
| Click-ops | A CLI plan to approve, when the tier is low or medium |
| Unknown | Recommendation only |
An account with no infrastructure as code at all can opt in to CLI fixes for unknown ownership with ALLOW_CLI_WHEN_OWNERSHIP_UNKNOWN=true.
The approval lifecycle
- Request. A pending action is created holding the rendered plan: the steps, the equivalent AWS CLI commands and a hash of both.
- Approve, reject or snooze. An approval needs a named approver. If the plan is irreversible it also needs
acknowledge_irreversible. The approved commands and hash are stored on the record. - Execute. Runs the plan under
CostAgentRemediator. - Record. On success the savings ledger gets an entry. Every event is appended to the action record with a time, an actor and detail.
Statuses: pending, approved, rejected, snoozed, executing, succeeded, failed.
Checks at execution
Before any step runs, all of these must hold. If one fails the action is refused and nothing is changed.
- The approval is younger than
APPROVAL_TTL_HOURS(default 72). - The plan, rendered again from the stored finding, has the same hash and commands as the approved one.
- The plan is still executable, and a medium-risk plan with no backup step was acknowledged.
- The resource is not managed by Terraform or CloudFormation.
- The account is not the management account, and it has opted in to remediation.
- The pre-check against live AWS passes: the resource still exists, is still idle, and is still not protected by a keep tag or the allow-list.
Steps that support it are sent with DryRun first. A backup step waits for its snapshot to complete before the delete.
Pre-checks
| Action | Refused if |
|---|---|
| Release Elastic IP | The address is gone or is now associated |
| Delete volume | The volume is gone or is no longer unattached |
| gp2 to gp3 | The volume is no longer gp2 |
| Delete snapshot | The snapshot backs an AMI, or its source volume exists again |
| Deregister AMI | An instance was launched from it, or a launch template references it |
| Set log retention | The log group is gone or already has a retention period |
Each also refuses when the resource has since been keep-tagged.
Approving through the API
POST /api/remediations {"finding_id": "f-..."}
POST /api/remediations/<action_id>/approve {"acknowledge_irreversible": false}
POST /api/remediations/<action_id>/reject {"reason": "..."}
POST /api/remediations/<action_id>/snooze
POST /api/remediations/<action_id>/execute
GET /api/remediations
These routes always need an identified caller, even when the deployment allows anonymous reads. They are not part of the versioned OpenAPI document.
Sources: remediation/engine.py, remediation/approvals.py, remediation/prechecks.py, iac/routing.py, app.py, security/auth.py.