TunnelVision

UX — Audit

The screens that turn a capture into evidence about one app: an audit project (the app, and the domains it is expected to contact), its sessions (named recordings, one per version and build, plus a baseline) and the markers placed inside a session. The model underneath is ../spec/audit.md; the scope and the attribution method are ADR 0008.

Audit (tab) ──▶ Project ──▶ Session ──▶ Evidence bundle (sheet, once the session has ended)
   │              │            └─────▶ Comparison (with another ended audit session)
   │              │  └─ Start session (sheet)
   │              └─ Edit project (sheet)
   └─ New project (sheet)

Dashboard ──(strip, only while a session records)──▶ Session

Control Center · Action Button · Shortcuts · Siri ──▶ a marker in the open session (no screen)

Why a tab

Audit is the fifth tab, between Captures and Settings. It is what the tool is used for once an assessment is under way — opened every session, with the audited app waiting in the background — so it does not belong two levels down in Settings beside preferences, nor inside Captures, whose screen is an inventory of files. Five is also the most a tab bar shows without a More item: there is no room for a sixth, and that is a constraint on what comes next, not an accident.

What the three screens decide

None of it is drawing. The decisions live in AuditPresentation and AuditForms (TunnelVision/Models), pure and tested; one AuditViewModel feeds all three screens and the Dashboard, because what they show is the same fact — which projects exist and which session is recording — and a view model per screen would leave the list behind a session saying it still records after it was ended.

Projects

A short grouped list, like Captures: name and how many sessions. No leading rail and no icon — the rule of the capture row: a mark on every row would say this is a project in a list of projects. The one exception gets its badge: the project whose session is recording. With no projects the screen teaches what the feature is for in one sentence and offers New project; it names no standard.

A project

A session

Ordered by what the screen is opened for:

  1. Status — Recording or Ended, how many connections it holds, when it started and ended. The footer says the thing that matters in each state: while recording, that every connection the device makes is tagged, whichever app makes it; once ended, that its connections and captures are kept as evidence outside the storage limits until the session is deleted.
  2. Mark now (only while recording) — Consent given, Logged in, Logged out and Other…. Above the list of markers already placed, because while recording this is what the screen is opened to do. A marker is stamped with the instant of the tap. Once the session has ended this place is taken by Evidence bundle, for the same reason: it is what the screen is opened for in that state (§ Exporting a session).
  3. Markers — as they happened, to the second. A closed session takes no more and says why: a marker placed after looking at the traffic is no longer an observation.
  4. Certificate pinning — what this session can say about it, decided by PinningEvidence (pure, no default): Can be assessed only when inspection was on and the device trusted the certificate; otherwise Not assessed, with a different sentence for each of the two reasons, because each has its own way out. It is never passed or failed: nothing was observed. And the readable case says how it is read — an app either refused the user-installed certificate or accepted it — and that nothing was bypassed to find out (ADR 0003).
  5. Recorded on — device model, iOS, TunnelVision version.
  6. Notes, only if any were written.
  7. End session (while recording) and Delete session, both confirmed. Ending is irreversible in the two ways the prompt names: tagging stops and no marker can be added. Deleting says what is not lost — the connections stay in the history — and what changes for them: they stop being evidence and expire with the storage limits.

Exporting a session

A session that has ended offers one row, Export evidence, under its status. A recording one does not: a bundle is closed evidence (../spec/audit.md § Evidence bundle).

Tapping the row shares nothing. It writes the bundle — the row shows progress and every other row that writes is disabled meanwhile — and then opens a sheet that says what was written. Sharing is the button at the bottom of that sheet. The order is the one the connections export already has in Captures, and it matters more here: what leaves the device is a capture.

The sheet, top to bottom, and why each line is there:

  1. What it is: Evidence bundle ready to share, its size, ZIP, how many files — eight, the report among them. The count is of the names the exporter returns, not a number of the screen’s.
  2. What the bundle says of itself, in the bundle’s own words (session.json’s contents): connection metadata, what it shows against a requirement catalogue, a capture — and that decrypted content is not part of it.
  3. Connections and Findings, as counts, and under them what findings.json says of its verdicts, again in its own words: a verdict is not a test result, and none states that a requirement is met.
  4. Capture — the part nobody can see by opening the archive. Decided by EvidenceCaptureStanding (pure, no default), which has three cases and not a tally with three zeroes:
    • every recorded packet is in the capture — one neutral sentence;
    • some are missing — a warning headline, 4,842 of 4,895 recorded packets are in the capture, and one sentence per reason that has packets: never written to a capture file, in a capture file that is no longer on this device, could not be read back;
    • the session recorded no packets — said as that, not as a complete capture.

    Then, if any, the packets that are in the capture but fall outside the session’s time window, with why they are there (their connection was also active during the session): without the sentence they read as traffic that should not be in the file. And always, last: anything that was sent unencrypted can be read in the capture. “No decrypted content” is easily read as “no content”, and from a medical app what travelled in the clear can be health data.

  5. The file name, in the literal role, and Share.

The bundle’s own sentences are quoted, not reworded. Items 2 and 3 show EvidenceSessionDocument.contentsNote and EvidenceWording.verdictsNote as they are written into the files. They are limits that were costly to settle, and a second wording on the screen would sooner or later say something else than the file the user is about to send. The cost is visible: the verdict note names toolCoverage, a key of findings.json. The sentences about the capture are the screen’s own, from the string catalog, because they carry this export’s numbers; each reason is worded as capture.json defines it.

An export that fails is a notice at the top of the session screen, never a sheet, and each of the eight ways it can fail has a sentence of its own (AuditPresentation.evidenceExportFailed). A catalogue this version does not include is named, and the sentence says no other was used instead; a history that changed under the export says to export again; a failed write says nothing was left behind and names the one cause a user can act on, free space. The section sits second on the screen so that the notice appears where the row was tapped.

Closing the sheet does not delete the archive: the system share sheet may still be reading it. The next export does — there is at most one bundle on disk, in the app’s temporary directory.

Comparing two versions

An ended audit session lists, under its evidence bundle, the project’s other ended audit sessions: Compare with. The section is absent when there is none — a heading over “nothing to compare” would take room in the first session of every project, where it is least needed. A baseline is not offered and offers nothing: it observes no version, and the model refuses it (../spec/audit.md § Release diff). Neither is a session still recording.

The comparison is its own pushed screen, ordered by the question that opens it — what is new in this version?:

  1. Which session is which, and the three counts: later, earlier, new domains, no longer contacted, in both. The footer says the rule nobody guesses: the later one is the session that started later, whatever its version number.
  2. New domains, each with what the allowlist says of it. Only a new domain the allowlist does not cover is marked — symbol and colour, never colour alone. One it covers is listed without alarm, and with an empty allowlist nothing is marked: there is nothing to measure against, and flagging every domain would call “no list was written” a finding. The footer says what new means here — not contacted while the earlier session was recording — and that it is not attributed to the app. When there is none the section says no named domain is new, and “named” is deliberate.
  3. TLS version changed, only if any did: the domain, the versions in each session, and who negotiated them. A change read from the connection TunnelVision opened to inspect the traffic says it is not what the app negotiated. A floor that went down is marked; one that went up is listed.
  4. No longer contacted, last: it is what the screen is least opened for.
  5. What the lists cannot say, as a closing paragraph when it applies: how many connections had no name in each session — a new destination among them would not show — and how many domains were only another name of a shared address, which are in neither list.

All of it is decided in ReleaseComparisonPresentation, pure and tested; the view draws. A comparison that can no longer be made — the other session was deleted between being offered and being tapped — is a sentence on the screen, not a spinner.

Measured on the Simulator with the seeded project (version 2.4.0 against 2.3.1): nine new domains, forty in both; the longest host name wraps to a second line in the literal role instead of truncating, and its verdict line stays under it.

The two forms

What is not asked. Device model, system version, tool version and the state of HTTPS inspection are read from the device when the session starts (AuditRecordingConditions). The form does show, before recording, whether pinning will be assessable — in the future tense, with its own copy — because a session recorded without inspection cannot be fixed afterwards. The conditions are read again at the instant recording starts: between opening the form and confirming it, the user may have been to Settings.

The open session, where the tunnel is watched

A session left open keeps tagging every connection until someone ends it. So the Dashboard shows a strip directly under the monitoring control for as long as one is open — Audit session recording, whose project it is, and Open, which lands on that session with its project behind it in the stack. It has the shape of the monitoring strip on purpose: both are things happening right now. It is absent the rest of the time, so the Dashboard of someone who never audits is unchanged.

Marking without coming back

Mark now needs TunnelVision in front, and the moment that matters most — accepting the audited app’s consent screen — happens with the audited app in front. So the three fixed markers can also be placed from outside (../spec/audit.md § Marking from outside the app):

What they say afterwards is the whole design. Placed: “Consent given” marked at 21:58:34 in Example Health. — the name, the time to the second, the project. Not placed: an error that begins No marker was placed, and then why (no session is recording, or the history could not be written). There is no third, quiet outcome.

Neither opens the app. Coming back later, the session screen already lists what was placed from outside: the tab re-reads when the app returns to the foreground.

The free-form marker (Other…) is not offered outside the app. It has to be typed, and by the time it is typed the instant is no longer the one being marked.

Measured, not eyeballed

From idb ui describe-all on an iPhone 17, and what each measurement changed: