Guide

Scanning & Issues

gori includes automated analysis that runs alongside your manual testing. Probe watches traffic for issues, the Param Miner discovers hidden inputs, and Issues is where results get triaged.

Probe: the Scanner

Probe groups security issues by type and severity. Its passive checks run as you browse (with zero extra requests), inspecting History flows and Repeater send results.

Its active checks are deliberately light-touch: a handful of safe, low-volume probes over traffic you've already captured. By default only safe methods (GET / HEAD) are probed, each unique surface is tested once, and nothing goes out until you arm active mode. It's built to confirm a quick hunch (a parameter reflects, an origin is honored) while keeping your footprint quiet.

Re-sending an unsafe method (POST / PUT / PATCH / DELETE) can mutate server state, so it is always opt-in. Tick unsafe methods in the per-flow Run active scan popup for a single deliberate re-send, or switch Probe to AGGRESSIVE mode, which also probes unsafe methods automatically and raises the per-rule caps (wider param sets, a wider forbidden-bypass header set). Both stay inside your project scope, so an out-of-scope host is never touched.

gori Probe scanner listing passive issues grouped by severity and category: permissive CORS, missing CSP and HSTS, cookie flag issues, and cacheable responses, each with an affected host
Probe surfaces passive issues as you browse (CORS, cookie hygiene, missing security headers, info leaks), grouped by severity and category.
Category What it covers
headers Security headers (HSTS, CSP incl. report-only-only, XFO, Permissions-Policy, …), cleartext Basic auth, a password submitted or a login form served over http://, mixed content, cacheable API responses, a Set-Cookie a shared cache may store, MIME-type confusion (an HTML body under a sniffable type with no nosniff, JSON served as text/html), JWT weaknesses (alg:none, non-standard alg, no exp), cross-origin subresources without integrity
cookies Secure / HttpOnly / SameSite and related cookie hygiene
tech Technology and protocol fingerprints (also surface on the Project tab)
infoleak Body disclosures, secrets in URLs / WS frames, GraphQL introspection, source maps shipped with production scripts, directory listings, sensitive JWT claims, exposed configuration and diagnostic artifacts (.env, .git/config, phpinfo(), .htpasswd, wp-config credentials, Spring actuator env), framework debug mode and interactive debuggers reachable in production (Symfony, Werkzeug/Flask, Django, Laravel, Rails, ASP.NET), a suspected subdomain takeover, an internal hostname or RFC 1918 address in a response header, and native-serialization blobs in cookies / parameters / hidden fields (Java, .NET BinaryFormatter/ViewState, PHP) β€” the insecure-deserialization surface
cors Wildcard / null origin / credentialed misconfigurations; a reflected origin cached without Vary: Origin; active origin reflection
client Client-side suspicions in page and bundle scripts: DOM-based XSS (source into sink), DOM clobbering, prototype pollution, and postMessage weaknesses. Heuristic, so treat as leads to confirm
active Confirmed by a light-touch probe: reflected parameters, backslash-powered injection points, open redirect, CRLF/response-header & host-header injection, access-control bypass (spoofed client-IP / path normalization / URL-rewrite headers), NGINX alias & parameter path traversal, server-side template injection, and Next.js server-action missing authorization (re-sends a Next-Action request with the session cookie/Authorization stripped β€” needs unsafe/AGGRESSIVE, since actions are POST). error-based SQL injection (a syntax-breaking payload per query parameter, flagged when a database-error signature appears in the probe but not in the clean baseline), and blind SSRF confirmed out of band β€” a URL parameter is pointed at an OAST payload and the finding is raised when the server calls back. Blind OS command injection is confirmed the same way β€” a shell-breakout payload is appended to a command/diagnostic parameter (cmd, ping, host, …) and the finding is raised when the server's shell calls the OAST listener back. GraphQL introspection is confirmed actively too (recorded under infoleak)

Two active rules do not behave like the rest, and the Rules sub-tab says so on the row:

Severities run info, low, medium, high, critical. Headless gori run probe runs passive checks by default, and pass --active to also run active checks.

Findings are grouped by check and host, so one row can stand for dozens of hits. Open it and the AFFECTED URLS list is the evidence: ↑/↓ walk it and Enter opens the flow that URL was captured on, in the same detail view History uses. o opens the finding's sample flow, r sends it to the Repeater, and y copies the selected URLs (or all of them).

Run analysis headless. By default it reads what's already captured (History + Repeater responses) and sends nothing, or pass --active to send probe requests:

gori run probe                       # passive issues
gori run probe --active              # include active checks (sends probe requests)
gori run probe --active --unsafe     # also re-send unsafe methods (may mutate server data)
gori run probe --active --aggressive # wider caps + unsafe methods (authorized targets only)
gori run probe --severity high       # only high-severity
gori run probe --category cors       # a single category
gori run probe -q 'host:example.com' # filter History with QL (Repeater still scanned)

Param Miner

The Miner discovers parameters a server accepts but doesn't advertise. Point it at a flow and it probes candidate names across locations: query string, form body, multipart/form-data, JSON (including nested objects and array roots), headers, and cookies. It buckets guesses efficiently and reports the ones that change the response. Multipart is applicable but off by default (a captured file part would be re-sent on every request); enable it with --locations multipart or its checkbox.

gori run mine <flow-id> \
  --locations query,headers \
  --wordlist params.txt \
  --bucket 50

A mine is latency-bound rather than CPU-bound β€” it sends a bucket, waits, bisects, waits β€” so what it mostly costs is round trips. Two things keep that count down: the run reuses one connection across its probes (one TCP, and on https one TLS, handshake per worker instead of one per probe β€” turn it off with the reuse connections checkbox, --no-keep-alive, or keep_alive: false when the target behaves per-connection), and every location is mined through one shared pool of workers, so three locations do not cost three times one location and the tail of a bisection no longer runs alone.

The Miner tab is hidden by default. Enable it from the command palette (Ctrl-P) when you need it.

Discover: Spider & Brute-Force

Where the Miner finds hidden inputs, Discover finds hidden endpoints. It spiders a target (following links you never clicked) and brute-forces unlinked directories and paths (/admin, .git/config, /api/v2). It lives as a sub-tab under the new Target tab, next to the Sitemap, and every endpoint it finds flows straight into that Sitemap.

Start a run from where you already are: on a Sitemap node or a History flow, press Space and pick Discover here. A small popup lets you choose the exploration style (spider, brute-force, or both, the default), a max depth, the crawl scope, and concurrency. The run happens in the background: watch the bottom bar, pause or stop it from the Discover sub-tab (^X stop, p pause), and jump to the results from the completion notification.

A finished run is not just a list of URLs. Every finding is stored with the request Discover framed and the response the origin sent back, so in the FINDINGS table Enter (or o) opens that exchange in the same detail view History uses: headers, body, pretty-printed JSON, and from there ^R to the Repeater. Headless runs and MCP runs store the same bytes, so a finding is openable with gori run show or get_flow too.

Discover is built for tight false-positive and false-negative rates on real sites:

Each run reports its FP/FN figures: how many probes the calibrator suppressed, how much exploration the traps guards cut, and the confidence spread of what it kept.

Headless, it's gori run discover, and it's exposed to agents over MCP (discover_start / discover_status / discover_results / discover_stop):

gori run discover --target https://target.example \
  --max-depth 3 \
  --extensions php,json,bak \
  --format jsonl

Discover sends real, unsolicited traffic to the target. Only run it against systems you are authorized to test.

From the Sitemap, Space also offers Send to Repeater, which opens the selected endpoint's captured request in the Repeater workbench.

Issues

Issues is your triage list. Promote anything worth tracking (from Probe, the Fuzzer, the Miner, or your own inspection) into an issue with a severity and a status, and jump straight back to the evidence flow. Issues can be exported for reporting:

gori run issues --format markdown --export report.md
gori run issues --format sarif --export issues.sarif   # for GitHub code scanning / a CI dashboard

In the TUI, ⇧E asks for the format and then the destination path. See Export the report for what a SARIF result carries.

⇧X (or Space β†’ X) clears the tab: every issue in the project, with its notes, CVSS score and evidence links. It asks first and names the total β€” which is the whole project, not the rows a filter is showing and not the marked set, so it is the one issues key that ignores both. ⇧X is the same clear-all key History, Probe, Authorize and the ACTIVITY feed answer, each in its own tab.

Marking issues (multi-select)

The list marks the same way History does. Press t to mark the issue under the cursor and step down, so a run of t marks consecutive rows. Shift-↑ / Shift-↓ extend a contiguous range from where you started, Shift-T marks everything the current filter shows, and Esc clears the marks. Marked rows get a full bar in the gutter and the filter row shows a live 3 marked count.

Letting go of Shift ends the range: a plain ↑ / ↓ (or PgUp / PgDn, or a click on another row) hands the range back and moves on, the way a GUI list collapses its highlight. Marks you placed deliberately with t or Shift-T stay, which is what makes a discontiguous set possible. The mouse wheel only scrolls, so it never drops a mark.

Marks change what the space menu acts on, not which actions exist:

the effective target is the marks if any are set, else the cursor row

So / status:open severity:low β†’ Shift-T β†’ Space β†’ c β†’ False positive re-triages the whole batch in one pick. The menu title reads SPACE Β· 3 MARKED and the entries rename themselves (Delete 3 issues) so a batch is never a surprise.

Action Key Over marks
Set severity Space s One pick, written to every marked issue
Set status Space c One pick β€” the bulk "false positive" / "resolved" pass
Delete Space d One confirm for the whole set

Marks survive a filter change, a re-sort (including the one your own severity edit causes), and leaving the tab and coming back; the count chip tells you how many are currently off-screen. Opening an issue pins the actions to that one issue β€” marks are a list-level idea. Export always writes the full report, so its menu entry says (all) while marks are set.

Notes & Comparer

Two more tools round out analysis:

Key Action
← / β†’ Diff the requests or the responses
n / ⇧N Jump to the next / previous changed row (wraps; the footer shows 3/8)
f Fold the unchanged runs to β‹― N unchanged lines β‹―, keeping 3 lines of context
↑ / ↓, ⇧↑ / ⇧↓ Move the row cursor Β· grow a whole-row selection
y Copy the selection β€” or the whole diff β€” as unified text
⇧← / ⇧→ Scroll both columns sideways together
s Swap A ⇄ B

On a changed row only the parts that actually differ are lit red/green; what both sides share is dimmed, so a re-signed token or one flipped JSON value is visible without reading the line.

Diff: what changed since last time

The Comparer diffs two messages. Retesting asks the same question one level up β€” what changed since the last engagement? β€” and that is the Diff sub-tab under Target, next to the Sitemap whose folding it keys on.

Slots hold projects, not flows: a picks the baseline (the earlier engagement), b defaults to the project you have open, s swaps them, and r re-runs the read. Nothing is sent β€” both sides are captured traffic. Rows are endpoints, and ↡ (or o) hands the selected endpoint's capture from each side to the Comparer for the byte-level answer.

Endpoint identity is the whole game. Two engagements never capture the same identifiers, so a diff keyed on literal paths reports every row twice β€” once removed, once added β€” and tells you nothing. Endpoints are therefore keyed by the same folded template the Sitemap draws: /users/{uuid}, /items/{n}, /search with its query variants folded on. The fold runs over the union of both sides, so a route that met the fold threshold on only one side still matches itself on the other.

Five verdicts, and the split between the last two is the point:

Verdict Meaning
added Captured in B, never captured in A
gone Captured in both β€” and every answer B got was 404/410 where A was reachable
changed Captured in both; status class, auth, content type, or size moved beyond tolerance
same Captured in both, equivalent
not seen In A, and B captured no request to it at all β€” a coverage gap, not evidence of removal

A thinner retest visits fewer endpoints. Collapsing "we did not go there" into "it is gone" would report a short afternoon as a wave of fixes, so the two are different verdicts and the caveat sits on the header where it cannot scroll away. Both sides' flow, endpoint and host counts print beside the numbers for the same reason.

changed is judged by a tolerance band β€” the same calibration the Repeater's minimizer and the Miner use β€” not byte equality, so a page whose length wanders between captures reads as unchanged. Status is compared by class, so a 200 that became a 201 is not a finding while a 200 that became a 403 is (reported on its own auth axis).

v cycles the verdict lens; the counts always cover all five whatever the lens shows. Headless, it is gori run diff β€” with --format md the report is a section you paste into the deliverable β€” and over MCP it is diff_projects.

A row is meant to leave the tab. A retest's deliverable is a list of findings and this is the tab that produces its input, so ⇧F files the selected endpoint as an Issue and n records it as a Note β€” same text, one without the form, and neither moves you off the list. Both carry what only this tab knows: which two projects were compared and where their databases are, what each side actually answered, and which axis moved. The capture behind the side that is your open project is linked as evidence, not copied; the other side's flow is named with its database, because entity_links do not cross projects. A not seen row files as info and says in its own words that the newer capture never requested the endpoint β€” it will not claim a removal the diff did not observe. The same sentence rides on every row of --format json, so an agent filing one issue per row cannot lose the distinction either.

Issues, notes, repeaters, and fuzz/miner sessions can be linked so you jump from an issue straight back to the evidence flow or the session that produced it. Issues record an optional CVSS vector or numeric score, and the severity follows from it β€” visible in the Issues list, in cvss:>=7 filters, and in every export. Space β†’ Set CVSS on an issue (or ↡ on the cvss row of the issue form) opens the calculator: type or paste a vector β€” or a bare score like 8.8 β€” into its vector: row, or build one from the base metrics with ←/β†’, and the two stay in step. The version: row picks 3.1 or 4.0; each keeps its own selections, because the two versions ask different questions (v4.0 adds Attack Requirements and splits impact into Vulnerable/Subsequent systems) and are not convertible. A pasted vector opens on its own version, and any version the parser knows β€” including v2 β€” is stored and scored as typed even though the builder only writes 3.1 and 4.0. Space β†’ Link… from History, the Repeater, the Fuzzer, or the Miner opens one card holding every issue and every note, with + New issue… / + New note… pinned above them β€” so filing a brand-new issue for what you are looking at, already linked, is the same keystroke as attaching it to an existing one. Type to filter by title, host, status, or the words issue / note; whatever you typed becomes the new issue's title if you land on the create row.

Next Steps