Back to the examples

Example report

Infrastructure audit.

This is the full infrastructure audit report for the same app: deployment, backups, monitoring, access and cost controls. Where the owner could not answer a question, the finding is marked as reported, not verified.

System: Example App (Google Cloud Run: app-api, app-frontend plus -staging copies; Supabase; Cloud Tasks; Cloud Scheduler), GCP project example-project, region europe-west1 Date: 2026-11-04 Scope: whole repository except audit archive/; read-only gcloud calls and Supabase advisors against production and staging services. Not examined: DNS, domain registrar, Stripe, Supabase dashboard (backups, billing), GCP IAM, Monitoring, Secret Manager and Billing (this gcloud login has no permission for them). Data handled: Google OAuth tokens, Google Ads access, user emails, Stripe payments (from the code; you said no other sensitive data). Method: scanner (scripts/scan.sh) + questions to the owner + checklist + rubric Live checks: read-only gcloud run services list/describe/get-iam-policy, gcloud scheduler jobs list, curl of /health, Supabase advisors, on 2026-11-04, using a personal Google login with limited project permissions.

Example report from a real audit of an AI-built app. Names, identifiers and customer details have been changed.

Summary

The code side is in good shape (CI with typecheck, build and tests; separate staging with the same setup). The operations side has gaps that you confirmed you cannot answer today: no way to be alerted on errors or downtime, no known backup or restore, no known spending cap, and one person with access. Findings: 4 Critical, 3 High, 4 Medium, 1 Low. The biggest risk is losing data or running up a bill without anyone noticing. Because the findings come from questions you could not answer, several are rated on missing information; each is marked "Reported by user, not verified" and settles quickly once checked in the consoles.

Critical High Medium Low
4 3 4 1

Top 3 actions

  1. Today: in the Supabase dashboard confirm backups are on for the production project (and on which plan), and do one test restore into a new project.
  2. This week: add error tracking and an uptime check with email alerts, and set a Google Cloud budget alert.
  3. This week: give a second trusted person owner access to GCP, Supabase and the domain, with MFA on all.

Findings

[C1] Database backups not confirmed and never restored — Critical

  • Where: provider console: Supabase production project <project-ref>
  • Evidence: You answered "no idea" about backups and restores. Backup settings are not visible to the tools available in this audit. Reported by user, not verified.
  • Severity reasoning: Failure 1 (permanent data loss: accounts, runs, Google connections), Likelihood A (backups that were never restored count as no backup). Payments and personal data: already at the top.
  • Failure it makes likely: One bad migration or deleted table and the data is gone for good.
  • Fix: Check the plan (daily backups come with Pro; free tier has none), enable Point-in-Time Recovery if affordable, and restore a backup into a scratch project once.
  • Fix prompt:
    Write docs/runbooks/database-restore.md: how to find the latest Supabase backup for project <project-ref>, restore it into a new project, point a staging deploy at it, and check row counts for accounts, runs and google_tokens. Leave (fill in) wherever I must look in the dashboard.
    
  • Verify: A restored copy exists and its row counts match production on the day.

[C2] No error tracking or alerting — Critical

  • Where: whole codebase (scanner NO_ERROR_TRACKING); Cloud Monitoring not readable
  • Evidence: No error-tracking library in package.json files. You answered "no idea" about how you would learn of an outage. Verified in code (no library); alert policies Reported by user, not verified.
  • Severity reasoning: Failure 2 (outage unnoticed for hours), Likelihood A (nobody would notice until a customer complains); raised one level because the service handles personal data and payments.
  • Assessor note: Your deploy script says production has no paying customers yet, which would lower real-world impact; the rubric has no adjustment for that, so the rating stays.
  • Failure it makes likely: Failing runs, broken Stripe webhooks or a down site go unseen.
  • Fix: Add Sentry (or Cloud Error Reporting, free on Cloud Run) and a Cloud Monitoring uptime check on /health for both services, alerting your email.
  • Fix prompt:
    Add @sentry/node to api/ and @sentry/nextjs to frontend/, initialised only when SENTRY_DSN is set, capturing unhandled errors in Hono (app.onError) and Next.js route handlers. Add SENTRY_DSN to .env.example. Do not add a DSN value.
    
  • Verify: Trigger a test error in staging and receive the alert; stop staging and receive the uptime alert.

[C3] Only one person holds access; recovery depends on them — Critical

  • Where: GCP project example-project, Supabase, DNS and registrar (provider consoles)
  • Evidence: You answered "only owner of the project". Cloud Run IAM (Verified live) shows an extra human user with invoker rights, scripts/deploy-api.sh pins deploys to a different Google account, and all 125 commits in six months have one author identity that differs from your git user. So there may be a second identity with access you do not control, or your answer needs correcting. MFA status Reported by user, not verified.
  • Severity reasoning: Failure 2, Likelihood A (single person or account that can lock out until a second person has access); raised one level for personal data.
  • Failure it makes likely: Loss of the one account or laptop means nobody can deploy, rotate secrets or restore.
  • Fix: Add a second owner to GCP, Supabase and the registrar; turn MFA on for all; keep recovery codes offline; decide who the second Google account (DEPLOY_GCLOUD_ACCOUNT) is and whether it should still have access.
  • Fix prompt:
    Create docs/runbooks/access-and-recovery.md listing every account needed to run Example App (GCP, Supabase, Stripe, Google Cloud OAuth client, domain registrar, DNS, GitHub), who owns it, whether MFA is on, and where recovery codes are kept. Use (fill in) for anything unknown. Do not write any passwords.
    
  • Verify: A second person logs in to each console with MFA and performs a harmless action.

[C4] No known spending cap on usage-based services — Critical

  • Where: GCP Billing, Gemini API, Cloud Run
  • Evidence: You answered "don't know" about the monthly bill and alerts; Billing is not readable with this login. Code side: Gemini calls on every run, Cloud Run API with CPU always on (--no-cpu-throttling, 4 GiB, up to 10 instances, 3,600 s timeouts), internal API publicly reachable behind one shared secret (SECURITY M2). The repo contains scripts/setup-cost-monitoring-alerts.sh and api/src/lib/cost-watcher.ts; whether they were ever run, and whether their scheduler job exists in production, was not verified (see H1). Reported by user, not verified.
  • Severity reasoning: Failure 1 (large unbounded bill), Likelihood B (needs a runaway job or leaked secret, and the cost watcher may partly catch it); raised one level for personal data and payments.
  • Failure it makes likely: A loop, abuse or leaked key produces a bill discovered at month end.
  • Fix: Create a Cloud Billing budget with alerts at 50, 90 and 100 percent; set a daily Gemini quota in the Google Cloud console; confirm the cost-watcher scheduler job runs in production.
  • Also reported in: SECURITY-AUDIT-REPORT.md, M2; SCALABILITY-AUDIT-REPORT.md, H1
  • Fix prompt:
    Read scripts/setup-cost-monitoring-alerts.sh and scripts/setup-cost-watcher-scheduler.sh and api/src/lib/cost-watcher.ts and summarise in plain words: what they alert on, at what thresholds, to whom, and what manual steps remain. Do not run anything.
    
  • Verify: A test budget alert email arrives; Billing shows the budget.

[H1] Production scheduled jobs not found — High

  • Where: Cloud Scheduler, europe-west1; api/src/routes/cron.ts (sweep-stale-runs, cancel-archived-cycle-tasks)
  • Evidence: Verified live. gcloud scheduler jobs list --location europe-west1 returns four jobs, all ending -staging (sweep-stale-runs-staging, runs-cleanup-staging, ads-label-refresh-staging, cancel-archived-cycle-tasks-staging). No production job appeared. Other regions were not checked.
  • Severity reasoning: Failure 2 (stuck runs are never swept, expired runs and cost watching never run), Likelihood B (the jobs may live in another region or be created at launch, per the deploy script's "prod promotion checklist"); raised one level for personal data and payments.
  • Failure it makes likely: Runs that crash stay "running" forever and storage is never cleaned in production.
  • Fix: Create the production scheduler jobs the same way as staging, or document that they are intentionally off until launch.
  • Fix prompt:
    Read scripts/setup-cost-watcher-scheduler.sh and docs/runbooks/prod-promotion-checklist.md and list which Cloud Scheduler jobs production needs, with schedule and target path, compared with the four -staging jobs. Output the gcloud commands I would run, but do not run them.
    
  • Verify: gcloud scheduler jobs list --location europe-west1 shows the production jobs; one run appears in the API logs.

[H2] Manual laptop deploys with nothing gating production — High

  • Where: scripts/deploy-api.sh, scripts/deploy-frontend.sh, .github/workflows/ci.yml (no deploy job; scanner PIPELINE_NO_DEPLOY)
  • Evidence: Verified in code and reported by user. You run the deploy scripts yourself. CI runs typecheck, build and tests on PRs and main, but nothing stops a deploy of code that failed CI. The script is pinned to a personal OAuth login, which expires.
  • Severity reasoning: Failure 2 (bad release reaches users), Likelihood B (needs a deploy of unreviewed or failing code; a pre-commit check script exists); raised one level for personal data and payments.
  • Failure it makes likely: A release built from an unchecked laptop state; no record of what is live.
  • Fix: Have CI deploy main to staging after green tests and promote to production on a manual approval, using a dedicated service account with Workload Identity Federation (no key file).
  • Fix prompt:
    Add .github/workflows/deploy-staging.yml that runs after the CI workflow succeeds on main and calls scripts/deploy-api-staging.sh and scripts/deploy-frontend-staging.sh using google-github-actions/auth with Workload Identity Federation (placeholders for the provider and service account, no secrets). Do not add a production job yet and do not run anything.
    
  • Verify: A merge to main with a failing test does not deploy; a green one updates staging.

[H3] Services run as the default compute service account — High

  • Where: Cloud Run app-api (and Cloud Tasks setup in scripts/deploy-api-staging.sh:132)
  • Evidence: Verified live. The service runs as <project-number>-compute@developer.gserviceaccount.com. Its project roles were not readable (no getIamPolicy permission). That default account often holds Editor.
  • Severity reasoning: Failure 1 (a code-execution bug in the API would hold broad project access), Likelihood C; raised one level for personal data.
  • Failure it makes likely: A single exploited dependency could reach secrets and every GCP service in the project.
  • Fix: Create app-api-runtime with only Secret Accessor, Cloud Tasks Enqueuer and the specific roles needed, and set --service-account in the deploy scripts.
  • Fix prompt:
    Propose (do not run) the gcloud commands to create a dedicated runtime service account for app-api with only the roles the code needs (Secret Manager accessor for the secrets listed in scripts/deploy-api.sh, Cloud Tasks enqueuer, Cloud Run invoker for task targets), and the one-line --service-account change for each deploy script.
    
  • Verify: gcloud run services describe app-api shows the new account and the app still runs.

[M1] Live configuration differs from the scripts — Medium

  • Where: Cloud Run app-api; scripts/deploy-api.sh (--max-instances=5)
  • Evidence: Verified live. The live service has maxScale=10; the deploy script sets 5, and uses --update-env-vars, so hand-made changes survive. There is no infrastructure as code (scanner IAC none).
  • Severity reasoning: Failure 3, Likelihood A (already different). No adjustment.
  • Failure it makes likely: Nobody knows what production is really configured as; a rebuild would not match.
  • Fix: Export the live config to a file in the repo, or align the script, and record why.
  • Fix prompt:
    Write docs/runbooks/cloud-run-live-config.md that records the current live settings of app-api and app-frontend (from gcloud run services describe, no secret values) next to the values in scripts/deploy-api.sh and deploy-frontend.sh, highlighting differences such as maxScale 10 vs 5.
    
  • Verify: The two columns match or each difference has a reason.

[M2] Environment variables undocumented — Medium

  • Where: .env.example
  • Evidence: The scanner lists variables read by the code but missing from the example (API_BASE_URL, CLOUDFLARE_API_TOKEN, CLOUD_TASKS_API_URL, CLOUD_TASKS_LOCATION, five CLOUD_TASKS_WF*_QUEUE, GEMINI_PRICING_*, and more). Verified in code.
  • Severity reasoning: Failure 3, Likelihood A. No adjustment.
  • Failure it makes likely: Recovery or a new environment takes hours of guessing; a missing variable breaks one workflow silently.
  • Fix: Add every variable with an empty value and a comment.
  • Fix prompt:
    Update .env.example with every variable listed under ENV_NOT_DOCUMENTED in INFRASTRUCTURE-AUDIT-SCAN.txt: empty value plus a one-line comment saying what it is and which service uses it. Never put real values.
    
  • Verify: Re-run the scanner: no ENV_NOT_DOCUMENTED line.

[M3] Containers run as root and have no health check — Medium

  • Where: api/Dockerfile, frontend/Dockerfile, frontend/Dockerfile.staging
  • Evidence: No USER and no HEALTHCHECK (scanner DOCKER_ROOT, DOCKER_NO_HEALTHCHECK). Verified in code.
  • Severity reasoning: Failure 3, Likelihood C is Low; raised one level for personal data. Cloud Run ignores Docker HEALTHCHECK, so the second part matters little.
  • Failure it makes likely: Slightly easier escalation if the app is compromised.
  • Also reported in: SECURITY-AUDIT-REPORT.md, L1
  • Fix: Add USER node; configure a Cloud Run startup probe on /health instead of HEALTHCHECK.
  • Fix prompt:
    Add USER node (with chown of /app) to the final stage of api/Dockerfile, frontend/Dockerfile and frontend/Dockerfile.staging, and add a --startup-probe flag pointing at /health to scripts/deploy-api.sh only if gcloud supports it in the flags already used. Do not run deploys.
    
  • Verify: docker run --rm <image> id -u is not 0.

[M4] CI token permissions not restricted — Medium

  • Where: .github/workflows/ci.yml
  • Evidence: No permissions: block (scanner NO_TOKEN_PERMISSIONS). Verified in code. The adjustment for personal data is not applied: the workflow does not touch the production service.
  • Severity reasoning: Failure 1, Likelihood C (rubric example), Medium.
  • Failure it makes likely: A compromised action or dependency in CI gets a token with write access to the repository.
  • Fix: Add permissions: contents: read at the top.
  • Fix prompt:
    Add a top-level permissions: contents: read block to .github/workflows/ci.yml and confirm each job still works without write access.
    
  • Verify: The CI run is green.

[L1] Lint failures do not block merges — Low

  • Where: .github/workflows/ci.yml:57 (continue-on-error: true)
  • Evidence: Verified in code. The file's own header lists this as intentionally non-blocking.
  • Severity reasoning: Failure 3, Likelihood B. No adjustment.
  • Failure it makes likely: Warnings accumulate unseen.
  • Fix: Fix the current warnings, then remove continue-on-error.
  • Fix prompt:
    Run the frontend lint with --max-warnings 0, list the current warnings by rule, and propose the smallest set of fixes. Do not change ci.yml yet.
    
  • Verify: Lint passes and the flag is removed.

Checked and found fine

  • CI runs typecheck (frontend, API, packages), frontend build, tests and structure/secret checks as blocking jobs.
  • Staging exists as a separate set of Cloud Run services and a separate Supabase project (app-staging), per your answer "same as production" (confirmed by the live service list).
  • Secrets come from Secret Manager via --set-secrets in the deploy script; no secret values were found in the repository.
  • Both production services answered /health with 200 (0.10-0.13 s warm; 5.2 s cold).
  • Supabase production project status: ACTIVE_HEALTHY.

Not checked or not verified

  • Backups, retention and any restore test (Supabase dashboard): you did not know. Settle: look at Database > Backups.
  • Alert policies, uptime checks, log retention: this login lacks monitoring permission. Owner of example-project.
  • Budgets and the monthly bill: no Billing permission. Billing account administrator.
  • GCP IAM roles of the runtime service account and who has access: no getIamPolicy. Project owner.
  • DNS, domain registrar, MFA on any account.
  • Disaster recovery (region or account loss, person unavailable): you did not know.
  • Scheduler jobs in regions other than europe-west1.
  • Local note: while listing monitoring policies, gcloud offered to install its alpha component; I did not confirm an install and it was not needed.

Remediation timeline

  • Today: C1 (confirm backups, test restore), C2 (alerts), C3 (second owner), C4 (budget alert).
  • This week: H1, H2, H3.
  • This month: M1-M4.
  • When convenient: L1.

Appendix: scanner output

SUMMARY|HIGH|NO_ERROR_TRACKING|1
SUMMARY|LOW|DOCKER_NO_HEALTHCHECK|3
SUMMARY|LOW|NO_TOKEN_PERMISSIONS|1
SUMMARY|MEDIUM|DOCKER_ROOT|3
SUMMARY|MEDIUM|ENV_NOT_DOCUMENTED|1

The full scanner output is in INFRASTRUCTURE-AUDIT-SCAN.txt.


This report is guidance produced with an AI assistant, based partly on information reported by the owner. It does not replace a review by an engineer with access to your environment.

Want this for your project?

See pricing