# Laravel

> Monitor a Laravel app: a terminable middleware logging each request with status, auth event listeners for sign-ins, and beam monitor with the Laravel and PHP rules, Ignition included.

Source: https://docs.agentbeam.com/cookbooks/frameworks/laravel

## How Beam reads a Laravel app

There's no in-process SDK for this stack, so the app writes what Beam needs to its logs and [`beam monitor`](https://docs.agentbeam.com/agent-cli/app-monitoring#install-and-start) reads them on the same server or container. Only redacted findings leave it. Two kinds of line:

- **One JSON line per request**, with `method`, `path` (with the query string), `status`, `ip`, `user_agent`, `host` and `duration_ms`. The status is what lets Beam tell an attack your app refused from one it answered.
- **Security events as plain lines**, worded like this (Beam matches the wording and reads `user=` and `ip=`):

```text
login failed user=42 ip=203.0.113.7
login succeeded user=42 ip=203.0.113.7
granted admin role to user=99 by user=12
API key created user=12
{"event":"llm_usage","user":"42","model":"gpt-5","total_tokens":1834}
```

Use an opaque user id, never an email. New outbound hosts are only reported by the Node.js SDK; for this stack, restrict outbound traffic at the network instead.

## Before you start

Do steps 1 and 2 of the [general cookbook](https://docs.agentbeam.com/cookbooks/app-monitoring#step-1-turn-it-on-and-create-the-apps-token) first: App monitoring is on, the app is added, and its `beam_app_` token is in your secret store as `BEAM_APP_TOKEN`. Everything below needs `@agent-beam/beam` 0.2.32 or newer.

## Add two log channels

```php
// config/logging.php, inside 'channels'
'beam_requests' => ['driver' => 'single', 'path' => storage_path('logs/beam-requests.log'), 'formatter' => Monolog\Formatter\LineFormatter::class, 'formatter_with' => ['format' => "%message%\n"]],
'beam_security' => ['driver' => 'single', 'path' => storage_path('logs/beam-security.log'), 'formatter' => Monolog\Formatter\LineFormatter::class, 'formatter_with' => ['format' => "%message%\n"]],
```

## Log each request with its status

```php
// app/Http/Middleware/BeamRequestLog.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class BeamRequestLog
{
    public function handle(Request $request, Closure $next)
    {
        $request->attributes->set('beam_started', microtime(true));
        return $next($request);
    }

    public function terminate(Request $request, $response): void
    {
        Log::channel('beam_requests')->info(json_encode([
            'method' => $request->method(),
            'path' => $request->getRequestUri(),
            'status' => $response->getStatusCode(),
            'ip' => $request->ip(),
            'user_agent' => (string) $request->userAgent(),
            'host' => $request->getHost(),
            'duration_ms' => (int) round((microtime(true) - $request->attributes->get('beam_started', microtime(true))) * 1000),
        ], JSON_UNESCAPED_SLASHES));
    }
}
```

Register it globally (in `bootstrap/app.php` on Laravel 11+: `->withMiddleware(fn ($m) => $m->prepend(\App\Http\Middleware\BeamRequestLog::class))`). Keep `JSON_UNESCAPED_SLASHES`: escaped slashes would hide paths like `../../etc/passwd` from the rules. Configure `TrustProxies` so `$request->ip()` is the client.

## Log sign-ins, admins and keys

```php
// app/Providers/AppServiceProvider.php, in boot()
use Illuminate\Auth\Events\{Failed, Login};
use Illuminate\Support\Facades\{Event, Log};

Event::listen(Login::class, fn (Login $e) =>
    Log::channel('beam_security')->info("login succeeded user={$e->user->getAuthIdentifier()} ip=".request()->ip()));
Event::listen(Failed::class, fn (Failed $e) =>
    Log::channel('beam_security')->warning("login failed user=".($e->user?->getAuthIdentifier() ?? '')." ip=".request()->ip()));
```

Where you grant admin rights or create tokens (for example `$user->createToken()` with Sanctum):

```php
Log::channel('beam_security')->info("granted admin role to user={$target->id} by user={$request->user()->id}");
Log::channel('beam_security')->info("API key created user={$request->user()->id}");
```

## Run `beam monitor` next to the app

```sh
npm install -g @agent-beam/beam
beam monitor --stack laravel --file /srv/app/storage/logs/beam-requests.log --file /srv/app/storage/logs/beam-security.log --app /srv/app
```

`--stack laravel` turns on this framework's rules and rolls up attacks on frameworks you don't run; `--app` also checks the app's lockfiles daily (and would detect the stack on its own). Keep it running with systemd:

```ini
# /etc/systemd/system/beam-monitor.service
[Unit]
Description=AgentBeam App monitoring
After=network-online.target

[Service]
EnvironmentFile=/etc/beam/app.env
ExecStart=/usr/bin/env beam monitor --stack laravel --file /srv/app/storage/logs/beam-requests.log --file /srv/app/storage/logs/beam-security.log --app /srv/app
Restart=always
User=beam

[Install]
WantedBy=multi-user.target
```

```sh
sudo install -m 600 /dev/null /etc/beam/app.env && echo "BEAM_APP_TOKEN=beam_app_..." | sudo tee /etc/beam/app.env >/dev/null
sudo systemctl daemon-reload && sudo systemctl enable --now beam-monitor
```

In Docker or Kubernetes, run the same command in a sidecar that mounts the log directory read-only, with `BEAM_APP_TOKEN` from a secret.
## Prove it works

```sh
beam monitor status   # app, level, stack, usage
beam monitor test     # a test finding through your alert workflows
curl -s "https://your-app.example.com/?id=1%20UNION%20SELECT%201" -o /dev/null
```

Within a minute, the SQL injection test shows on **App monitoring**: under **Needs action** if the app answered 2xx, or counted under "needed nothing from you" if it refused it. Then sign in to a test account with a wrong password ten times and the right one once: **Sign-in succeeded after 10 failed attempts** appears as critical.
## Check dependencies on every pull request

```yaml
# .github/workflows/beam-cve.yml
name: Dependency check
on: [pull_request]
jobs:
  cve:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npx -y @agent-beam/beam cve check . --fail-on high
        env:
          BEAM_APP_TOKEN: ${{ secrets.BEAM_APP_TOKEN }}
```

`composer.lock` is checked.
## What Beam watches for on this stack

Besides every [built-in rule](https://docs.agentbeam.com/agent-cli/app-monitoring#levels-and-detection) and the [attack-in-progress signals](https://docs.agentbeam.com/agent-cli/app-monitoring#signs-an-attack-is-underway):

| Rule | Means | Severity |
| --- | --- | --- |
| Laravel Ignition remote code execution | A request to `/_ignition/execute-solution` (CVE-2021-3129) | Critical |
| Laravel debug tools or logs served | Telescope, Horizon, Debugbar or `storage/logs/laravel.log` answered 2xx | High |
| PHPUnit eval-stdin | `vendor/phpunit/.../eval-stdin.php` (CVE-2017-9841) | Critical |
| PHP-CGI argument injection | `?-d allow_url_include=1` style requests (CVE-2024-4577) | Critical |
| PHP stream wrappers | `php://filter`, `phar://` and similar in a request | High |
| .env served | `.env` answered 2xx | High |

Attacks on other frameworks (WordPress probes, Laravel or Spring exploits, and so on) are rolled up as **Not your stack** when there's no status to say otherwise, and never alert.
## Close what Beam would catch

Beam raises these when they're found; better that there's nothing to find:

- `APP_DEBUG=false` and `APP_ENV=production`; keep `spatie/laravel-ignition` in `require-dev` and run `composer install --no-dev`.
- Gate Telescope and Horizon (`viewTelescope`, `viewHorizon`), and point the web root at `public/` only, so `vendor/`, `storage/` and `.env` are never served.
## Coverage checklist

| Feature | Done when |
| --- | --- |
| Request log with status | Findings show **Answered**, **Refused** or **Errored** |
| Stack declared | `beam monitor` prints the stack at start |
| Sign-in events | `login failed` and `login succeeded` lines are written |
| Admin and key events | Role grants and new keys are logged |
| LLM spend | An `llm_usage` line follows each model call |
| Dependency checks | The CI job runs on pull requests |
| Alerts | Critical pages someone, high reaches a channel ([step 8](https://docs.agentbeam.com/cookbooks/app-monitoring#step-8-send-alerts-to-the-right-place)) |
| Routine | Someone reads **Needs action** daily and **Attack analytics** weekly |
