The eight phases
1
preflight
Identify the repository root and stack. Load the session context. Detect a local test
or development runtime without mutating production configuration. Inventory local
origins and the external origins the project references. Identify available test
identities and safe fixtures. Note whether an isolated runtime is available, and say
so calmly if it is not.
2
scope
Settle what is in bounds. Loopback is the normal target class. Every external origin
stays out of bounds until it passes the authorization
gate.
3
discovery
Build a threat-oriented application map before hunting for bugs: entry points and
routes, the authentication and session implementation, where authorization is
enforced, data stores and trust boundaries, upload and download surfaces, outbound
requests and webhooks, parsing and template and query construction, secret handling,
admin endpoints, third-party integrations, and dependency manifests.
4
source_review
Start from the static pass’s candidates rather than the file tree — each names a
route, a line, and the data flow it believes in. Then look for plausible root causes
the static pass could not reach: authorization missing at a server-side boundary,
unsafe composition of queries or commands or templates or paths, insecure file
handling, server-side fetches without destination validation, token and cookie and
session mistakes, trust in client-controlled role or ownership identifiers, and
security-relevant business-logic assumptions.
5
runtime_validation
Exercise routes against the local running service with representative safe inputs and
the configured test identities, to establish whether a candidate is actually
reachable and security-relevant. Anything risky goes through
action check first.6
access_control
Compare identities: unauthenticated, normal user A, normal user B for object-ownership
boundaries, and a privileged user. The focus is server-side enforcement and object
ownership, using non-destructive fixture objects. A class that cannot be tested
because an identity or fixture is missing is recorded as a limitation, not guessed.
7
validation
Assign verification honestly, deduplicate, and — at
high effort and above — run a
false-positive challenge pass that argues the opposite of each candidate and keeps
only what survives.8
reporting
session finish validates every finding, renders all four formats, and syncs if this
scan opted in.Session states
ScanState in packages/contracts/src/domain.ts:
created · preflight · scope · discovery · assessment · validation ·
reporting · complete · cancelled · failed · policy_blocked
Phase records themselves carry their own state: pending, running, complete, or
skipped, with start and completion instants and a duration.
Cloud state
Separate from the scan state, because a scan is never blocked by the cloud.LocalCloudState:
Live progress events
A synced report receives only allowlisted, privacy-safe events. The payload schema is closed, so the scanner cannot deliver source, prose, evidence, or secrets through this path even by accident —schemas/scan-event.schema.json is the contract, and the server
rejects anything that fails it with UNSAFE_PAYLOAD_REJECTED.
Event types (ScanEventType):
report_created · phase_started · phase_completed · aggregate_updated ·
warning_safe · scan_completed · scan_failed
An event may carry a phase, a stable safeCode, a short message, a duration, and an
aggregate of counts (candidateCount, validatedCount, and per-severity totals).
Nothing else.
Browsers read the stream over server-sent events at
GET /api/v1/reports/{id}/events, which re-checks report access before the stream opens,
replays the stored history, and heartbeats every 20 seconds. Streams have a bounded
lifetime (SSE_MAX_STREAM_MS, default 5 minutes) so no viewer can hold a container
through a deploy; the browser reconnects and the route replays.
Ending a session
cancel is the honest way to stop: it keeps the
validated findings and the coverage notes rather than throwing the work away.