# Security Review Pull Requests on a Laravel App

Yes. An agent reviews each Laravel pull request for SQL injection, missing authorization and leaked secrets, and comments only where the diff is unsafe.

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

## What the agent does

The agent checks out the open pull request and reads the diff against the authorization the application already uses. In a Laravel 11 app like laravelio/laravel.io, composer.json requires php 8.4.*, laravel/framework ^11.5, livewire/livewire ^3.3 and pestphp/pest ^3.0, and the tree has no Dockerfile. Routes are split between routes/web.php and routes/api.php. bootstrap/app.php sends guests to the login route, appends RedirectIfBanned and DisableFloc on the web stack, and calls throttleApi(). A new web route is judged by its neighbours. Forum writes go through Forum\ThreadsController, which applies Authenticate and EnsureEmailIsVerified except on overview and show, then calls $this->authorize with a ThreadPolicy ability: UPDATE and DELETE allow the author, a moderator or an admin; LOCK is moderator or admin only; REPORT_SPAM refuses a report against either.

It then follows each new request field to where it is stored. Admin ban and unban are the pattern for a privileged write: Admin\UsersController stacks Authenticate with VerifyAdmins, and VerifyAdmins throws a 403 unless the user can UserPolicy::ADMIN, after which ban still calls $this->authorize(UserPolicy::BAN, $user). BanRequest::authorize returns true and only validates reason as a required string, so the policy in the controller is the real check. A pull request that copies the form request and skips the policy is the finding. ArticlesController::show uses abort_unless so an unpublished article is a 404 unless the viewer wrote it or is an admin or moderator. Livewire is in the same net: EditReply authorizes ReplyPolicy::UPDATE before it saves. routes/api.php only registers article store, update and delete, behind throttle:api.

SQL is read the same way. The queries already in the tree bind their values. Thread.php uses DB::raw only inside an aggregate, avg(datediff(replies.created_at, threads.created_at)), with no request input in the string. User.php uses selectRaw to count solutions and articles, again with no concatenated parameter. A new whereRaw, orderByRaw or selectRaw that interpolates a query string, a header or a route parameter is the comment, and the comment names the exploit in one sentence. A query that goes through the query builder with bindings is left alone.

Secrets are a separate pass over the diff. .env.example lists the names and leaves the values empty: APP_KEY, DB_PASSWORD, GITHUB_ID, GITHUB_SECRET, ALGOLIA_SECRET, VITE_ALGOLIA_SECRET, BLUESKY_PASSWORD, TELEGRAM_BOT_TOKEN, FATHOM_TOKEN and UNSPLASH_ACCESS_KEY. VITE_ALGOLIA_SECRET is the one that would be compiled into the browser bundle. phpunit.xml already sets APP_ENV to testing, DB_DATABASE to testing, and TELEGRAM_BOT_TOKEN and FATHOM_TOKEN to placeholders. The agent does not flag those existing lines. It flags a new committed .env, a real token added to a fixture, or a secret pasted into a workflow. It posts one inline comment per finding, with the file and line, and posts nothing when the diff adds no new input path, route or secret. It does not push commits.

## 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. Check out its branch and read the diff. Trace every new request parameter, Livewire property and route parameter to where it reaches Eloquent, the filesystem or a shell. Compare each new route and Livewire action with its neighbours: in this app, forum writes authorize through ThreadPolicy, admin writes pass VerifyAdmins and UserPolicy, and BanRequest::authorize returning true is not itself a check. Flag whereRaw, orderByRaw or selectRaw that interpolates request input; leave bound query-builder calls and the existing DB::raw aggregate in Thread.php alone. Look for secrets in the diff, including a committed .env and any VITE_ variable that would ship to the browser. Do not flag the placeholders already in phpunit.xml. Post findings as inline review comments with the exploit in one sentence each. Post nothing if there are none. Do not push commits.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app/pull/42"
    }
  ]
}
```

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 a review on the pull request that was named in resources, not a second pull request. Each finding is an inline comment on the new or changed line, and the comment states the exploit in one sentence: who can call the route, and what the unbound input reaches. When the diff adds no new input path, route or secret, the agent posts no review and says so in its answer. The session read includes the repository and the pull request URL, and the transcript keeps every file the agent opened and the diff it traced.

A reviewer treats a comment as a lead. They check that the cited line is in the diff, that the neighbouring controller authorizes the way the comment says, and that a fix would match ThreadPolicy or VerifyAdmins. No comments means the agent finished the trace, which the transcript shows.

Nothing in the default branch changes until a person merges. The agent does not rewrite the patch. If the author pushes more commits, a new run reviews the updated diff the same way, and earlier comments stay on the lines they were attached to.

## Run it on every pull request

In the Project, an automation with a GitHub trigger on pull_request.opened and pull_request.synchronize starts this review for each push. Only work by an owner, member or collaborator fires it, and pull requests from forks never do. The run checks out that pull request's branch because resources can be a .../pull/<n> URL.

From your own server, send the same POST /o/{org}/sessions when your webhook receives the pull request, with that pull request's URL in resources. A webhook URL on the API key then receives run.completed, run.failed or run.cancelled when the turn finishes. A key runs at most max_concurrent sessions at once, 4 by default, and the next start answers 429, so a backlog of open pull requests should be queued.

## Why run it on Rush?

A missing authorize call ships when the security pass is skipped on a busy pull request. On Rush each pull request is reviewed in its own sandbox, on that branch, with this app's policies and form requests in front of the agent. Closing a laptop does not stop the review.

The proof is the comment, or the explicit absence of one, plus the transcript of the files read. Several pull requests can be in review at once, each with its own clone, up to the key's concurrency limit. A person still decides what to change; the agent does not push.

## Frequently asked questions

### Will it push commits or open its own pull request?

No. The request tells it to post inline review comments and not to push. The session's pull request URL is the one you put in resources. If you want a fix as well as a review, that is a different request, and it should say so.

### Does a FormRequest that returns true from authorize pass the review?

Not by itself. BanRequest in this app returns true and validates the reason string, while Admin\UsersController performs the real check with VerifyAdmins and UserPolicy::BAN. The agent looks for that second check. A new endpoint whose only gate is authorize(): true is a finding.

### Does it run the Pest suite?

The review is of the diff, so it does not need a database to comment. The README runs the suite with vendor/bin/pest -p, and phpunit.xml points DB_DATABASE at testing. Ask for that in the prompt if you want the existing feature tests run before the agent comments. tests/Feature/AdminTest.php already covers who can ban a user, which is the behaviour a new admin route should match.

### What about a pull request from a fork?

A Project automation on the GitHub trigger does not start for a fork. If you send the pull request URL yourself, Rush still has to reach that branch through your GitHub App installation. A repository the installation does not cover is 422 RESOURCE_NOT_ACCESSIBLE, and no run starts.

### Will it report the keys already in phpunit.xml?

No. Those values are the suite's existing placeholders, and the prompt says to leave them. A new secret in the diff, or a VITE_ variable that would be bundled for the browser, is what gets a comment, and the comment names the file rather than repeating the secret.
