# Why this exists

A Brotli audit looks like a version check and is not one. This page states what a complete audit
owes, why eyes miss the failures, and what the tool does differently.

## The four questions a host asks

| Question | Why a version check cannot answer it |
|---|---|
| Which release is installed? | a distribution revision (`1.2.0-3ubuntu0.1`) keeps the vulnerable upstream number while carrying the fix, and a rebuild (`1.2.0-3build1`) keeps it without the fix |
| Does a supported fix exist? | the security pocket appears in the package lists only after `apt-get update`; before that the candidate equals the installed version |
| Do running processes still use the vulnerable decoder? | replacing the `.so` on disk changes nothing for a process that mapped the old inode |
| Which application paths actually decode untrusted Brotli? | the package is everywhere; the decode path is in a specific argument set in specific application code |

Answering them by hand costs dozens of tool calls per host, and the answer arrives as prose that
nobody can re-check. The tool answers them with three commands and a fixed exit-code contract.

## Why eyes miss the failures

Two defects found in the field produced a *false green* — the run reported success while having
done nothing:

- a `printf` without a trailing newline collapsed the unit list into one string, so
  `systemctl restart` was called with a nonexistent unit and its failure was not fatal;
- the inode snapshot was taken from a glob that matched the symlink, so the "no process maps the
  old inode" check compared against an inode nobody can map.

Both are invisible in the exit code and in the log. Both were caught by cross-checking the claim
against `/proc/*/maps` on the host — a measurement, not a reading.

The general rule the tool now follows: **a claim about effect is verified where the effect lives.**
The package database proves the file; only `/proc` proves the process.

## What is different here

| Ordinary approach | This tool |
|---|---|
| one long shell session per host | three scripts, delivered once, reused per host |
| verdict in prose | verdict on one line, plus JSON in `/var/lib/cve-2026-111497/` |
| install on the newest candidate | install only after the candidate changelog names the CVE |
| restart everything that maps the library | restart server consumers, defer desktop and infrastructure, explain both |
| "updated, should be fine" | `VERIFY: PASS` built from version, changelog, inodes, failed units, round-trip |

## The distinction that decides most verdicts

An encoder compresses data the server produced. A decoder parses data from outside.

nginx with `ngx_brotli`, Apache with `mod_brotli`, a static asset pipeline — all encode. They link
the same vulnerable package and are not the attack surface. The attack surface is a process that
accepts `Content-Encoding: br` or fetches a response with it.

On the three hosts of the field run the web tier was compression-only. The reachable path was
outbound HTTP through libcurl, reached from application code that sets `CURLOPT_ENCODING` to an
empty string: WordPress Requests, SimplePie, Guzzle. One host carried 164 PHP files with that
argument. That number — not the installed package — measured the exposure.

## When not to use this tool

- A host without apt: the classification matrix travels, the remediation does not.
- A host where Brotli is compiled into a binary or a container image: `dpkg` cannot see it.
- A fleet: the tool is per host by design; orchestration belongs to the layer above it.

Next: [Deployment](02-Install) — deliver the scripts and prove the contract.
