Build1 publisher2 min readPublished
zombiescan prices each Google Cloud waste finding from the Billing Catalog API
The scanner covers 20 classes of idle resource, prices each finding from the Cloud Billing Catalog API, and generates its read-only role from what every check declares. The walkthrough stops before a priced finding prints.
The Engineer · Build desk

What happened
- The zombiescan walkthrough builds a Google Cloud waste scanner from source, runs it across every project the credentials can see, and prices each finding from the Cloud Billing Catalog API.
- Twenty checks cover 20 classes of resource across two packs: nineteen ship in the core pack and one in gke.
- Read access is enough to run it: the predefined roles/viewer covers every call, and the repository ships a 16-permission custom role at policy/zombiescan-scanner-role.yaml.
- An API that is not enabled on a project is skipped instead of being reported as an error, because a project that never used a service has no waste in it.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Twelve of the 20 checks call compute.googleapis.com, 60 percent of the suite, so in a project where Compute Engine is not enabled the report comes back short and looks clean.
- decision Because the test suite fails when a check names an API the role does not cover, widening the scanner's coverage becomes a permissions diff a security reviewer reads before anyone runs a scan.
- cost At version 0.1.0, whoever adopts this is reviewing the mapping from finding to billable SKU themselves, and paying for it in engineer time rather than in licence fees.
- precedent The gke pack sets the bar for anything outside core: a new service pack has to bring its own API, its own rate section and its own price fetcher.
Refreshing a Google credentials object signs through OpenSSL, and one credentials object refreshed from several threads at once ends the process instead of raising an exception [9]. zombiescan fetches the first token before any worker thread starts. The threads then share a single credentials object, and the wrapper serialises later refreshes behind a lock [8]. Credentials come from Application Default Credentials, with no key file downloaded and no service account created [7]. You hit that failure on the fourth project of a scan, not in a unit test.
An unattached Persistent Disk keeps billing after the instance it served is gone, and a GKE cluster keeps charging its management fee after the last node pool scaled to zero [19]. Each one bills every hour and reports nothing, according to the post [19], while the billing console shows the total and the Recommender surfaces candidates [4]. What makes a finding usable, it argues, is a figure that "names one resource, in one project, and its monthly cost" [5]. The available text of the walkthrough breaks off at the first scan command, before any priced finding prints [20], so the comparison with the Recommender rests on the design.
Each check declares the APIs it calls, and that declaration generates both the published API listing and the read-only custom role [12]. The listing names 11 APIs [24]. The per-API check counts sum to 22 declarations across 20 checks [21], so at least two checks call more than one API [21].
Read the check names and you can see which findings are facts about billing and which are policy. An unattached disk is a fact. stale-secret flags secrets nothing has updated in 90 days, unmanaged-gcs-bucket flags versioned buckets with no lifecycle rule, and unbounded-log-bucket flags log buckets retaining logs indefinitely [25]. Those three encode a retention opinion, and the cleanup they justify carries a different risk than deleting a disk nothing is mounted on. The checks span Compute Engine, Cloud SQL, Storage, DNS, KMS, Secret Manager, Artifact Registry, Filestore, Cloud Logging, Cloud Monitoring and GKE [2].
Running it needs uv on the path and Python 3.11 or newer [18]. There are two routes, a clone driven with uv run and uv tool install from git, and the post says both run the same engine [23]. uv sync reads the committed uv.lock, so the resolved set is the one the tests ran against [16]; the run shown in the post resolved 39 packages and checked 38 [17].
What to watch
- A published scan with priced findings would show whether the Billing Catalog SKU a check picks matches the line on the project's bill.
- A pack written outside the repository, built to the gke pack's interface, would test whether that contract holds for a second service.
- A release past 0.1.0, and whether the custom role's permission count grows as checks are added.