# Django

> Monitor a Django app: a request-logging middleware with status, auth signals for sign-ins, and beam monitor reading the logs with the Django rules on.

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

## How Beam reads a Django 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.

## Log each request with its status

```python
# yourproject/beam_middleware.py
import json, logging, time

log = logging.getLogger("beam.requests")

class BeamRequestLog:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        started = time.monotonic()
        response = self.get_response(request)
        log.info(json.dumps({
            "method": request.method,
            "path": request.get_full_path(),
            "status": response.status_code,
            "ip": client_ip(request),
            "user_agent": request.META.get("HTTP_USER_AGENT", ""),
            "host": request.get_host(),
            "duration_ms": round((time.monotonic() - started) * 1000),
        }))
        return response

def client_ip(request):
    # Behind one trusted proxy, the client is the first X-Forwarded-For entry.
    forwarded = request.META.get("HTTP_X_FORWARDED_FOR")
    return forwarded.split(",")[0].strip() if forwarded else request.META.get("REMOTE_ADDR", "")
```

```python
# settings.py
MIDDLEWARE = ["yourproject.beam_middleware.BeamRequestLog", *MIDDLEWARE]

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {"plain": {"format": "%(message)s"}},
    "handlers": {
        "requests": {"class": "logging.handlers.WatchedFileHandler", "filename": "/var/log/app/requests.log", "formatter": "plain"},
        "security": {"class": "logging.handlers.WatchedFileHandler", "filename": "/var/log/app/security.log", "formatter": "plain"},
    },
    "loggers": {
        "beam.requests": {"handlers": ["requests"], "level": "INFO", "propagate": False},
        "beam.security": {"handlers": ["security"], "level": "INFO", "propagate": False},
    },
}
```

Put the middleware first so it logs requests other middleware rejects. Only trust `X-Forwarded-For` behind a proxy you control.

## Log sign-ins, admins and keys

```python
# yourapp/signals.py (import it in your AppConfig.ready())
import logging
from django.contrib.auth.signals import user_logged_in, user_login_failed
from django.dispatch import receiver
from yourproject.beam_middleware import client_ip

security = logging.getLogger("beam.security")

@receiver(user_logged_in)
def on_login(sender, request, user, **kwargs):
    security.info("login succeeded user=%s ip=%s", user.pk, client_ip(request))

@receiver(user_login_failed)
def on_login_failed(sender, credentials, request=None, **kwargs):
    ip = client_ip(request) if request else ""
    security.warning("login failed user=%s ip=%s", user_id_for(credentials.get("username")), ip)
```

Where you grant staff or superuser rights, or issue API keys:

```python
security.info("granted admin role to user=%s by user=%s", target.pk, request.user.pk)
security.info("API key created user=%s", request.user.pk)
security.info(json.dumps({"event": "llm_usage", "user": str(request.user.pk), "model": model, "total_tokens": usage.total_tokens}))
```

## Run `beam monitor` next to the app

```sh
npm install -g @agent-beam/beam
beam monitor --stack django --file /var/log/app/requests.log --file /var/log/app/security.log --app /srv/app
```

`--stack django` 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 django --file /var/log/app/requests.log --file /var/log/app/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.

With gunicorn's default access log, you can also add `--file` for it: it's in the combined format Beam reads.
## 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 }}
```

Pin your Python dependencies (`requirements.txt` with `==`, `poetry.lock`, `uv.lock` or `Pipfile.lock`): unpinned ranges aren't 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 |
| --- | --- | --- |
| Django debug toolbar served publicly | `/__debug__/` answered 2xx in production | High |
| Failed sign-in bursts | Many `login failed` lines from one address, including on `/admin/` | High |
| Sign-in after failures | `login succeeded` right after a run of failures | Critical |

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:

- `DEBUG = False` in production, and `django-debug-toolbar` only in development settings.
- Move `/admin/` to an unguessable path or put it behind your SSO, and rate-limit sign-ins (django-axes).
- Set `ALLOWED_HOSTS` and `SECURE_PROXY_SSL_HEADER` correctly.
## 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 |
