Skip to main content

Administrator Guide

<!--
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. |