<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2026 ndaal Gesellschaft für Sicherheit in der Informationstechnik mbH & Co KG, Cologne
SPDX-FileCopyrightText: Author: Pierre Gronau <Pierre.Gronau@ndaal.eu>
-->
# Administrator Guide — ndaal SBOM Auditor
This guide covers deploying, configuring and operating the ndaal SBOM
Auditor. For the exhaustive option and route tables see
[REFERENCE.md](../REFERENCE.md); for the architecture see
[structure.md](structure.md) and
[arc42/en/](arc42/en/01-introduction-and-goals.md).
## Overview
The auditor is a single self-contained binary. The web UI, fonts and the
SQLite engine are embedded, so a deployment is the binary plus a writable
data directory. It serves HTTPS only (TLS 1.3) and binds loopback by
default.
## Web UI languages
The web UI is available in 49 languages. English, German and French are
human-authored and pinned to the front of the switcher; the others are
machine-translated, pending native review, with automatic English fallback
for any missing string, and Urdu renders right-to-left. Selection is
per-browser. The generated reports (SARIF, CSAF, PDF) and the HTTP API are
English-only.
## Prerequisites
- A host that can run the binary (built for its target).
- Optionally, the analyzer tools on `PATH`: `syft`, `grype`, `trivy`,
`osv-scanner`, `bomber`. Each is optional; an absent tool is skipped,
not fatal.
- No database server, runtime or network service is required.
## Installing the auditor
There are two supported install paths:
- **From source (crates.io):** `cargo install sbom-auditor` compiles the
published crate for your target with your local toolchain.
- **Prebuilt binary:** `cargo binstall sbom-auditor` fetches the
matching GitLab-release binary instead of compiling. The download URL
is resolved from the crate's `[package.metadata.binstall]` `pkg-url`,
so no build toolchain is needed.
The running version is shown on the About page (Info → About) and by
`sbom-auditor --version`.
## Installing the analyzer tools
The tools are detected on `PATH` at analysis time and their versions are
shown on the `/tools` page. Install whichever you want to use, for
example:
```bash
# Anchore syft + grype
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s
# Aqua trivy, Google osv-scanner, devops-kung-fu bomber
# install per each project's documentation
```
To point the auditor at a specific binary, set the per-tool override:
`SBA_TOOL_SYFT_BIN`, `SBA_TOOL_GRYPE_BIN`, `SBA_TOOL_TRIVY_BIN`,
`SBA_TOOL_OSV_SCANNER_BIN` or `SBA_TOOL_BOMBER_BIN`.
## Starting the server
```bash
sbom-auditor --port 8680
```
The server binds `https://127.0.0.1:8680` (TLS 1.3 only). A self-signed
certificate for localhost is regenerated on every start-up, so browsers
warn once. Port 8681 is reserved for a future QUIC / HTTP/3 listener.
## Configuration
Every option has an `SBA_*` environment equivalent; the flag wins over
the environment. The full table is in [REFERENCE.md](../REFERENCE.md);
the most common options:
| Flag | Environment | Default | Purpose |
| --- | --- | --- | --- |
| `--port <PORT>` | `SBA_PORT` | `8680` | Listen port, `1025` to `65535`. Ports 0 to 1024 are refused; to serve on 443, put a reverse proxy in front. |
| `--bind <ADDR>` | `SBA_BIND` | `127.0.0.1` | Bind address. |
| `--data-dir <DIR>` | `SBA_DATA_DIR` | `./data` | SQLite store and stored SBOMs. |
| `--db <FILE>` | `SBA_DB` | `<data-dir>/auditor.db` | Database path override. |
| `--encryption-key-file <FILE>` | `SBA_ENCRYPTION_KEY_FILE` | none | Enable storage encryption. |
| `--tls-cert <FILE>` | `SBA_TLS_CERT` | self-signed | PEM certificate chain. |
| `--tls-key <FILE>` | `SBA_TLS_KEY` | self-signed | PEM private key. |
| `--allowed-host <H>` | `SBA_ALLOWED_HOSTS` | none | Extra `Host` authorities. |
| `--allow-non-loopback` | `SBA_ALLOW_NON_LOOPBACK` | off | Permit a non-loopback bind. |
| `--log-dir <DIR>` | `SBA_LOG_DIR` | `./ndaal/log` | Log-file directory. |
| `--log-format <FMT>` | `SBA_LOG_FORMAT` | `text` | `text` or `json`. |
| `--no-self-update` | `SBA_NO_SELF_UPDATE` | off | Refuse `--self-update`. |
Runtime settings that are not start-up options — the import and export
directories, analyzer timeout and concurrency, the analyzer and sidecar
toggles and the upload size limit — are edited on the `/settings` page and
persisted in the database. Their keys and defaults are in
[REFERENCE.md](../REFERENCE.md).
The export page offers two actions that differ in what they persist. The
primary **Export** button (`POST /export/run`) renders the selected
formats and writes each file plus its checksum sidecars into the export
directory, so that directory must be writable. **Start download**
(`POST /export/download`) renders the same selection in memory and streams
it to the browser — a single document for one format, otherwise a ZIP
bundle — writing nothing on the server, so it needs no writable export
directory.
## Vulnerability enrichment
The auditor can annotate each finding with live exploitation intelligence,
looked up per CVE from a vulnlookup service. Enrichment is **on by
default** and is the auditor's only unattended outbound connection, so
review it when you deploy. Configure it on the **Settings** page.
- **Master switch.** `enrichment_enabled` (default on) turns all enrichment
lookups on or off. With it off, the auditor sends no enrichment request
and runs fully offline.
- **Sources.** Up to three source URLs (`vulnlookup_source_1` through
`vulnlookup_source_3`) are queried in order; the first to answer wins, and a
blank slot is skipped. Sources 1 and 2 default to
`https://vulnlookup.ndaal.eu/`; source 3 defaults to a loopback
`https://127.0.0.1/` for a vulnlookup co-located on this host.
- **Ports.** Each source carries its own TCP port
(`vulnlookup_source_N_tcp_port`; a blank port is treated as 443) and a QUIC
port (`vulnlookup_source_N_quic_port`) reserved for a future HTTP/3 transport
that the current TCP path does not use. Sources 1 and 2 default to 443; source
3 defaults to this host's own `8680` (TCP) / `8681` (QUIC) listener ports.
Each source URL is combined with its own TCP port. The endpoint is
`GET {source}api/v1/vulnerability/{cve}/enrichment`, and a request carries
only the CVE identifier over the same HTTPS-only, TLS 1.3 client the
auditor uses elsewhere. Results are cached per CVE and refreshed on a
24-hour TTL, so a repeated analysis does not re-query within the day. When
every source is unreachable the enrichment fields are left blank and the
analysis still completes — offline operation is never blocked.
The lookups come from the auditor's own process, so check reachability **from
the server host, as the service account** — a workstation that reaches the
source proves nothing about the server. The client accepts TLS 1.3 only and
trusts the operating system's CA bundle; on a host without a readable bundle
every lookup fails and every finding stays `Not enriched`. This reproduces the
client's request exactly:
```bash
curl -sS --tlsv1.3 --tls-max 1.3 -o /dev/null -w '%{http_code}\n' \
https://vulnlookup.ndaal.eu:443/api/v1/vulnerability/CVE-2021-44228/enrichment
```
`200` means the path works; `000` means the host cannot reach the source; a
certificate error means the CA bundle is missing or unreadable. The server log
records each enrichment pass as `post-analysis enrichment finished` with
`looked_up=`, `stored=` and `failed=` counts, and logs
`post-analysis enrichment failed` when the client could not start at all.
**Enrichment cache (schema v7).** The lookups are cached in the
`cve_enrichment` table, one row per CVE, joined onto findings by `cve_id`
at read time. Schema version 7 drops and recreates this table for the
current EPSS + CISA-KEV + nuclei model — the earlier v6 columns only ever
held blanks — so the migration is a clean rebuild:
```sql
CREATE TABLE cve_enrichment (
cve_id TEXT PRIMARY KEY,
epss_probability REAL,
epss_percentile REAL,
kev INTEGER,
nuclei INTEGER,
nuclei_severity TEXT,
source_url TEXT,
fetched_at INTEGER NOT NULL,
source TEXT
);
```
`fetched_at` is the cache timestamp that drives the refresh TTL. The table
lives in the SQLite database, so the normal [backup](#backup-and-recovery)
captures it and the schema-migration ladder rebuilds it forward; no
separate action is required.
## Security posture
- HTTPS only — there is no plaintext-HTTP fallback.
- Loopback bind by default; a non-loopback bind requires the explicit
`--allow-non-loopback` opt-in because the auditor ships no
authentication.
- Every response carries the ndaal security-header baseline (HSTS,
`X-Frame-Options: DENY`, a hash-pinned Content-Security-Policy, nosniff,
referrer minimisation, Permissions-Policy and cross-origin isolation),
every state-changing POST is same-origin checked, and an
anti-DNS-rebinding `Host` allowlist is enforced.
- All data, import and export I/O is confined by cap-std.
## Uploads and request limits
Uploads are capped to protect the server from oversized or malformed
input:
- **Size cap.** A multipart upload larger than the configured limit is
rejected with `413`. The limit is **per request**, not a cumulative
quota: it bounds one upload, and there is no ceiling on how many
uploads an instance accepts. The default is 10 GiB; change it with the
`upload_max_bytes` setting on the **Settings** page.
Size it against your RAM, not just your largest SBOM. The request body
is read into memory in full (`Limited` + `collect`) before the
multipart parse, so the cap is also the worst-case allocation for a
single in-flight upload — and concurrent uploads multiply it. On a
host with a few GiB of RAM, a 10 GiB cap means one oversized request
can exhaust memory before the `413` would ever fire. Lower it to fit
the box when the auditor is exposed to anything but a trusted operator.
- **Multipart boundary hardening.** The parser rejects any multipart
boundary longer than RFC 2046's 70-character maximum and locates the
boundary with a linear scan, so a crafted request cannot drive CPU
cost super-linearly.
## Export directory
`GET /export/download` is confined by cap-std to the export directory and
additionally serves only file names with a recognised export or
checksum-sidecar extension. A misconfigured `export_dir` therefore cannot
be used to hand out an unrelated system file (for example `passwd` or a
private key). As defence in depth, keep `export_dir` a relative,
app-owned directory dedicated to exports — never point it at a shared or
system path.
### How many files one export writes
Plan directory capacity from the finding count, not the format count. Each
artifact is written with all five checksum sidecars, so a single export of all
eight formats already lands 48 files. The `csaf_split_per_finding` setting then
multiplies it:
- **`false` (default).** Each CSAF version renders one rollup advisory
containing every finding. Eight formats produce 8 artifacts + 40 sidecars.
- **`true`.** Each CSAF version additionally renders one advisory *per
finding* (`sbom-{id}-csaf-v2.1-0001.json`, `-0002`, …). For an SBOM with 73
findings the same eight-format export produces
`6 + 2 × (1 + 73) = 154` artifacts — 924 files once sidecars are counted.
Turn it on when downstream consumers need one advisory per vulnerability;
leave it off if the export directory is size-constrained.
## Exposing the auditor on a network
The auditor has no authentication, so do not expose it directly. To reach
it from another host:
1. Run a TLS-terminating, authenticating reverse proxy (for example nginx
or Caddy with basic auth or SSO) in front of it.
2. Add the external hostname to the `Host` allowlist:
`--allowed-host auditor.example:443` (or `SBA_ALLOWED_HOSTS`).
3. Only then pass `--allow-non-loopback` (or bind the proxy to loopback
and keep the auditor on loopback too).
## Operator certificates
For a real (non-localhost) deployment, present a CA-issued certificate
instead of the ephemeral self-signed one:
```bash
sbom-auditor --tls-cert /etc/ssl/auditor.pem --tls-key /etc/ssl/auditor.key
```
Both flags must be given together. The operator key is held to the same
no-RSA policy as the self-signed path:
- **Algorithm — ECDSA or Ed25519 only.** A PKCS#1 RSA `--tls-key` is
refused; supply an ECDSA (SEC1 or PKCS#8) or Ed25519 (PKCS#8) key.
- **Permissions (Unix) — owner-only.** If the key file is readable or
writable by group or other (`mode & 0o077 != 0`), start-up is refused,
OpenSSH-style. Run `chmod 600` on the key file before starting the
server.
## Storage encryption
Optional XChaCha20-Poly1305 encryption seals stored SBOM bytes and raw
analyzer output while they sit on disk. Supply a key file containing
exactly 32 raw bytes or 64 hex characters:
```bash
sbom-auditor --encryption-key-file /etc/sbom-auditor/at-store.key
```
Key management is the operator's responsibility:
- A missing or malformed key file is a hard error — encryption is never
silently skipped.
- Back up the key separately from the database; a lost key makes sealed
data unreadable.
- A store opened with a key still reads pre-existing plaintext rows, so
enabling encryption later is safe.
## Search index
The dashboard's full-text search box is served by an in-memory index (tantivy),
not a file on disk. The SQLite database is the source of truth; the index is a
derived copy, rebuilt from the database at every start-up and kept in sync as
SBOMs are ingested and deleted. Two operational consequences follow:
- There is no index directory to back up. Backing up the data directory (see
[Backup and recovery](#backup-and-recovery)) captures everything; the index
reconstructs itself on the next start.
- Restart cost scales with the store. The rebuild reads every stored SBOM's
component text, so start-up time grows with the number and size of stored
SBOMs. The rebuild is best-effort: if it fails it is logged and search is left
empty until the next re-ingest — it never blocks start-up, and the dashboard
keeps working without it. Search is an accelerator, not a correctness
dependency.
The tantivy engine pulls in a transitive dependency, `lru`, that carries
advisory RUSTSEC-2026-0253 (classified `unsound`, not a live CVE). It is
unreachable in this usage and is recorded as a documented, accepted ignore in
`.cargo/audit.toml` and `deny.toml`; it will be removed once tantivy bumps
`lru`. No operator action is required.
The engine also introduces a duplicate `hashbrown` version in the dependency
tree: its columnar crate pins an older one than the direct dependency. The two
copies sit in disjoint subtrees and are benign, so the duplicate is an accepted
entry in `deny.toml`'s `skip` list, needing no operator action either.
## Health probes
- `GET /healthz` — liveness; `200 ok` while the process serves.
- `GET /readyz` — readiness; `200 ready` once the app is serving.
Wire these into your process supervisor or load balancer.
## Verifying a deployment
`tests/scripts/test_examples_upload_via_api.sh` is a pure-API smoke test:
it uploads every SBOM under `example/` to a running instance and asserts
that each accepted SBOM yields a SARIF, a CSAF and a PDF artifact (checked
in the streamed `POST /export/download` ZIP; non-SBOM files are counted as
skipped, not failed). It needs no analyzer tools and no server disk
access, so it works against any instance.
```bash
tests/scripts/test_examples_upload_via_api.sh --base-url https://sbom.ndaal.eu/
```
Options:
- `-u, --base-url URL` — instance to target (default the local dev
server).
- `-n, --limit N` — stop after N accepted SBOMs (`0` = no limit).
- `-t, --timeout SECONDS` — per-request timeout; raise it for very large
SBOMs.
- `--cacert FILE` — verify the server certificate against this CA bundle.
- `-k, --insecure` — skip TLS verification (the default for a loopback
host, which serves the self-signed dev certificate).
- `--secure` — force TLS verification even for a loopback host.
- `-l, --list-only` — list the files that would be uploaded, then exit.
TLS is verified against the system CA store for non-loopback hosts by
default; loopback hosts are not verified unless you pass `--cacert`.
## Logging
Logs are written to the console and to `<log-dir>/sbom-auditor.log`. Use
`--log-format json` for structured, newline-delimited logs suitable for a
log aggregator. An in-database audit log additionally records operator
actions such as settings changes.
## Backup and recovery
- The data directory holds everything stateful: the SQLite database
(`auditor.db` plus its WAL files) and the stored SBOM bytes. Stop the
server or use SQLite-consistent backup, then copy the data directory.
- The in-memory search index is not part of the backup set: it lives only in
RAM and is rebuilt from the database at start-up (see
[Search index](#search-index)).
- If storage encryption is enabled, back up the key file separately and
securely.
- Recovery is copying the data directory (and key file) back and starting
the binary; the schema migration ladder brings an older database
forward automatically.
## Updating
- `--check-update` contacts gitlab.com read-only and reports whether a
newer release exists.
- `--self-update` downloads the latest release for the target, verifies it
against the published checksums, atomically replaces the running binary
and exits; it never downgrades and never installs on a mismatch.
- For package-managed or locked-down installs, set `--no-self-update`
(or `SBA_NO_SELF_UPDATE=1`) so `--self-update` refuses;
`--check-update` stays available.
## Running as a service
The binary is a well-behaved long-running process (graceful shutdown,
bounded connections and timeouts). Run it under systemd as a sandboxed
daemon: a dedicated unprivileged user, a private writable
`StateDirectory` for `--data-dir`, and the analyzer tools on the unit's
`PATH`. Keep the bind on loopback and place a reverse proxy in front for
remote access.
## Platform support
Built and tested on Debian 13.x and macOS. Any platform with a
compatible Rust target and the optional analyzer tools available on
`PATH` is supported; the auditor itself has no other runtime dependency.
## Troubleshooting
The table covers the most common cases. The Troubleshooting Guide
(`documentation/Troubleshooting_Guide.md`, also under **Info → Troubleshooting
Guide** in the UI) works through each problem area in depth: symptom, checks,
likely cause and fix.
| Symptom | Likely cause | Action |
| --- | --- | --- |
| Browser warns about the certificate | Self-signed dev certificate | Expected once; accept it, or supply `--tls-cert` / `--tls-key`. |
| A tool always shows as skipped | Binary not on `PATH` | Install it or set `SBA_TOOL_<NAME>_BIN`; check `/tools`. |
| bomber skips on a newer SBOM | CycloneDX newer than 1.6 and syft absent | Install syft so the input can be converted. |
| Refused to bind | Non-loopback address without opt-in | Add `--allow-non-loopback` behind a proxy, or bind loopback. |
| Start-up aborts on the key file | Missing or malformed encryption key | Provide a 32-byte / 64-hex key file. |
| Refused the `--tls-key` | RSA key supplied | Use an ECDSA or Ed25519 key; RSA is not accepted. |
| Refused the `--tls-key` (Unix) | Key file group/other-accessible | `chmod 600` the key file so only its owner can read it. |
| Upload rejected as too large | SBOM exceeds `upload_max_bytes` | Raise the limit on **Settings**, or split the input. |
| Findings show `Not enriched` | Enrichment disabled, or every source unreachable | Enable it under **Vulnerability enrichment** on **Settings**; confirm the source host is reachable. |
| Enrichment stays blank from a source | Wrong source URL or port | Point it at the HTTPS host and TCP port (443 by default), not port 8080; the client appends `/api/v1/vulnerability/{cve}/enrichment`. |
| Every finding stays `Not enriched` although enrichment is on | The server host cannot reach the source, or cannot read its CA bundle (the client is TLS 1.3-only) | Run the `curl` check under **Vulnerability enrichment** on the server, as the service account. |