# Run a Security Audit on a FastAPI App

Yes. An agent boots your FastAPI app with Postgres in its sandbox, checks auth, secrets, access control and dev-only routes, and reports without changing code.

[Run this on Rush](https://cloud.getrush.ai/run/security-audit/fastapi/), in a Project that has your repository.

## What the agent does

The agent reads the configuration before it sends a request. In a FastAPI app like `fastapi/full-stack-fastapi-template`, `backend/pyproject.toml` requires Python 3.14 and `fastapi[standard]`, `sqlmodel`, `pyjwt`, `pwdlib` with Argon2 and bcrypt, `psycopg`, `alembic` and `sentry-sdk`. `backend/app/core/config.py` loads a `.env` one level up, and the committed `.env` sets `SECRET_KEY`, `FIRST_SUPERUSER_PASSWORD` and `POSTGRES_PASSWORD` to `changethis`. The same file refuses to start with those values unless `FASTAPI_ENV=development`, where it only warns; the audit records which environments set that variable, because `backend/app/api/main.py` also uses it to mount the `private` router. That router, in `backend/app/api/routes/private.py`, creates a user from an email and password with no authentication at all. It is meant for tests, and the first finding of the report is whether any deployment can reach it.

Then authentication. `backend/app/core/security.py` signs access tokens with HS256 and `SECRET_KEY`, and `config.py` sets `ACCESS_TOKEN_EXPIRE_MINUTES` to eight days. `backend/app/api/deps.py` decodes the token, loads the user and refuses inactive accounts; `backend/app/api/routes/login.py` issues tokens from an OAuth2 password form, always answers password recovery with the same message to prevent email enumeration, and keeps the HTML-preview route for recovery emails behind `get_current_active_superuser`. The agent reads each of these, then sends the request that shows the behaviour against the app running on localhost: a token with a tampered signature, an expired token, a recovery request for an unknown address, and an inactive user's login.

Access control is the item and user routes. `backend/app/api/routes/items.py` checks `owner_id` against the current user on read, update and delete and answers 403 otherwise, with superusers exempt; `users.py` keeps listing and creation behind the superuser dependency, answers 409 when a user changes their email to one already taken, and refuses a password change to the same password. The audit creates two ordinary users through the public registration route, has one request the other's item, and records the status code. It reads `backend/app/main.py` for the CORS middleware, which allows only `FRONTEND_HOST` with credentials, and for the OpenAPI document at `/api/v1/openapi.json`, which is public and lists every route the audit should have covered.

To run the app it follows the repository's own CI. `.github/workflows/test-backend.yml` starts the `db` and `mailpit` services from `compose.yml`, runs `scripts/prestart.sh` for the Alembic migrations and the first superuser, and then the tests under coverage. The agent does the same in its sandbox with `uv`, starts the API on localhost only, and never publishes a port. The prompt says change no code, so there is no fix commit: each finding in `security-audit.md` carries the file and line, the request sent, the response, a severity and a one-sentence fix, and a check that passed is written as passed.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Audit this FastAPI app for security problems and change no code. Read backend/pyproject.toml, .env, backend/app/core/config.py, backend/app/core/security.py, backend/app/api/deps.py, backend/app/api/main.py and every file under backend/app/api/routes/ first. Start Postgres from compose.yml, run backend/scripts/prestart.sh and start the API on localhost only, the way .github/workflows/test-backend.yml does. Check default secrets and where FASTAPI_ENV=development is set, the routes mounted only in development, token signing and lifetime, password recovery and reset, ownership checks on items, superuser-only routes, CORS, and what the public OpenAPI document exposes. For each finding write the file and line, one request that shows it against the running app, the response, a severity and the fix, in security-audit.md. Write checks that passed as passed. Do not paste secrets or tokens into the report.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is `security-audit.md` as an artifact on the session, with a `url` on `api.prix.dev` that downloads it without a key until `url_expires_at`, and the same file through `GET /o/{org}/sessions/{id}/artifacts/security-audit.md` with the key. The repository gets no commit and no pull request; `git status` at the end of the transcript is clean.

The report has one section per area: configuration and secrets, development-only routes, tokens, password recovery, access control, CORS and exposure. Each finding is a file and line, the request the agent sent to localhost, the response that shows it, a severity, and a fix it did not apply. A check that held, such as the owner check on items answering 403, is written as held, so the reader knows it was tried rather than skipped.

A reviewer judges the report by opening the cited lines and comparing them with the recorded responses, not by the severity words. The transcript holds the compose start, the migration run, the API start and every request with its output, so a finding whose response does not match its line is visible. The sandbox parks when the turn ends, so a follow-up can ask for one route to be re-checked without installing the app again.

## Run it on every release tag

When your release workflow tags a version, a job in it POSTs this body to `https://api.prix.dev/o/{org}/sessions` with the repository in `resources` and the tag in the `Idempotency-Key` header, so a re-run of the workflow returns the audit already started rather than a second one. The Rush GitHub App must be installed on the repository, or the resource is `422 RESOURCE_NOT_ACCESSIBLE`.

Give the key a `webhook_url` and Rush POSTs `run.completed` when the turn finishes, within about 30 seconds. The handler verifies the signature with the key's `whsec_` secret and downloads the artifact from the session, since the report is a file rather than the `result` text.

## Why run it on Rush?

An audit that only reads code produces guesses. This one starts Postgres, migrates the schema, creates the first superuser and sends requests, which on a laptop means a database on port 5432, a `.env` with real-looking secrets, and an API process left running. The sandbox runs all of it on localhost and keeps every request and response on the session, and nothing stays bound on your machine.

Each finding is checkable: a line you can open and a response you can compare. Several apps or branches can be audited at once, each in its own sandbox, up to the key's `max_concurrent` limit. Because the report is written as passed and failed checks rather than only failures, a clean audit is also a record of what was tried.

## Frequently asked questions

### Will it open a pull request with fixes?

No. The prompt says change no code, so the result is the report and nothing else. Each finding ends with a one-sentence fix, and a later run can apply one and open a pull request. Keeping the audit separate from the fix lets the report be checked against the code as it was.

### Is the private router a real problem or a test helper?

It is a test helper that `backend/app/api/main.py` mounts only when `FASTAPI_ENV` is `development`. The audit's job is to show whether any deployment sets that value, since the committed `.env` does. If none does, the report says the route is unreachable in production and marks the check as passed.

### Does it need my production secrets or database?

No. It runs the app in its sandbox with throwaway values and the Postgres image from `compose.yml`, seeded by `scripts/prestart.sh`. The report names the files that hold default secrets and does not paste their values.

### What about dependency vulnerabilities?

The prompt above is about the app's own behaviour. Add a sentence asking for `uv` to audit the lockfile, or run that as its own job, so the report stays one subject. The dependency versions in `backend/pyproject.toml` and `uv.lock` are what the agent would check.
