ASP.NET Core

Monitor an ASP.NET Core app: a middleware logging one JSON line per request with Serilog, Identity sign-in events, and beam monitor with the .NET rules.

On this page

How Beam reads a ASP.NET Core app

There's no in-process SDK for this stack, so the app writes what Beam needs to its logs and beam monitor 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, client_ip, user_agent, host and duration_ms. The status is what lets Beam tell an attack your app refused from one it answered, and client_ip is the address Beam can pause.
  • Security events as plain lines, worded like this (Beam matches the wording and reads user= and ip=):
Code
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 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; pausing needs 0.2.35.

Log each request with its status

Code
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Formatting.Compact
dotnet add package Serilog.Sinks.File
Code
// Program.cs
using Serilog;
using Serilog.Formatting.Compact;

var requests = new LoggerConfiguration()
    .WriteTo.File(new CompactJsonFormatter(), "/var/log/app/requests.log")
    .CreateLogger();
var security = new LoggerConfiguration()
    .WriteTo.File(new CompactJsonFormatter(), "/var/log/app/security.log")
    .CreateLogger();

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<Serilog.ILogger>(security);
var app = builder.Build();
app.UseForwardedHeaders(); // configure KnownProxies so RemoteIpAddress is the client

app.Use(async (context, next) =>
{
    var started = System.Diagnostics.Stopwatch.GetTimestamp();
    await next();
    var request = context.Request;
    requests.Information("{method} {path} {status} {client_ip} {user_agent} {host} {duration_ms}",
        request.Method,
        request.Path + request.QueryString,
        context.Response.StatusCode,
        context.Connection.RemoteIpAddress?.ToString(),
        request.Headers.UserAgent.ToString(),
        request.Host.ToString(),
        (long)System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalMilliseconds);
});

Register it before your other middleware. The property names (method, path, status and so on) are what Beam reads.

Log sign-ins, admins and keys

With ASP.NET Core Identity, after PasswordSignInAsync:

Code
var result = await signInManager.PasswordSignInAsync(input.Email, input.Password, false, lockoutOnFailure: true);
var ip = HttpContext.Connection.RemoteIpAddress?.ToString();
if (result.Succeeded) security.Information("login succeeded user={user} client_ip={client_ip}", userId.ToString(), ip);
else security.Warning("login failed user={user} client_ip={client_ip}", userId.ToString(), ip);

Where roles or keys change: security.Information("granted admin role to user={user} by user={by}", target.Id.ToString(), actor.Id.ToString()) and security.Information("API key created user={user}", user.Id.ToString()). Pass ids as strings: Beam reads the user property only as a string.

Run beam monitor next to the app

Code
npm install -g @agent-beam/beam
beam monitor --stack dotnet --file /var/log/app/requests.log --file /var/log/app/security.log --app /srv/app

--stack dotnet 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:

Code
# /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 dotnet --file /var/log/app/requests.log --file /var/log/app/security.log --app /srv/app
Restart=always
User=beam

[Install]
WantedBy=multi-user.target
Code
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.

Pause attackers

Beam can pause a sender caught scanning the app, guessing passwords or sending critical exploits. On this stack the web server in front of the app does the refusing: beam monitor writes the paused addresses to deny lists and reloads it. Save the lists once:

Code
sudo install -d -o beam /etc/nginx/beam
beam monitor configure --deny-file /etc/nginx/beam/deny.conf \
  --auth-deny-file /etc/nginx/beam/deny-auth.conf --reload "sudo /usr/sbin/nginx -s reload"
Code
server {
  include /etc/nginx/beam/deny.conf;          # paused from the whole app
  location /Identity/Account/Login {
    include /etc/nginx/beam/deny-auth.conf;   # paused from sign-in only
    # ...your existing proxy_pass or fastcgi_pass
  }
  # ...your other locations
}

Let the beam user run the reload (beam ALL=(root) NOPASSWD: /usr/sbin/nginx -s reload in /etc/sudoers.d/beam), run sudo nginx -t, and restart the service. Pausing starts in dry run: the dashboard's Paused list shows who would have been paused. Turn it on under Settings > Pausing once that list looks right. Pauses use the client_ip property, which is safe once UseForwardedHeaders lists your proxies in KnownProxies.

No nginx you control (Heroku, Render, Cloud Run)? Pauses still show on the dashboard; refuse the address at your CDN or load balancer.

Prove it works

Code
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

Code
# .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 }}

Turn on NuGet lock files (<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>) so packages.lock.json can be checked.

What Beam watches for on this stack

Besides every built-in rule and the attack-in-progress signals:

RuleMeansSeverity
ASP.NET diagnostics servedelmah.axd or trace.axd answered 2xxHigh
Telerik UI upload exploitTelerik.Web.UI.WebResource.axd?type=rau (CVE-2019-18935)Critical
web.config servedweb.config answered 2xxCritical

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:

  • Use UseExceptionHandler and never the developer exception page in production.
  • Remove ELMAH and tracing from public apps, and update or remove old Telerik UI.
  • Set ForwardedHeadersOptions.KnownProxies so client addresses can't be spoofed.

Coverage checklist

FeatureDone when
Request log with statusFindings show Answered, Refused or Errored
Stack declaredbeam monitor prints the stack at start
Sign-in eventslogin failed and login succeeded lines are written
Admin and key eventsRole grants and new keys are logged
LLM spendAn llm_usage line follows each model call
Dependency checksThe CI job runs on pull requests
AlertsCritical pages someone, high reaches a channel (step 8)
Pausingclient_ip in the request log, the deny lists included in nginx, and Settings > Pausing on after a dry run
RoutineSomeone reads Needs action daily and Attack analytics weekly

Set this up with your coding agent

Install the agentbeam-setup skill for Claude Code, Codex, Cursor, Gemini CLI, OpenCode and Copilot, then ask your agent to set up AgentBeam on this machine. Beam scans the skill before it installs.

npx -y @agent-beam/beam add skill agentbeam-setup -g --all-agents
Docs
Esc

Jump to a section

Actions