Your backups run. But can you actually restore?
A read-only CLI that inventories your AWS backups, checks them against five recoverability criteria, and prints a Recoverability Score (0–100) with the gaps that matter. It runs locally with your own credentials, changes nothing, and sends nothing anywhere.
Most backups are never test-restored. This tool tells you which of yours.
$ drillproof audit scan --region ap-southeast-1
DrillProof Audit — account 1234-5678-9012 — ap-southeast-1
──────────────────────────────────────────────────────────
RESOURCE TYPE BACKUP AGE IMMUTABLE X-REGION RESTORE
payments-db (RDS) database ok 6h no no untested
orders-pv (EKS/EBS) volume ok 3h yes yes verified
etcd (prod-cluster) k8s-state none - - - failed
──────────────────────────────────────────────────────────
Recoverability Score: 61/100 (3 critical gaps)
Top findings:
x etcd (prod-cluster) — no backup of cluster state — the cluster is unrecoverable
! payments-db (RDS) — backups can be deleted — no Vault Lock or Object Lock
! payments-db (RDS) — never test-restored — "backed up" is not "recoverable"
Fix these free: https://drillproof.com/audit (or run `drillproof audit init`)
The table above is illustrative. Your own output will differ.
This matters more than the features, so it comes first.
- Runs locally, with the credentials already on your machine.
- Strictly read-only. Only
Describe,List, andGetAPIs are called. The tool cannot create, modify, restore, or delete anything — the AWS client interfaces ininternal/awsexpose no mutating operation, so a write call would not compile. A test (TestInterfacesAreReadOnly) and a CI job enforce this on every commit. - Nothing leaves your machine. No telemetry, no phone-home, no uploads. If opt-in telemetry is ever added it will be explicit and off by default.
- Metadata only. It reads backup configuration — resource ids, timestamps, retention and lock settings. It never reads the contents of a backup, a volume, or a database.
Reports are only written where you redirect them. Nothing is saved by default.
# Homebrew
brew install drillproof/tap/audit
# Shell installer
curl -sSL https://get.drillproof.com | sh
# Docker (mounts your AWS config read-only)
docker run --rm -v ~/.aws:/root/.aws:ro drillproof/audit scan
# From source
go install github.com/drillproof/audit@latest# Scan every enabled region — the provider and regions are auto-detected
drillproof audit scan
# Narrow it down
drillproof audit scan --profile prod --region us-east-1,eu-west-1
# Show the score arithmetic
drillproof audit scan --explain
# Gate a pipeline on the score
drillproof audit score --fail-under 80
# Machine-readable output
drillproof audit report --format json > recoverability.json
drillproof audit report --format sarif > recoverability.sarif
drillproof audit report --format html > recoverability.html
# Print the exact IAM policy this tool needs
drillproof audit init
drillproof audit init --format terraformAlso include cluster state, which AWS APIs cannot see:
drillproof audit scan --kubeconfig ~/.kube/config --context prod| Command | What it does |
|---|---|
scan |
Inventory + all five checks + score (the default) |
score |
Just the score; --fail-under N gates CI |
report |
Render as html, json, or sarif |
init |
Emit the least-privilege IAM policy, Terraform, CFN, or K8s RBAC |
verify |
Coming soon — a real isolated test-restore. Not in v1. |
version |
Version and build info |
| Flag | Default | Notes |
|---|---|---|
--profile |
standard AWS chain | AWS profile |
--region |
all enabled regions | Repeatable/comma-separated; only narrows |
--output, -o |
table |
table | json | sarif |
--kubeconfig |
none | Enables the EKS/Velero cluster-state check |
--context |
current context | Kube context |
--explain |
off | Show how the score was calculated |
--warn-after |
24h |
Freshness warning threshold |
--fail-after |
168h |
Freshness failure threshold |
--no-color |
off | Also respects NO_COLOR |
--quiet, -q |
off | Score and failures only |
--verbose, -v |
off | Progress logging to stderr |
Any flag can also be set by environment variable: DRILLPROOF_REGION,
DRILLPROOF_PROFILE, and so on.
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Error (bad flags, no credentials, API failure) |
2 |
Score below --fail-under |
3 |
verify — not implemented in v1 |
For every EBS volume, RDS instance/cluster, and EKS cluster state:
| Check | Question | Why it matters |
|---|---|---|
| Coverage | Does a backup exist at all? | The only unrecoverable state. Dominates the score. |
| Freshness | How old is the newest successful backup? | Catches schedules that silently stopped succeeding. |
| Immutability | Is it on WORM storage (Vault Lock / Object Lock)? | A deletable backup may not be there during the incident that needs it. |
| Redundancy | Is a copy in another region? | A backup beside the resource does not survive a regional event. |
| Restore-testing | Has a restore ever actually been performed? | The gap this whole tool exists to surface. "Backed up" ≠ "recoverable". |
Every finding carries the resource, region, status (ok / warn / fail /
skipped), a one-line explanation, and a remediation hint.
Two different things get reported separately, because conflating them would mean lying to you:
- Blocked — we were not permitted to look. Fixable by granting the
permission; run
drillproof audit init. - Not applicable — there was nothing to look at. A volume with no backup has no backup age and no cross-region copy; no permission changes that.
A resource whose lock state we could not read is never reported as immutable.
A single 0–100 integer plus a count of critical gaps. It starts at 100 and subtracts a documented weight per failing check:
| Failing check | Deduction |
|---|---|
| Coverage (no backup) | −25 |
| …and it is cluster state or prod DB | −10 more |
| Immutability | −10 |
| Freshness (past fail threshold) | −8 |
| Freshness (past warn threshold) | −4 |
| Redundancy | −6 |
| Never test-restored | −5 |
Clamped to 0. Deterministic — the same estate always scores the same, regardless
of the order results arrive in. Every deduction is itemised by --explain, so
you can check the arithmetic:
$ drillproof audit scan --explain
...
How this score was calculated:
Starting score: 100
-25 etcd (prod-cluster) — no backup exists for critical resource (coverage)
-10 payments-db (RDS) — backups are deletable (no WORM/Object Lock) (immutability)
-5 payments-db (RDS) — never test-restored (restore-testing)
Total deduction: 40
Recoverability Score: 60/100
The weights live in internal/score/score.go as named constants with the
reasoning written out. If you disagree with a weight, you can see exactly what to
change. Untested-restore is weighted last on purpose — not because it matters
least, but because it is nearly universal, and penalising it heavily would flatten
every score to zero and destroy the signal that makes the rest actionable.
If nothing could be assessed at all, the score is 0, not 100. An account we cannot read is not an account with perfect backups.
drillproof audit init prints the exact policy — 17 actions, no wildcards:
{
"Version": "2012-10-17",
"Statement": [
{ "Sid": "Identity", "Effect": "Allow", "Action": ["sts:GetCallerIdentity"], "Resource": "*" },
{ "Sid": "RegionAndVolumeInventory", "Effect": "Allow", "Action": [
"ec2:DescribeRegions", "ec2:DescribeSnapshots", "ec2:DescribeVolumes"
], "Resource": "*" },
{ "Sid": "DatabaseInventory", "Effect": "Allow", "Action": [
"rds:DescribeDBClusterSnapshots", "rds:DescribeDBClusters",
"rds:DescribeDBInstances", "rds:DescribeDBSnapshots"
], "Resource": "*" },
{ "Sid": "BackupPosture", "Effect": "Allow", "Action": [
"backup:DescribeBackupVault", "backup:ListBackupVaults",
"backup:ListRecoveryPointsByBackupVault", "backup:ListRecoveryPointsByResource",
"backup:ListRestoreTestingPlans"
], "Resource": "*" },
{ "Sid": "ClusterInventory", "Effect": "Allow", "Action": [
"eks:DescribeCluster", "eks:ListClusters"
], "Resource": "*" },
{ "Sid": "ObjectLockPosture", "Effect": "Allow", "Action": [
"s3:GetBucketLocation", "s3:GetBucketObjectLockConfiguration"
], "Resource": "*" }
]
}Resource: "*" is unavoidable for discovery: the tool cannot name ARNs it has
not found yet. Every action is a read.
For the optional cluster-state check, drillproof audit init --format rbac
emits a read-only ClusterRole (get/list only).
Missing a permission never crashes the scan — that check is reported as blocked, naming the operation that was denied.
go test ./... # unit tests, AWS fully faked — no credentials needed
go test -race ./...
go vet ./...
gofmt -l .
go build -o drillproof .
goreleaser release --snapshot --clean # local release dry-runLayout:
cmd/ cobra commands
internal/
aws/ read-only clients behind narrow interfaces (+ fakes)
k8s/ Velero detection via discovery + dynamic client
checks/ the five checks — pure functions, no I/O
score/ the scoring function and its documented weights
report/ table / json / sarif / html renderers
iam/ policy, Terraform, CloudFormation, RBAC emitters
model/ shared vocabulary, dependency-free
The design rule worth knowing before changing anything: the checks are pure.
Discovery translates AWS and Kubernetes into model.BackupState, and the checks
read only that. It is why they are testable without credentials and why the score
is reproducible.
Two tests exist specifically to stop the product's promises rotting:
TestInterfacesAreReadOnlyreflects over every AWS interface and fails if a mutating operation appears.TestPolicyCoversEverySDKCallfails if the scanner calls something the emitted IAM policy does not grant — or grants something it never calls.
v1 covers EBS, RDS, and EKS cluster state on AWS, read-only.
v1 does not cover, deliberately: actual test-restores (verify), S3/EFS/
DynamoDB resources, org-wide or cross-account scans, non-AWS clouds, and any
write operation whatsoever.
verify is absent for a reason. Performing a restore needs write permissions and
real isolation guarantees, and shipping that carelessly is how an audit tool
becomes an incident. Until then, scan can prove a restore point has never
been tested — and a free Restore-Assurance Audit
will run the real thing with you.
Apache-2.0. See LICENSE.