Security

Scout: scanning your own app

Scout is a security scanner that runs inside your API, the way GORM Studio and Pulse do. It checks the things that are true of a deployment rather than of a line of code: which headers you send, what your TLS offers, whether a .env is reachable, whether a dependency has a CVE anything actually calls.

Opt-in, unlike Sentinel and PulseScout is not in a new project. Run grit add scout when you want it. A scanner that appears because you ran grit new is one nobody decided to run, and an undecided scanner gets either ignored or trusted without being read.

Adding it

grit add scout

That writes apps/api/internal/routes/scout.go, adds four settings to your config and .env.example, and mounts the dashboard at /scout. It installs disabled. Turn it on when you have set a password:

# .env, beside grit.json
SCOUT_ENABLED=true
SCOUT_USERNAME=admin
SCOUT_PASSWORD=something-long
SCOUT_DB=scout.db

With no password it logs a line saying so and does not mount. The dashboard lists everywhere your application is weakest, which is the one page you least want to serve to anybody who finds the URL.

grit doctor reports both ways of getting this wrong: enabled with no password, where you think a scanner is running and none is, and enabled with a guessable one, which is an error in production. A project that has not added Scout reports nothing.

The part Grit can give it that a scanner usually cannot

A scanner looking at your application from outside sees URLs. It cannot see which of them are protected, so when it wants to report "this endpoint has no authentication" it has to infer that from the shape of the path, and the shape of the path does not say. /api/v1/auth/login and /api/v1/auth/me sit in the same place and want opposite things.

Nor can it ask the router. Gin's route table carries each route's method, path and last handler, and nothing about the middleware in front of it. The running process genuinely does not know.

Grit already solved this for its own Routes screen: grit parses your route files, resolves each group's middleware chain, and writes internal/access, which grit doctor fails on when it goes stale. grit add scout hands that table over, so Scout reads what each route requires instead of guessing:

POST /api/v1/auth/login middleware: []
GET /api/v1/auth/me middleware: ["AuthRequired"]
GET /api/v1/admin/blogs middleware: ["AuthRequired",
"RequireRole(ADMIN)",
"RequirePermission(blogs.view)"]

A route that should be behind a role and is not is then a finding Scout can state rather than suspect, and it finds it without sending a single request.

One rough edge, in active mode onlyScout currently treats any path containing /api as sensitive, so its missing-auth check reports every deliberately public route, your sign-in included, as lacking authentication. The check is active mode, so a default scan never runs it. Tracked at scout#2.

Modes

Every check declares the most aggressive thing it does, and a scan runs only the checks at or below the mode you asked for. The default is safe.

ModeWhat it sendsWhen
passiveNothing. Reads response headers, the TLS handshake, DNS records, your Dockerfile, your repository and the route table.A scan you can run against production without telling anyone first.
safeHarmless probes: a GET for /.git/config, a version banner, a canary string. The default.Routine. Nothing it sends would be logged as an attack.
activeReal injection payloads: SQL, command, path traversal, SSRF, auth bypass.A target you own, with permission, knowing it will appear in your logs as an attack.
intrusiveBrute force, load, state-changing requests.A throwaway environment. Not production, not staging that anyone depends on.
Active and intrusive are attacksThey are real payloads against a real application. Run them against something you own, on purpose, knowing your logs will record an attack because one happened.

What it checks

Headers

CSP, HSTS, clickjacking, nosniff, referrer policy, permissions policy, cookie flags, server version disclosure.

TLS

Weak protocols and ciphers, certificate chain and expiry, HSTS preload eligibility.

DNS

SPF, DKIM, DMARC, CAA, dangling CNAMEs and subdomain takeover.

Exposure

A reachable .git, a .env, directory listings, backup files, an open Swagger or metrics endpoint, a dashboard with no login.

Code

Secrets committed to the repository, dangerous sinks, insecure randomness, weak crypto, unpinned dependencies, secrets reaching the logs.

Dependencies

Reachable CVEs in your Go modules, via govulncheck. Reachable, not merely present: a CVE in code nothing calls is not a finding.

Config

Gin left in debug mode, default credentials, in-memory storage in production, Sentinel not enforcing, Pulse in dev mode.

Application

Reflected XSS, open redirect, SQL and command injection, path traversal, SSRF, CSRF forms, host-header injection. Most of these are active mode.

Auth

CORS policy, JWT header and claims, and the route table check below.

Findings you have already decided about

A finding can be marked accepted-risk with a date, or a false positive. An accepted risk reopens itself when the date passes, which is the difference between deciding to live with something and forgetting about it.

What it is not

Scout is not Sentinel. Sentinel is a WAF: it sits in the request path and blocks things as they happen. Scout runs on a schedule and tells you what it found. One defends, the other looks. They answer different questions and the answers do not substitute for each other.

It is also not a penetration test. It finds the classes of problem a tool can find. Business logic, a broken permission model, an authorization rule that is right for every case but one: those need somebody thinking about your application, and nothing here replaces that.