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.
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.jsonSCOUT_ENABLED=trueSCOUT_USERNAME=adminSCOUT_PASSWORD=something-longSCOUT_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.
/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.
| Mode | What it sends | When |
|---|---|---|
| passive | Nothing. 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. |
| safe | Harmless probes: a GET for /.git/config, a version banner, a canary string. The default. | Routine. Nothing it sends would be logged as an attack. |
| active | Real 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. |
| intrusive | Brute force, load, state-changing requests. | A throwaway environment. Not production, not staging that anyone depends on. |
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.
