# Security Review Pull Requests on a Django App

Yes. An agent traces each new request parameter in the pull request to the database or shell, checks the new view's authorization and flags secrets in the diff.

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

## What the agent does

The agent checks out the pull request's branch in its own sandbox and learns the app's authorization pattern before it reads the new code. In a Django app like healthchecks/healthchecks, `requirements.txt` pins `Django[argon2]==6.1.2`, and `hc/front/views.py` defines the access helpers every dashboard view uses: `_get_check_for_user`, `_get_channel_for_user` and `_get_project_for_user` return the object for a superuser or the project owner, otherwise look up a `Member` row with `get_object_or_404`, so a stranger gets a 404, and the `_get_rw_*` variants raise `PermissionDenied` for read-only members. Views stack `@require_POST` and `@login_required` above them, as `update_name` does.

The JSON API has its own pattern in `hc/api/decorators.py`. `@authorize` takes a 32-character key from the `X-Api-Key` header or the `api_key` field of a POST body and accepts only read-write keys; `@authorize_read` also accepts read-only keys and sets `request.readonly` when the key starts with `hcr_` or matches the project's read-only key. Every API view then scopes its query with `Check.objects.filter(project=request.project)`. With both patterns in hand, the agent knows what a new route must do, so a missing check stands out as a difference rather than an opinion.

Then it traces each new input. For every new URL in `hc/front/urls.py` or `hc/api/urls.py` it follows each `request.GET`, `request.POST`, form field and URL parameter to where it lands. A value reaching `.raw()`, `.extra()`, `RawSQL` or a `cursor().execute()` built with an f-string or `%` is SQL injection; the repository's own raw queries are the safe pattern, such as the docs search, which passes the query to `sqlite3` as a bound parameter. A value reaching `subprocess`, as the shell integration in `hc/integrations/shell/transport.py` does behind `SHELL_ENABLED`, or reaching a filesystem path, is checked the same way. A new view that looks up a `Check` by `code` with `Check.objects.get` instead of the helper, or a write behind `@authorize_read`, is missing authorization.

It proves what it can rather than asserting it. It runs `./manage.py test` for the apps the pull request touches, against the default SQLite database `hc/settings.py` falls back to, and for each authorization finding writes a short throwaway test in its sandbox that logs in as a user from another project and calls the new route, recording the status code it gets. Last, it scans the diff for secrets: keys, tokens and passwords committed in settings, fixtures or tests. `hc/settings.py` reads secrets through `envsecret`, from an environment variable or a `_FILE` path, so a literal value in its place is a finding.

## Start it with one API call

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

```json
{
  "agent": "claude",
  "input": "Review the open pull request for security problems only. First read the views, helpers and decorators that existing routes use to authorize access. Trace every new request parameter to where it reaches the database, the filesystem or a shell, check that each new route enforces the same authorization as its neighbours, and look for secrets in the diff. Run ./manage.py test for the touched apps, and for each authorization finding, confirm it with a throwaway test in the sandbox that calls the route as a user from another project. Post findings as inline review comments with the exploit in one sentence each and the status code you observed; post nothing if there are none. Do not push commits.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-django-app/pull/123"
    }
  ]
}
```

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 either nothing or a set of inline review comments, one per finding, on the exact line where the input reaches a query, a shell or a path, or where a view skips the access helper. Each comment states the exploit in one sentence, such as a member of another project reading a check's ping log by changing the UUID in the URL, and gives the fix in the repository's own idiom: call `_get_rw_check_for_user`, use `@authorize` instead of `@authorize_read`, pass the value as a query parameter.

For authorization findings the comment includes the status code the throwaway test saw, so the claim is observed rather than inferred. The session's transcript keeps the test runs, the throwaway tests and the trace for each input, and the review says which findings were confirmed by a request and which come from reading the code alone.

Before merging, a reviewer confirms each exploit is reachable in production settings, for example whether an integration is enabled there, and adds a permanent test for every confirmed finding. An authorization comment backed by a 200 from another project's user is the one to fix first.

## Run it on every pull request that adds a route

An automation with a GitHub trigger on `pull_request.opened` and `pull_request.synchronize` runs this review on each push. The prompt posts nothing when there are no findings, so it can run on every pull request without adding noise; to narrow it, check on your server whether `urls.py`, `views.py` or `forms.py` changed before sending the request with the pull request URL in `resources`.

Set a webhook URL on the API key, and when `run.completed` arrives read the session for the result. A run that found nothing says so in its final answer, which gives you a record that the pull request was checked even when the review thread stays empty.

## Why run it on Rush?

A security review that matters has to read the whole path from a request to the database, compare it with how every neighbouring view authorizes, and ideally call the route as the wrong user. Doing that by hand for every pull request is exactly the work that gets skipped. On Rush each review runs in its own sandbox with the branch, the test suite and a throwaway database, so the agent can run `./manage.py test` and send a real request as another project's user without any risk to your data.

Findings come with evidence: the trace, the test it wrote, the status code it saw. Because each run is isolated, it can review every pull request in parallel, and the session's record shows a security reviewer exactly what was checked on a change that later turns out to matter.

## Frequently asked questions

### Will it flag every raw SQL query in the codebase?

No. It reviews only what the pull request adds or changes, and it distinguishes a parameterized query, like the existing docs search that binds the user's input, from one built with string formatting. An unchanged query elsewhere is out of scope.

### How does it know what the right authorization is?

It reads the helpers and decorators the neighbouring views use, such as `_get_rw_check_for_user` and `@authorize`, and compares the new route with them. A new view that fetches an object without the helper, or allows a write with a read-only key, is a finding.

### Does it need a production database or real secrets?

No. It runs the app's own tests against a throwaway database in its sandbox, which for healthchecks is the SQLite file `hc/settings.py` uses by default. No production URL or secret is needed or given.

### What happens when there is nothing to report?

It posts no review, as the prompt asks, and says in its final answer that it found nothing and which routes and inputs it traced. That answer is in the session and the `run.completed` webhook, so you keep a record without a comment on the pull request.
