
Secret scanners usually watch Git. In a GCP project credentials also end up in Cloud Run environment variables, bucket objects (Terraform state, .env files), application logs and VM metadata. gcp-secrets-sprawl-detector scans those places read-only, finds credentials, and ranks each by two things a plain grep cannot know: who can read the place it was found, and what the credential can do. Secret values never leave the scanner: findings carry a masked preview only, and models see redacted metadata.
Where it looks and how exposure is judged
| Source | How | Exposure means |
|---|---|---|
| Cloud Run env vars | Admin API v2; Secret Manager references are skipped (that is the right pattern) | Holders of run viewer/developer/admin or project viewer/editor/owner |
| Bucket objects | Allow-listed buckets only, text-like files up to 512 KB | Bucket and project storage roles; public if allUsers is granted |
| Cloud Logging | Recent text and JSON payloads | Logging viewer roles |
| VM / project metadata | Compute API | Compute viewer roles |
| Local directory | sprawl scan-dir | Unknown |
Detectors cover AWS and Google keys (including service-account key JSON and private keys), GitHub, GitLab, Slack, Stripe, OpenAI, Anthropic, SendGrid and npm tokens, JWTs, database URLs with passwords, and generic high-entropy assignments, skipping placeholders and code that merely handles secrets.
Is the leaked key still alive?
For a leaked service-account key the scanner asks IAM, using its own identity and never the leaked key, whether the key is still active and what roles the account holds. An active key on an Editor or Owner account is critical; a key that no longer exists is low. In the screenshot the deployer.json key found in the canary bucket is rated low, "key deleted or disabled", with the advice to remove the file because there is nothing to rotate. The other six findings are rated by type and exposure. Only service-account keys get a liveness check; GitHub, Stripe, AWS and database credentials are not validated as live.
Reading the live scan
The scan covered 47 Cloud Run env vars, 3 bucket objects and 3,000 log entries (639 skipped), 3,050 items in all. It found 7 credentials: a Stripe live key (critical), an AWS access key id and a database URL with password in config/app.env, a GITHUB_TOKEN and a REDIS_URL in the ss-canary service's env vars, a GitHub token that appeared in a log entry, and the dead service-account key. Each shows a masked preview such as sk_l…dc (32 chars), a readers count ("exposure: limited (5 readers)"), the rotation advice, and a concrete fix: gcloud run services update ss-canary --update-secrets=GITHUB_TOKEN=... for env vars, gcloud storage rm for bucket objects, and a log exclusion for log entries.
How I validated it
| Check | Result |
|---|---|
| Offline tests | 52 pass, including no-leak properties across API, CLI and model payloads |
| Planted canaries in real GCP | 7 of 7 found; 0 unexpected findings in 47 env vars and 3,000 log entries; benign controls not flagged; local and deployed identical |
| False-positive sweep, 1,335 real files in 16 of my repos | First version: 18 false positives from the generic rule. After the fix: 0, plus one real private key correctly found in a gitignored .secrets/ directory |
| Model eval | Findings identical to model-free; no secret value, preview or fingerprint ever sent to a model (audited) |
Limits
It is pattern-based: low-entropy passwords made only of letters, secrets split across lines and encoded or encrypted blobs are missed. The "key is active" path was verified with a fake IAM in unit tests only, because this project's org policy blocks creating real keys; the live canary was a fake key file for a real throwaway account. Exposure counts principals holding a role that can read the source, without expanding groups or evaluating conditions. Coverage is bounded to 200 objects per bucket, 512 KB per object and 3,000 log entries per scan; Terraform state in other buckets, Secret Manager contents, image layers and Cloud Build logs are not scanned. The runtime identity needs broad read roles on logs, Cloud Run config and VM metadata, so keep the invoker role tight.
Try it yourself
git clone https://github.com/soodrajesh/gcp-secrets-sprawl-detector
cd gcp-secrets-sprawl-detector
.venv/bin/sprawl scan-dir ./my-repo
.venv/bin/sprawl scan my-project --sources cloudrun_env,log_entry --hours 24
.venv/bin/sprawl scan my-project --bucket my-bucket --llm vertex # bucket scanning is opt-in, per bucket
📢 Have questions or feedback? Drop a comment below or connect with me on Twitter/X@spysood!