# Inventory

Patching starts with a list, and the list cannot come from source manifests: a `package.json` says
nothing about the runtime that executes it, and a `requirements.txt` says nothing about the container
base image. This page collects the commands that produce the list, sorted by what you are looking
for.

Record every asset in one row of the [table at the end](#8-what-to-record) and keep the command
output next to it. An inventory without a command behind it is an opinion.

## 1. System packages

```bash
# Debian and Ubuntu
dpkg -l | grep -Ei 'brotli|libbrotli'
apt-cache policy libbrotli1 brotli

# RHEL, CentOS, AlmaLinux, Rocky Linux
rpm -qa | grep -i brotli

# Alpine
apk info -a | grep -i brotli

# macOS
brew list --versions brotli 2>/dev/null
```

`apt-cache policy` matters more than `dpkg -l`: the installed version tells you what runs, the
candidate tells you whether a supported fix exists at all.

## 2. Runtimes

```bash
# Node.js
node --version
which node
readlink -f "$(which node)"
# patched: v22.23.4, v24.22.1, v26.12.1 or later

# Python packages
python3 -m pip show Brotli brotlicffi
python3 -m pip list | grep -Ei 'brotli'
# patched: Brotli >= 1.2.1, brotlicffi >= 1.2.1.0

# Bun
bun --version
which bun
# patched: 1.4.4 or later
```

`readlink -f` is not cosmetic: on a host with several runtimes installed, `node` may be a shim that
resolves to a version other than the one a service uses.

## 3. Container images

```bash
# Debian or Ubuntu based
docker run --rm --entrypoint sh IMAGE -c '
  cat /etc/os-release;
  echo "---";
  dpkg -l | grep -Ei "brotli|nodejs" || true
'

# Alpine based
docker run --rm --entrypoint sh IMAGE -c '
  cat /etc/os-release;
  echo "---";
  apk info -a | grep -i brotli || true
'
```

An image built from a patched base can still carry a vulnerable copy from a build stage or a vendored
tree. The check belongs on the final artifact, not on the Dockerfile.

## 4. Static and embedded copies

Static linking, vendored source trees, single-file executables and compiled serverless bundles keep
the vulnerable code after the host is patched.

```bash
# files with Brotli-related names
find / -type f \( -iname '*brotli*' -o -iname 'libbrotli*' \) 2>/dev/null

# application trees: symbols or references
grep -RIlE 'BrotliDecoderDecompress|brotli_decoder|libbrotlidec' \
  /opt /srv /app /usr/local 2>/dev/null

# ELF binaries that link Brotli symbols
find /opt /srv /app -type f -executable 2>/dev/null | while read -r f; do
  nm "$f" 2>/dev/null | grep -qi brotli && echo "$f"
done
```

The third command is slow and worth it once: it is the only one that finds a copy compiled into a
binary that the package manager never sees.

## 5. Application decode paths

A version tells you what is installed; a decode path tells you what is reachable. Search for the
primitives, not for the word "brotli" alone — the word appears in minified assets and plugin
documentation far more often than in real decode paths.

```bash
# Node.js and TypeScript
grep -RInE 'Content-Encoding|content-encoding|createBrotliDecompress|brotliDecompress|fetch\(' \
  --include='*.js' --include='*.ts' --include='*.mjs' --include='*.cjs' .

# Python
grep -RInE 'brotli|Brotli|brotlicffi|decompress|Content-Encoding|httpx|aiohttp|scrapy' \
  --include='*.py' --include='requirements*.txt' --include='pyproject.toml' .

# .NET
grep -RInE 'AddRequestDecompression|UseRequestDecompression|AutomaticDecompression|Brotli' \
  --include='*.cs' --include='*.csproj' .

# PHP: libcurl accepts every encoding when the argument is empty
grep -rIlE 'CURLOPT_ENCODING|CURLOPT_ACCEPT_ENCODING' /var/www --include='*.php' 2>/dev/null | wc -l
```

The PHP line is not in the vendor report; it is the measurement that found the real path on our
hosts — 164 files on one server, through WordPress Requests, SimplePie and Guzzle.

## 6. Runtime truth on a live host

The file on disk and the process in memory are two different facts. The only place the second one is
visible is `/proc`:

```bash
# every process that maps the decoder, with its executable name
for p in /proc/[0-9]*; do
  m=$(grep libbrotlidec "$p/maps" 2>/dev/null | head -1) || continue
  [ -n "$m" ] && printf '%s %s\n' "$(cat "$p/comm")" "$(echo "$m" | awk '{print $5}')"
done | sort | uniq -c
```

`detect.sh` runs exactly this scan and reports the consumer summary in its seventh line; the
[operating runbook](10-Operations) explains how to read it. Desktop and font stacks appear here too
and are not an attack surface — see the deferred classification in the [field cases](04-Field-Cases).

## 7. Classify before you patch

| Exposure class | Definition | Consequence |
|---|---|---|
| public decode | internet-facing, decodes untrusted Brotli | patch first, treat as exploitable |
| partner decode | reachable by a third party with its own trust level | patch in the same window |
| internal decode | decodes data from services inside the perimeter | normal emergency cycle |
| offline | only trusted local files | routine maintenance |

## 8. What to record

| Field | Why it is needed |
|---|---|
| Asset and owner | the row has to survive the person who ran the command |
| Runtime and library version, with the command | a version without its command cannot be re-checked |
| Version source: running process or manifest | manifests lie about runtimes |
| Decode path and direction: inbound, outbound, file | decides whether the asset is exposed at all |
| Exposure class | decides the patch order |
| Artifact hash or build ID | proves which artifact was verified |
| Patch version, restart time, verification result | the closure evidence |

Next: [Remediation playbooks](08-Remediation-Playbooks) — how to patch each row of that list.
