J0 beta

GET /invoices/8843 as the owner 200 as anyone else 200 one of these should have been a 403

Broken access control sits at the top of the OWASP Top 10 and is still found by hand, one identifier at a time. J0 is a lightweight workbench that replays every request you made as every other account, and proves what came back — locally, with no account and no cloud.

Beta. macOS verified, Linux and Windows unproven. There is no signed bundle yet, so what you download is the source and three commands.

9090 The port it listens on, from the moment the window opens. No setup step.
480 Tests passing — 407 Rust, 73 frontend, run before this page was written.
0 Requests a language model composes. The gate is a type, not a prompt.
1 of 3 Platforms verified. macOS runs; Linux and Windows are unproven, not broken.

GET /invoices/8843 as the owner · 200 · 41,208 B as a viewer · 200 · 41,208 B got through

Three moves, and none of them guess

You browse. J0 watches whose session carried what, sends it again as everyone else, and then tries to talk itself out of the result before it reports one.

  1. 01 · watch

    Whose session was that

    Every request is attributed to the credential set that carried it — including when two accounts arrive with differently named cookies. Names are recorded; values never are.

  2. 02 · replay

    As everybody else

    The whole credential set is exchanged, not just a cookie: auth headers, body tokens and all three anti-forgery carriers. Swap half of it and the rejection looks exactly like a control holding.

  3. 03 · prove

    A 200 is not a finding

    An identifier belonging to nobody, the request repeated, every credential stripped — then the question that settles it: could this account have obtained that identifier at all?

Numbers that carry their own bar

“Similar in shape and length” is the sentence that gets a report sent back. J0 carries the measurement that settled a finding into the write-up: the size gap, the threshold it was weighed against, and where that threshold came from.

The bar is learned per endpoint from its own repeated reads, so an endpoint that swings 19 % between two identical requests is not judged by the same bar as one that is byte-stable. Resemblance is never decisive on its own.

issues · F-014 cross-account read
what settles it owner values returned 3 of 17 present in the owner’s reply, absent from the viewer’s own measurements body similarity 93% size gap 4% vs 19% bar learned from this endpoint’s own repeat reads, not a constant probes ✓ unknown id 404 ✓ repeated identical ✓ no credentials 401 verdict got through

The shape of an Issues entry. The identifiers are illustrative; the format is the one the application prints.

GET /org/billing/invoices as a viewer · 403 denial fingerprint matches blocked · the control held

What it refuses to do

Most of what makes a security tool trustworthy is what it will not do. None of these is a setting, and none is on a roadmap to be relaxed.

  1. 01

    A skipped check never reads as clean

    Every refusal, every unsupported case, every control that was not reachable is recorded with its reason and appears in the report. A results list that does not say what it never reached reads as an all-clear, and an all-clear nobody earned is the most expensive thing a security tool can produce.

  2. 02

    No model composes a request

    A language model chooses what to look at and writes up what came back. It never builds a request, never picks a payload, never sees a credential. The sender accepts only a value minted from real captured traffic, so a string a model invented cannot reach it — the program would not compile.

  3. 03

    Nothing runs on its own

    No daemon, no schedule, no background scan that was already going when you opened the window. A run starts because somebody started it. The certificate authority is generated locally and offered as a download; it never lands in a system trust store on J0’s initiative.

  4. 04

    The bytes are preserved

    Duplicate headers, duplicate parameters, binary bodies and unusual encodings survive intact, and anything normalised is kept apart from the original. A tool that quietly canonicalises a request is reporting on a request it did not send.

  5. 05

    No severity score

    Severity is demonstrated impact, and only somebody who knows what the data is can judge it. Findings are bucketed by what to do next — dig, candidate, not applicable, merged — and never reduced to a number.

One window, eighteen tabs

Burp’s shape without its drawer of single-purpose extensions, grouped Capture, Manual tools and Analysis — and the proxy is already running when you get there.

Where each tool stands today. The right-hand column is the honest one.
ToolWhat it doesState
ProxyForward, CONNECT tunnel, HTTPS interception with a local CA, bidirectional WebSocket capture, Match & Replace rules applied in flight.built
HistoryByte-faithful storage in a SQLite project file, bodies de-duplicated, credential-set attribution per exchange, per-request notes.built
RepeaterTabbed and persisted, HTTP/1.1 and HTTP/2, per-tab send history, Send as another account, and a compare bench that runs the engine’s own comparison.built
IntruderSniper, battering ram, pitchfork, cluster bomb. List and number-range payloads, grep-match columns, outlier highlighting, hard caps.built
IssuesFindings worst-first with triage buckets, each carrying the measurement that settled it, and a coverage ledger beside them.built
Agentic pentestLogs each account in, crawls their features in a browser it drives click by click, then runs the cross-account scan and the passive passes.built
Upstream proxyChaining J0 through another proxy.deferred
HTTP/2 at the proxyThe Repeater speaks HTTP/2; the interception path does not yet.deferred
WebSocket injectionFrames are captured and viewable. Editing one in flight is not built.deferred

agent · list_sessions owner cookies: JSESSIONID, csrf_token names only no value returned

A model can drive it. It cannot send anything.

The AI layer is optional and off until you turn it on. It is bounded by the Rust engine rather than by a prompt, which is the only kind of boundary worth having.

Pick a brain, or none

The deterministic engine runs the whole cross-account scan with no model at all. Point it at Claude Code, at any OpenAI-compatible endpoint including a local one, or at a hosted provider — the run is the same run, and every one of them is recorded the same way.

It drives a real browser

On an agentic run the model logs each account in through the proxy and crawls their features click by click, choosing controls by role and accessible name. It never reads a field’s value. The click policy refuses destructive controls, and a feature it declines is written down as declined.

The prompt is data, not a constant

The system prompt is an editable tree of files, one source for every driver. The boundary clauses are not in that tree and cannot be edited out — they are compiled in, and a test fails the build if any driver loses one.

  • may

    Read the capture through 17 tools over MCP, ask the engine questions, choose which feature to look at, and draft the write-up.

  • may not

    Compose request bytes. The sender accepts only a value minted from real captured traffic, so a string a model invented cannot reach it — the program would not compile.

  • sees

    Cookie and header names. Page reads are redacted: tokens and JWTs scrubbed, object identifiers and UUIDs deliberately kept, because those are the thing under test.

  • never sees

    A credential value, in any carrier. The tool API has no shape that could return one.

  • fires

    Only behind an explicit toggle. Every agentic request is marked as one in HTTP history, with the account it ran as.

  • never decides

    A verdict. The engine settles findings; the model explains them. A run is recorded with its brain, scope and ending — the deterministic control included, because a control that is not recorded is not a control.

Coverage is an output, not a footnote

Each class of check reports tested, not measured, or refused — with the reason attached. Not measured is never rendered as a pass, anywhere in the application.

The same rule governs an agentic run: every feature it walks is recorded as tested, skipped or never reached, and a refusal to touch a destructive control is written down rather than quietly passed over.

An example coverage ledger, in the vocabulary the Issues tab uses.
ClassReachState
Access control389 of 412 endpointstested
Reflected XSSpassive, on captured repliestested
Business logicleads only, never confirmedcandidates
Injectionout of scope by designnot measured
File uploadrefused: destructive controlnot reached

Twenty-three endpoints were refused in this example, each with its reason, and all twenty-three appear in the report.

Running in three commands

There is no installer yet — signing, checksums and an SBOM are the work left before a tagged release. Until then it builds from source, and the first launch needs no configuration: pick a project file, and the proxy is already listening.

# Rust on PATH, once per shell $ source "$HOME/.cargo/env" $ pnpm install $ pnpm tauri dev

Then point your browser at 127.0.0.1:9090 and download the CA from the landing page it serves. Verified on macOS aarch64; the Linux and Windows jobs in the CI matrix have not been run.