# barox appsec

Internes Werkzeug der Produktsicherheit: es zerlegt eine Firmware-Datei,
bestimmt die darin enthaltenen Fremdkomponenten und beantwortet drei Fragen –
„welches Modul ist von welchem CVE betroffen", „welche Lizenzpflichten löst
das Gerät aus" und „wie schwer ist ein Speicherfehler hier überhaupt
auszunutzen". Damit ersetzt es Black Duck für alle drei Anwendungsfälle:
Schwachstellenabgleich, FOSS-Prüfung und Härtungsprüfung.

- URL: <https://appsec.barox.io>
- Anmeldung: Microsoft 365 / Entra ID
- Stack: PHP 8.1+, MySQL/MariaDB, Apache 2.4 — kein Composer, kein Build-Schritt

## Was es kann

**Firmware auswählen statt hochladen.** Die zu prüfenden Stände stehen bereits
im [barox Firmware-Portal](https://fw.barox.ch) (`barox-firmware`). appsec liest
dessen Datenbank und Dateiablage – ausschliesslich lesend – und zeigt sie zur
Auswahl an: Modell, Version, Status, ob die Datei da ist und wann sie zuletzt
analysiert wurde. Ein Klick startet den Lauf; kopiert wird nichts, im Portal
ändert sich nichts. Dateien, die dort nicht liegen (Vorabversionen des
Herstellers, Fremdgeräte), lassen sich weiterhin hochladen.

**Entpacken.** Arbeitet sich schichtweise durch die Firmware und protokolliert
jeden Schritt: Microchip-Container (`.IMGS`), U-Boot-FIT-Images (`.itb`),
uImage, Android-Boot-Images, SquashFS, cramfs, cpio/initramfs, tar, ZIP sowie
gzip, xz, LZMA, bzip2, LZ4 und zstd — auch mehrfach ineinander verschachtelt.
Unbekannte Abbilder werden nach eingebetteten Dateisystemen durchsucht.

**Komponenten bestimmen.** Über fünf voneinander unabhängige Verfahren:

| Verfahren | Quelle | Verlässlichkeit |
|---|---|---|
| Hersteller-Stückliste | `/etc/mscc/licenses.txt.gz`, Buildroot-Manifest, dpkg/opkg/apk | niedrig |
| Systemdateien | `os-release`, `/lib/modules/<version>`, `libc-<version>.so` | hoch |
| Bibliotheksnamen | `libz.so.1.2.11` → zlib 1.2.11 | hoch, aber nur wo geprüft |
| Zeichenketten in Programmdateien | `OpenSSL 1.1.1g`, `BusyBox v1.31.1` | hoch |
| Programmpfad | `/usr/sbin/mosquitto` → Mosquitto, Version offen | belegt nur das Vorhandensein |

Das letzte Verfahren beantwortet eine Frage, an der die anderen vier
scheitern. Manche Programme schreiben ihre Version nicht als feste
Zeichenkette in die Datei, sondern setzen sie zur Laufzeit in eine
Formatvorlage ein — `mosquitto version %s starting`. Dort ist nichts zu
finden, und weil es keine Bibliothek ist, greift auch die Auswertung der
Dateinamen nicht. In der geprüften barox-Firmware betraf das vier Dienste,
die deshalb in **keiner** Stückliste standen:

| Komponente | Fundstelle | Warum sie fehlte |
|---|---|---|
| Eclipse Mosquitto | `/usr/sbin/mosquitto` | MQTT-Broker, von aussen erreichbar |
| radvd | `/usr/sbin/radvd` | Router-Advertisement-Dienst |
| Shell In A Box | `/usr/bin/shellinaboxd` | Kommandozeile im Browser, seit 2016 ohne Pflege |
| netcat | `/usr/bin/netcat` | seit 2004 ohne Pflege |

Die Version dieser Programme lässt sich trotzdem bestimmen — nur nicht dort,
wo man sie erwartet. Der Übersetzer legt die Zeichenketten einer
Übersetzungseinheit zusammenhängend ab, die nackte Versionsnummer steht also
in der Datei **neben** der Vorlage. Gesucht wird deshalb im Umkreis eines
Ankers nach einer Zeichenkette, die *vollständig* aus einer Versionsnummer
besteht; genommen wird die nächstgelegene:

```
… "mosquitto version %s starting" \0 "2.0.18" \0 …
   └── Anker ──────────────────┘      └ Version
```

Das bleibt ein Indizienschluss und wird als solcher geführt: Güte
*wahrscheinlich* statt *sicher*, und im Nachweis stehen Anker und Abstand in
Byte, damit die Zuordnung am Gerät nachprüfbar ist. Nennt dieselbe Datei die
Version irgendwo zusammenhängend, gewinnt dieser Beleg — ein Indiz verdrängt
nie einen Nachweis. Findet sich gar nichts, bleibt die Komponente ohne
Version in der Stückliste; ohne Version wird nicht gegen die NVD abgeglichen,
geraten wird hier nichts.

**Dasselbe Programm nicht zweimal zählen.** iPerf 2 und iPerf 3 sind
verschiedene Produkte mit verschiedenen CPE-Bezeichnern — aber das Programm
heisst in beiden Zweigen `iperf`, und wo nur der Pfad den Fund belegt, ist der
Zweig nicht bestimmbar. Ein versionsloser Fund wird deshalb von einem
versionierten Geschwister derselben Produktfamilie aufgesogen, sein Nachweis
wandert mit. Stehen beide Versionen fest, sind es tatsächlich zwei Programme
und beide bleiben.

Das ist kein Selbstzweck. In der geprüften barox-Firmware
(`RY-LGSO38-10_V8.90.1800`) **fehlt OpenSSL im Hersteller-Manifest vollständig**,
obwohl `libcrypto.so.1.1` mit der Kennung `OpenSSL 1.1.1g` (April 2020)
enthalten ist. Sechs weitere Angaben im Manifest sind veraltet:

| Komponente | laut Manifest | tatsächlich in der Firmware |
|---|---|---|
| Mbed TLS | 2.16.6 | **3.5.2** |
| Dropbear SSH | 2019.78 | **2022.82** |
| Hiawatha | 10.10 | **11.5** |
| libssh2 | 1.9.0 | **1.10.0** |
| wpa_supplicant | 0.6.1 | **2.9** |
| U-Boot | 2019.10 | **2020.01** |

Widersprechen sich Stückliste und Datei, gewinnt der Nachweis aus der Datei;
die Abweichung erscheint im Bericht. Umgekehrt gilt: wo die Zahl im
Bibliotheksnamen **nicht** der Produktversion entspricht (`libcurl.so.4.6.0`
gehört zu curl 7.68.0, `libnet.so.1.7.0` zu libnet 1.1.6), wird sie bewusst
nicht als Version verwendet.

Gleichnamiges wird auseinandergehalten, wo es zwei Produkte sind: **iPerf 2
und iPerf 3** stammen aus verschiedenen Quellbäumen und werden von der NVD
unter verschiedenen CPE-Bezeichnern geführt. Die geprüfte Firmware enthält
beide — `/usr/bin/iperf` (2.0.13) und `libiperf.so.0.0.0` (3.7). Sie
zusammenzufassen hiesse, die Schwachstellen des einen gegen die
Versionsnummern des anderen abzugleichen.

**Rückstand zeigen.** Zu jeder Komponente steht neben der eingebauten die
zuletzt bekannte Fassung des Projekts: erst dadurch wird aus „OpenSSL 1.1.1g"
die Aussage, wie weit das zurückliegt und ob eine Aktualisierung ein
Patch-Stand oder ein Hauptzweig ist. Die Angabe wird **nicht selbsttätig
nachgeführt** — sie ist Datenbestand, kein Messwert. Sie trägt deshalb ein
Prüfdatum, das überall mitläuft, und ist unter *Verwaltung →
Erkennungsregeln* je Komponente pflegbar.

Ist die im Gerät gefundene Fassung **neuer** als die hinterlegte, gilt nicht
das Gerät als veraltet, sondern der Eintrag — und die Oberfläche sagt das auch
so. Das ist kein Randfall: das zum Vergleich herangezogene kommerzielle
Werkzeug führt für den in dieser Firmware enthaltenen Hiawatha 11.5 die
Fassung 10.12 als „latest version" und fordert damit ein Downgrade.

**Widersprüche der Quelle sichtbar machen.** Widersprechen sich Stückliste und
Datei, gewinnt die Datei. Widerspricht sich die Stückliste **selbst**, gewinnt
niemand: die geprüfte Firmware führt ISC DHCP zweimal — als 4.1.0 unter
ISC-Lizenz und als 4.4.1 unter MPL-2.0, beides in derselben Datei. Als zwei
Zeilen in der Stückliste sieht das aus wie zwei eingebaute Fassungen;
tatsächlich ist es eine Angabe, die sich selbst widerspricht.

Verworfen wird nichts — entscheiden lässt sich das nur am Gerät. Aber beide
Einträge werden gekennzeichnet, der Widerspruch erscheint als Hinweis, und die
FOSS-Prüfung weiss, dass hier zwei unvereinbare Lizenzangaben nebeneinander
stehen. Zwei verschiedene Lizenzen für dasselbe Paket sind kein Schönheitsfehler:
davon hängt ab, welche Pflichten das Gerät auslöst.

**Abgleichen.** Gegen einen lokalen Spiegel der National Vulnerability Database
(NIST), ergänzt um die CISA-Liste aktiv ausgenutzter Schwachstellen (KEV) und
die EPSS-Werte. Der Abgleich läuft über CPE-Bezeichner und wertet die
Versionsbereiche der NVD korrekt aus — inklusive der Eigenheiten echter
Versionsschemata (`1.1.1g` ist neuer als `1.1.1`, `1.0-rc1` aber älter als `1.0`).

**Den Konfigurationsbaum lesen, nicht nur die CPE einsammeln.** Ein CVE-Eintrag
der NVD ist kein Produktverzeichnis, sondern eine Bedingung. Er nennt neben dem
betroffenen Produkt oft die Umgebung, in der es laufen muss — verknüpft mit
`AND`, `OR` und gelegentlich `negate`. Wer den Baum flachklopft und alle CPE
gleich behandelt, erbt für jedes System die Fehler aller Programme, die darauf
laufen. Der Anlass war eine Chrome-Schwachstelle, die nebenbei ein
Betriebssystem nennt: auf einem barox-Switch läuft kein Chrome, der Eintrag hing
trotzdem am Linux-Kernel der Firmware.

`src/Vuln/CpeConfig.php` wertet den Baum vollständig aus und gibt jeder Zeile
eine Rolle:

| Rolle | Bedeutung | wird abgeglichen |
|---|---|---|
| **betroffen** | Das Produkt selbst ist verwundbar (`vulnerable: true`, und die Struktur widerspricht dem nicht). | ja |
| **Umgebung** | Voraussetzung, auf der das betroffene Produkt läuft — Plattform-CPE. | nein |
| **ausgenommen** | Durch `negate` ausdrücklich ausgeschlossen. | nein |

Sieben Regeln entscheiden das:

1. Der Baum wird vollständig ausgewertet — `AND`, `OR`, verschachtelte
   Kindknoten und `negate`. Zeilen einer UND-Kombination bleiben als solche
   erkennbar (`and_group`), sie gelten nur zusammen.
2. Nur CPE mit `vulnerable: true` gelten als verwundbares Produkt. Fehlt das
   Feld, ist die Zeile keine Aussage über Verwundbarkeit.
3. Plattform-CPE werden gespeichert, aber nie als betroffene Komponente
   abgeglichen — sie bleiben als Nachweis in der CVE-Ansicht sichtbar.
4. Der CPE-Teil zählt: `a` Anwendung, `o` Betriebssystem, `h` Hardware.
5. Hersteller **und** Produkt müssen zur erkannten Komponente passen, dazu der
   Teil. Wird eine Zeile nur wegen des Teils verworfen, vermerkt die Analyse
   das sichtbar — ein falscher Teil im Regelkatalog darf keine Befunde
   stillschweigend verschlucken.
6. Liegen die Originalangaben des CNA vor, gehen sie den von der NVD
   nachgetragenen Plattform-CPE vor. Passen die Namen beider Welten zu keiner
   einzigen Zeile zueinander (der Kernel heisst beim CNA „Linux", in der CPE
   `linux_kernel`), bleibt es beim Baum — lieber keine Verschärfung als eine,
   die den ganzen Eintrag entwertet.
7. Eine Chrome-Schwachstelle landet niemals allein wegen des Betriebssystems
   am Linux-Kernel. Nennt ein Eintrag eine verwundbare Anwendung und daneben
   ein Betriebssystem **ohne jede Versionsgrenze**, ist das eine Angabe darüber,
   wo die Anwendung ausgeliefert wird — keine über den Kernel. Eine echte
   Kernel-Schwachstelle trägt immer Grenzen und bleibt unangetastet.

Ändert sich diese Auswertung, reicht der nächtliche Nachtragsabgleich nicht:
er fragt die NVD nur nach neuerem Datum. Der Bestand muss dann einmal
vollständig neu gelesen werden — `php bin/nvd-sync.php --force`.

**Befunde nach Gewissheit trennen.** Eine einzelne Zahl „offene Befunde" ist
keine Aussage. Sie wirft Treffer, deren Version aus der Datei belegt ist, mit
Treffern zusammen, die allein an einer Angabe der Hersteller-Stückliste hängen
— und dieselbe Stückliste lag in dieser Firmware bei sechs von sieben
nachprüfbaren Komponenten daneben. Ein Treffer auf eine falsche Version ist
kein Befund, sondern ein Fehlalarm mit Nachkommastelle. Gezählt wird deshalb
getrennt:

| | Bedeutung |
|---|---|
| **bestätigt** | Die Version, gegen die abgeglichen wurde, ist aus dem Gerät nachgewiesen — Zeichenkette in der Datei, Bibliotheksname, Systemdatei, Kernel. |
| **potenziell** | Der Treffer hängt an einer Angabe statt an einem Nachweis. Er stimmt, *wenn* die Angabe stimmt. |
| **ungeprüft** | Komponenten ohne bestimmbare Version. Für sie wurde gar nicht abgeglichen — das ist etwas anderes als „keine gefunden". |

**Den Kernel gesondert führen.** Der Linux-Kernel bringt in einer Firmware
mehr Treffer mit als alle übrigen Komponenten zusammen — in der geprüften rund
5800 von 6100, davon 70 kritische. Neben dieser Zahl ist nicht mehr zu sehen,
dass in OpenSSL einer steckt. Die Kennzahlen der Analyse zählen den Kernel
deshalb nicht mit, sondern führen ihn daneben; aktiv ausgenutzte Einträge
(KEV) zählen immer mit, unabhängig davon.

Ausgeblendet ist nichts. Die Befunde stehen unverändert in der Liste, die
Vorauswahl des Filters heisst *ohne Kernel* und lässt sich auf *nur
Linux-Kernel* oder *alle* stellen. Der Kernel braucht eine eigene Betrachtung
— seine Treffer sind weder falsch noch alle erreichbar, und in dieselbe Spalte
geworfen macht er beide Zahlen unbrauchbar.

**Bewerten nach CVSS 4.0.** Angezeigt wird der Basiswert nach CVSS 4.0, sofern
die NVD einen führt; sonst 3.1, ersatzweise 3.0 oder 2.0. Diese Reihenfolge ist
nötig, weil die NVD 4.0-Werte **nicht selbst berechnet** — sie stammen vom
zuständigen CNA und liegen bisher nur für einen Teil der Einträge vor. Ein
harter Schnitt auf 4.0 liesse die Mehrzahl der Befunde ohne Bewertung zurück.
Die Schwellen der Schweregrade sind in 4.0 und 3.1 identisch, die Werte bleiben
also untereinander vergleichbar. Welche Fassung hinter einem Wert steht, ist am
Befund vermerkt, steht in der CSV-Spalte *CVSS-Fassung* und landet im
CycloneDX-Export als `method` (`CVSSv4` bzw. `CVSSv31`). Wie weit 4.0 im
eigenen Bestand reicht, zeigt `php bin/nvd-sync.php --cvss`.

**Härtung der Programmdateien prüfen.** Der CVE-Abgleich beantwortet nur die
halbe Frage. Er sagt, welche bekannte Schwachstelle in einer Komponente
steckt — nicht, wie teuer es ist, aus einem Speicherfehler einen Angriff zu
machen. Das entscheiden die Übersetzungseinstellungen, mit denen der
Hersteller gebaut hat, und dieser Befund gilt auch für Schwachstellen, die
heute noch niemand kennt. Er hängt an keiner Fremdkomponente und verschwindet
deshalb nicht dadurch, dass eine Bibliothek aktualisiert wird.

Geprüft wird jede ELF-Datei im Abbild, ohne Fremdwerkzeug (kein `checksec`,
kein `readelf`) direkt am Dateiformat:

| Merkmal | Woran es erkannt wird | Wozu es dient |
|---|---|---|
| Lageunabhängig (PIE) | `ET_DYN` mit Interpreter statt `ET_EXEC` | ohne PIE liegt das Programm bei jedem Start an derselben Adresse; Sprungziele lassen sich fest eintragen statt raten |
| Stack-Schutz | Symbol `__stack_chk_fail` | Wächterwert vor der Rücksprungadresse |
| FORTIFY_SOURCE | Symbole der Form `__…_chk` | `memcpy` & Co. kennen die Zielgrösse und brechen beim Überlauf ab |
| RELRO | `PT_GNU_RELRO` | Schreibschutz der Sprungtabellen nach dem Laden |
| Sofortbindung | `DT_BIND_NOW`, `DF_BIND_NOW`, `DF_1_NOW` | erst zusammen mit RELRO voller Schutz |
| Stack nicht ausführbar | `PT_GNU_STACK` ohne Ausführungsrecht | in einen Puffer geschriebene Daten sind keine Befehle |
| Suchpfad | `DT_RPATH` / `DT_RUNPATH` | relative oder beschreibbare Pfade erlauben untergeschobene Bibliotheken |

Gezählt wird über die Prüfsumme, nicht über den Pfad: BusyBox liegt unter
Dutzenden Namen im Dateisystem und würde die Quote sonst beherrschen. Jede
Datei bekommt eine Note von 0 bis 4, die Firmware daraus einen Wert von 0 bis
100. Ausgewiesen wird beides — die Einzelfälle und die Quote je Massnahme,
denn erst die Quote nimmt den Hersteller in die Pflicht: „keines von 148
Programmen ist lageunabhängig übersetzt" ist eine Aussage über den
Bauprozess, eine einzelne ungehärtete Datei ist Rauschen.

Zwei Einschränkungen stehen in der Ansicht und im Bericht:

- **Stack-Schutz und FORTIFY_SOURCE werden am Symbol erkannt.** Ein Programm,
  das keine einzige zu schützende Funktion aufruft, trägt auch bei
  eingeschalteter Option kein solches Symbol. Der Befund lautet deshalb „kein
  Nachweis", nicht „abgeschaltet" — bei kleinen Hilfsprogrammen ist das ein
  Unterschied.
- **PIE ist bei gemeinsam genutzten Bibliotheken gegenstandslos.** Eine
  `.so`-Datei ist ohnehin lageunabhängig; sie zählt bei dieser einen Massnahme
  nicht mit, sonst schönte sie das Bild.

**Lizenzen prüfen (FOSS-Prüfung).** Neben der Frage „welche Schwachstelle" steht
die Frage „welche Pflicht". Für jede Komponente wird die Lizenz bestimmt, auf eine
**SPDX-Kennung** normalisiert und eingeordnet — freizügig, schwaches Copyleft,
starkes Copyleft, Netzwerk-Copyleft, gemeinfrei, unfrei. Dieselbe Mehrgleisigkeit
wie bei den Versionen, und wieder gewinnt der Nachweis aus dem Gerät:

| Quelle | Beispiel | Rang |
|---|---|---|
| Lizenzdatei im Abbild | `/usr/share/licenses/busybox/COPYING` — erkannt am **Wortlaut**, nicht am Dateinamen | 1 |
| Paketdatenbank | Feld `License:` in opkg, apk, dpkg | 2 |
| Hersteller-Stückliste | Spalte „License Type" | 3 |
| Regelkatalog | die bekannte Lizenz des Projekts | 4 |

Widersprechen sich zwei Quellen, gilt der Lizenztext, und die Abweichung wird
ausgewiesen — sagt die Stückliste `GPL-2.0` und der `COPYING`-Text „version 2 …
or (at your option) any later version", ist das ein Unterschied, der zählt.
Unklare Angaben bleiben unklar: `BSD` ohne Klauselzahl wird **nicht** geraten, weil
BSD-4-Clause eine Werbeklausel trägt und BSD-2/3 nicht.

Daraus entstehen drei Dinge:

- eine **Pflichtenliste** je Gerät (Lizenztext beilegen, Quelltext anbieten,
  Austausch der Bibliothek ermöglichen, Änderungen kennzeichnen, Werbeklausel …),
- eine **Bewertung** je Lizenz — unbedenklich, zu prüfen, nicht zulässig; Vorgabe
  je Kategorie, unter *Verwaltung → Lizenzrichtlinie* je Lizenz festlegbar,
- die **FOSS-Erklärung** als Beilage zum Gerät: Komponentenliste plus die in der
  Firmware gefundenen Lizenztexte im Wortlaut, als Webseite und als Textdatei —
  letztere passt direkt in das Feld „FOSS declaration" des Firmware-Portals.

Das ersetzt keine juristische Prüfung. Es zeigt, wo eine nötig ist.

**Kernel-CVE auf das Relevante begrenzen.** Der Linux-Kernel ist das
CVE-reichste Produkt der NVD, und der grösste Teil betrifft Subsysteme, die es
auf einem Switch nicht gibt. Diese Einträge werden beim Abgleich gar nicht erst
geholt — auf Basis eines Nachweises aus der Firmware selbst:

- die im Kernel-Image eingebettete Konfiguration (`IKCFG`-Block; in der
  geprüften barox-Firmware 818 gesetzte Optionen)
- `/lib/modules/<version>/modules.builtin` (176 einkompilierte Module)

Verworfen wird nur, was in einer **Positivliste** steht *und* für das die
Abwesenheit belegt ist. Netzwerk-Stack, netfilter, Bridge, VLAN, Crypto,
Dateisysteme, MTD, SPI/I²C/GPIO und alles Unbekannte werden nie verworfen.
Gegen die echte Firmware geprüft:

| CVE-Beschreibung | Entscheidung | Beleg |
|---|---|---|
| `ALSA: usb-audio: …` | verworfen | `CONFIG_SND` nicht gesetzt |
| `drm/amdgpu: …` | verworfen | `CONFIG_DRM` nicht gesetzt |
| `wifi: ath11k: …` | verworfen | `CONFIG_CFG80211` nicht gesetzt |
| `KVM: x86: …` | verworfen | `CONFIG_KVM` nicht gesetzt |
| `io_uring: …` | **behalten** | `CONFIG_IO_URING=y` |
| `netfilter: nf_tables: …` | **behalten** | Kernbereich, nie verwerfbar |
| `ubifs: …` | **behalten** | `CONFIG_UBIFS_FS=y` |

Was verworfen wurde, wird pro Subsystem mitgezählt und ist unter
**Verwaltung → Schwachstellendaten** sowie über `php bin/nvd-sync.php --skips`
einsehbar. Taucht später ein Gerät mit grösserem Funktionsumfang auf, meldet
das Tool, dass ein Neuabgleich nötig ist. Abschalten über
`KERNEL_RELEVANCE_FILTER=false` bzw. `--no-filter`.

**Bewerten und ausleiten.** Jeder Befund lässt sich mit Status und
VEX-Begründung versehen. Die Bewertung wird dauerhaft gespeichert und bei
künftigen Analysen derselben Komponenten-Version automatisch übernommen.

**Auswählen und Massnahmen festhalten.** Die Befundliste lässt sich nach
Schweregrad, CVSS-, EPSS-Schwelle, KEV, Status und Suchbegriff filtern; die
angehakten Treffer bleiben in der Analyse, der Rest wird aussortiert. Aussortiert
heisst nicht gelöscht: die Zeilen bleiben in der Datenbank, verschwinden aber aus
Kennzahlen, SBOM, Bericht und Komponentenansicht und lassen sich über den Filter
„aussortiert" jederzeit zurückholen. Zu jedem Befund lässt sich ein **Workaround
in Deutsch und Englisch** hinterlegen. Er hängt wie die Bewertung an Komponente
und CVE und steht deshalb bei jeder weiteren Analyse derselben Komponente wieder
zur Verfügung; ein Eintrag für eine konkrete Version geht dem Eintrag für alle
Versionen vor. Der Bericht führt die ausgewählten Befunde mit ihren Massnahmen
auf und erscheint auf Wunsch englisch (`?sprache=en`).

Export als CycloneDX-1.5-SBOM (inkl. VEX, mit der Zielmarke je Komponente als
`barox:latestVersion`), CSV — Komponenten, Befunde und Härtung — und Bericht als Webseite und
als **PDF im barox-Erscheinungsbild**, das sich direkt herunterladen lässt.
Das PDF wird ohne Fremdbibliothek geschrieben (`src/Export/Pdf.php`), bleibt
also mit dem übrigen Werkzeug ohne Composer und ohne Build-Schritt lauffähig;
gesetzt wird in Helvetica, dem im Design-System vorgesehenen Ersatz für
Helvetica Now.

## Projektstruktur

```
barox-appsec/
├── public/                Webroot – der einzige öffentlich erreichbare Ordner
│   ├── index.php          Front-Controller / Router
│   ├── .htaccess          Rewrites, HTTPS-Zwang, Härtung
│   └── assets/            CSS, JS, Logos, Schriften
├── src/
│   ├── Core/              Env, Database, Session, Csrf, Auth, OidcClient, View,
│   │                      Router, SchemaDiff (Soll-/Ist-Vergleich des Schemas)
│   ├── Unpack/            Format, EzcmContainer, FitImage, SquashFs, Decompressor,
│   │                      ArchiveExtractor, Carver, Unpacker, Tools
│   ├── Detect/            RuleCatalog, ComponentDetector, VersionUtil, Cpe,
│   │                      ElfInfo (ELF-Leser), Hardening (Schutzmassnahmen),
│   │                      Detectors/{Manifest,System,Soname,String,Path}Detector
│   ├── Vuln/              NvdClient, NvdSync, CpeConfig, Matcher
│   ├── Portal/            FirmwarePortal (lesender Zugriff auf barox-firmware)
│   ├── License/           Spdx (Lizenzwissen), LicenseDetector, Policy
│   ├── Scan/              ScanRunner, FilenameParser
│   ├── Export/            CycloneDx, Pdf (PDF-Schreiber), ReportPdf (Bericht)
│   └── Controllers/
├── templates/             PHP-Templates
├── database/schema.sql    Sollzustand einer Neuanlage
├── database/migrations/   Schemaänderungen für bestehende Datenbanken
├── bin/                   install, selftest, scan-dryrun, scan-worker, nvd-sync, cleanup
├── config/.env.example
└── storage/               NICHT im Webroot: uploads/, work/, logs/, cache/
```

## Installation auf dem Ubuntu-Server

### 1. Pakete

```bash
sudo apt update
sudo apt install -y apache2 mysql-server \
    php php-mysql php-curl php-mbstring php-zip php-xml \
    squashfs-tools xz-utils unzip cpio zstd lz4
```

`squashfs-tools` ist keine Kür: ohne `unsquashfs` bleibt das Rootfs der
allermeisten Firmwares verschlossen. Die übrigen Werkzeuge decken einzelne
Formate ab; welche fehlen, zeigt `php bin/install.php --check`.

### 2. Datenbank

```bash
sudo mysql <<'SQL'
CREATE DATABASE barox_appsec CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'barox'@'localhost' IDENTIFIED BY 'HIER_PASSWORT_SETZEN';
GRANT ALL PRIVILEGES ON barox_appsec.* TO 'barox'@'localhost';
FLUSH PRIVILEGES;
SQL
```

### 3. Dateien ablegen

```bash
sudo mkdir -p /var/www/html/barox-appsec
# Projekt hochladen (SFTP, siehe .vscode/sftp.json), dann:
cd /var/www/html/barox-appsec
sudo chown -R root:www-data .
sudo find . -type d -exec chmod 750 {} \;
sudo find . -type f -exec chmod 640 {} \;
sudo chmod 750 bin/*.php
sudo chmod -R 770 storage
sudo chown -R www-data:www-data storage
```

Der Ordner `assets/` enthält nur die Design-Quellen (Original-Schriften und
-Logos) und muss nicht aufs Produktivsystem. Der Ordner `FW/` mit
Beispiel-Firmware ebenso wenig.

### 4. Konfiguration

```bash
sudo cp config/.env.example config/.env
sudo -e config/.env
sudo chown root:www-data config/.env && sudo chmod 640 config/.env
```

Mindestens zu setzen:

```ini
APP_URL=https://appsec.barox.io
APP_SECRET=…                 # php -r "echo bin2hex(random_bytes(32));"
DB_NAME=barox_appsec
DB_USER=barox
DB_PASS=…
MS_TENANT_ID=…
MS_CLIENT_ID=…
MS_CLIENT_SECRET=…
NVD_API_KEY=…                # kostenlos, sonst ist der Abgleich zehnmal langsamer
AUTH_DEV_BYPASS=false
FWPORTAL_PATH=/var/www/html/barox-firmware/firmware
```

### 4a. Firmware-Portal anbinden

Damit sich eine Firmware auswählen statt hochladen lässt, braucht appsec
Lesezugriff auf die Datenbank und die Dateiablage von `barox-firmware`. Der Pfad
steht dort in `config.php` als `FIRMWARE_PATH`.

```sql
GRANT SELECT ON barox_firmware.models            TO 'barox'@'localhost';
GRANT SELECT ON barox_firmware.firmware_versions TO 'barox'@'localhost';
FLUSH PRIVILEGES;
```

```bash
# Lesezugriff auf die Ablage, ohne die Rechte des Portals anzutasten
sudo setfacl -R -m u:www-data:rX  /var/www/html/barox-firmware/firmware
sudo setfacl -dR -m u:www-data:rX /var/www/html/barox-firmware/firmware
```

Läuft das Portal auf einem anderen Datenbankserver oder unter anderem Konto,
sind `FWPORTAL_DB_HOST`, `FWPORTAL_DB_USER` und `FWPORTAL_DB_PASS` zu setzen;
leer bedeutet „wie die appsec-Datenbank". `php bin/install.php --check` meldet,
ob Datenbank und Ablage erreichbar sind. Ohne diese Anbindung bleibt der
Upload – das Werkzeug funktioniert vollständig, nur eben mit Dateiauswahl.

Geschrieben wird im Portal nichts: appsec liest die Datei an Ort und Stelle und
packt sie in sein eigenes Arbeitsverzeichnis aus.

### 5. Entra-ID-App registrieren

Im Entra Admin Center (<https://entra.microsoft.com>):

1. **App-Registrierungen → Neue Registrierung**, Single-Tenant.
2. Redirect-URI (Typ *Web*): `https://appsec.barox.io/auth/callback`
3. **Zertifikate & Geheimnisse → Neues Client-Secret**, Wert in `.env` eintragen.
4. `MS_TENANT_ID` ist die Verzeichnis-(Mandanten-)ID.

Es werden nur die Standardberechtigungen `openid email profile` benötigt —
keine Administratorzustimmung nötig.

### 6. Apache

```apache
<VirtualHost *:443>
    ServerName appsec.barox.io
    DocumentRoot /var/www/html/barox-appsec/public

    <Directory /var/www/html/barox-appsec/public>
        AllowOverride All
        Require all granted
    </Directory>

    # storage/ und config/ liegen ausserhalb des DocumentRoot und sind
    # damit ohnehin nicht erreichbar.

    SSLEngine on
    SSLCertificateFile      /etc/letsencrypt/live/appsec.barox.io/fullchain.pem
    SSLCertificateKeyFile   /etc/letsencrypt/live/appsec.barox.io/privkey.pem

    ErrorLog  ${APACHE_LOG_DIR}/appsec-error.log
    CustomLog ${APACHE_LOG_DIR}/appsec-access.log combined
</VirtualHost>
```

```bash
sudo a2enmod rewrite ssl headers expires
sudo systemctl reload apache2
```

Firmware-Dateien sind gross — in der `php.ini` (bzw. im FPM-Pool) anpassen:

```ini
upload_max_filesize = 512M
post_max_size       = 520M
memory_limit        = 512M
max_execution_time  = 300
```

### 7. Einrichten und Daten holen

```bash
cd /var/www/html/barox-appsec
sudo -u www-data php bin/install.php          # prüft alles, legt das Schema an
sudo -u www-data php bin/nvd-sync.php --seed  # Produkte des Regelkatalogs vormerken
sudo -u www-data php bin/nvd-sync.php         # NVD + KEV + EPSS holen
```

Der erste Abgleich dauert mit API-Schlüssel wenige Minuten, ohne deutlich
länger. Danach genügt der nächtliche Nachtrag.

Der Linux-Kernel ist dabei der Sonderfall: seine Einträge führen je Stück
hunderte Versionsbereiche, eine volle Seite von 2000 Stück ist ein Vielfaches
dessen, was andere Produkte liefern. Reisst die Übertragung ab, senkt der
Abruf die Seitengrösse selbst und holt erneut — sichtbar als Hinweis im Lauf.
Bleibt es dabei, lässt sich mit `NVD_PAGE_SIZE` (Vorgabe 2000) und
`NVD_TIMEOUT` (Vorgabe 180 s) dauerhaft kleiner ansetzen.

### 8. Cronjobs

Die Jobs müssen als **www-data** laufen, weil `storage/` diesem Benutzer
gehört. Sie gehören deshalb nach `/etc/cron.d/` und **nicht** in ein
persönliches Crontab (`crontab -e`) — dort gibt es das Benutzerfeld nicht.

```bash
sudo tee /etc/cron.d/barox-appsec > /dev/null <<'CRON'
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# Auffangnetz für Analysen, deren Hintergrundstart fehlgeschlagen ist
* * * * * www-data /usr/bin/php /var/www/html/barox-appsec/bin/scan-worker.php --queue

# Schwachstellendaten nachts aktualisieren
17 3 * * * www-data /usr/bin/php /var/www/html/barox-appsec/bin/nvd-sync.php >> /var/www/html/barox-appsec/storage/logs/nvd-sync.log 2>&1

# alte Arbeitsverzeichnisse aufräumen
40 4 * * * www-data /usr/bin/php /var/www/html/barox-appsec/bin/cleanup.php
CRON

sudo chmod 644 /etc/cron.d/barox-appsec
```

Zwei Eigenheiten von cron, die hier zählen:

- **Es gibt keine Zeilenfortsetzung.** Jeder Eintrag muss auf *einer* Zeile
  stehen; ein `\` am Zeilenende führt zur Fehlermeldung `bad minute`, weil die
  Folgezeile als neuer Eintrag gelesen wird.
- **Ein `%` im Kommando** beendet für cron den Befehl und muss als `\%`
  geschrieben werden. Die Zeilen oben enthalten keines.

Prüfen, ob cron die Datei angenommen hat:

```bash
sudo systemctl status cron
grep CRON /var/log/syslog | tail
```

### 9. Erste Anmeldung

Die erste Person, die sich anmeldet, wird automatisch Administrator. Danach
werden weitere Konten unter **Verwaltung → Benutzer** freigeschaltet; neue
Konten starten mit Lesezugriff.

## Bedienung

| Aufgabe | Weg |
|---|---|
| Firmware prüfen | **Firmware analysieren** → Stand im Portal suchen → **Analysieren** |
| Datei prüfen, die nicht im Portal liegt | **Firmware analysieren** → **Datei hochladen** |
| Stückliste ansehen | Analyse → **Stückliste** |
| Härtung der Programme prüfen | Analyse → **Härtung** |
| Zielmarke einer Komponente pflegen | **Verwaltung → Erkennungsregeln** → *Aktuelle Fassung* |
| Lizenzen und Pflichten prüfen | Analyse → **Lizenzen** |
| FOSS-Erklärung erzeugen | Analyse → **Lizenzen** → **FOSS-Erklärung (Text)** |
| Lizenz anders bewerten | **Verwaltung → Lizenzrichtlinie** |
| Befunde bearbeiten | Analyse → **Alle Befunde** → Befund öffnen → Bewertung speichern |
| Relevante CVEs auswählen | Analyse → **Alle Befunde** → filtern → anhaken → **Auswahl übernehmen** |
| Nur belegte Treffer ansehen | Analyse → **Alle Befunde** → *Gewissheit: bestätigt* |
| Kernel-Befunde ansehen | Analyse → **Alle Befunde** → *Bereich: nur Linux-Kernel* |
| Workaround hinterlegen | Befund öffnen → Abschnitt **Bericht** → Text DE/EN → speichern |
| Bericht auf Englisch | Analyse → **Report (PDF, EN)** bzw. `?sprache=en` |
| Nachvollziehen, woher eine Komponente stammt | Komponente öffnen → Abschnitt **Nachweise** |
| Sehen, wie die Datei zerlegt wurde | Analyse → **Aufbau** |
| Zwei Firmware-Stände vergleichen | Analyse → **Mit früherer Analyse vergleichen** |
| SBOM weitergeben | Analyse → **SBOM (CycloneDX)** |
| Härtung ausleiten | Analyse → **Härtung** → **CSV** |
| Bericht ansehen | Analyse → **Bericht** |
| Bericht ablegen | Analyse → **Bericht (PDF)** – lädt die gesetzte Fassung herunter |

### Rollen

- **Lesezugriff** — darf alles ansehen.
- **Analyst** — darf zusätzlich Firmware hochladen und Befunde bewerten.
- **Administrator** — darf zusätzlich Benutzer, Erkennungsregeln und
  Datenbestände verwalten.

## Kommandozeile

```bash
php bin/install.php [--check]      # Umgebung prüfen, Schema anlegen bzw. nachziehen,
                                   # Regeln einspielen. --check meldet offene
                                   # Schemaänderungen, ohne etwas zu ändern.
                                   # Ergänzt fehlende Tabellen und Spalten und
                                   # verbreitert zu kurz gewordene Textspalten;
                                   # echte Typwechsel werden nur gemeldet.
php bin/selftest.php [firmware]    # Engine testen (ohne Datenbank, ohne Netz)
php bin/scan-dryrun.php <datei>    # Probelauf: entpacken + Komponenten, ohne Datenbank
php bin/scan-dryrun.php --rootfs <verzeichnis>
php bin/scan-worker.php <id>       # einen Analyselauf ausführen
php bin/scan-worker.php --queue    # wartende Läufe abarbeiten
php bin/nvd-sync.php [--seed|--kev|--epss|--product hersteller:produkt|--force]
php bin/nvd-sync.php --skips       # was der Kernel-Relevanzfilter verworfen hat
php bin/nvd-sync.php --cvss        # Verteilung der CVSS-Fassungen im Bestand
php bin/nvd-sync.php --rescore     # Score und Schweregrad bestehender Befunde
                                   # aus dem Spiegel nachziehen (ohne Neuanalyse)
php bin/nvd-sync.php --product linux:linux_kernel --force --no-filter
                                   # Kernel vollständig, ohne Relevanzfilter
php bin/cleanup.php [--dry]        # Arbeitsverzeichnisse aufräumen
```

`scan-dryrun.php` ist der schnellste Weg, eine neue Erkennungs- oder Lizenzregel
zu prüfen: er braucht weder Datenbank noch Anmeldung und gibt Stückliste,
Rückstand, Härtung, Lizenzen und gefundene Lizenztexte direkt aus (`--json` für
die maschinelle Weiterverarbeitung).

## Erkennungsregeln erweitern

Rund 60 Komponenten sind im Katalog hinterlegt
(`src/Detect/RuleCatalog.php`). Ergänzungen gehen ohne Code-Änderung über
**Verwaltung → Erkennungsregeln**:

| Feld | Bedeutung |
|---|---|
| Schlüssel | interner Bezeichner, dient dem Abgleich mit Stücklisten |
| CPE-Hersteller / -Produkt | genau so, wie die NVD sie führt — davon hängt der Abgleich ab |
| Suchausdruck | regulärer Ausdruck, **Gruppe 1 muss die Version enthalten** |
| Aktuelle Fassung | Zielmarke für den Rückstand; das Prüfdatum setzt das Werkzeug |

Der Ausdruck wird beim Speichern auf Gültigkeit geprüft; ein fehlerhaftes
Muster würde sonst jeden Analyselauf abbrechen. Zwei Fallstricke aus der
Praxis:

- **Zu weit gefasste Muster erzeugen Fehlalarme.** `/BIND (\d+\.\d+\.\d+)/`
  trifft in `libdns.so` auf den Hilfetext „BIND 9.17.2 and later if AXFR
  source" — und meldet damit eine Version, die gar nicht enthalten ist. Die
  eingebaute Regel schliesst solche Fliesstext-Treffer deshalb aus.
- **Offene Platzhalter greifen über Zeichenkettengrenzen hinweg.** In
  Binärdateien sollte der Suchbereich eng begrenzt sein
  (`[^\x00]{0,40}?` statt `.*?`).

Nach dem Anlegen greift die Regel ab dem nächsten Analyselauf. Ob der neue
CPE-Bezeichner Daten liefert, prüft **Verwaltung → Schwachstellendaten →
Einzelnes Produkt nachladen**.

## Was das Werkzeug nicht leistet

- **Ein Treffer ist keine Aussage über Ausnutzbarkeit.** Er bedeutet, dass die
  erkannte Version im Bereich liegt, den die NVD als betroffen führt. Ob der
  verwundbare Code im Produkt überhaupt erreichbar ist, entscheidet die
  Bewertung durch die Produktsicherheit — dafür gibt es die Triage.
- **Komponenten ohne bestimmbare Version werden nicht abgeglichen.** Sie
  erscheinen in der Stückliste und im SBOM, aber jeder CVE-Treffer wäre dort
  geraten. Das ist Absicht. Sie stehen als *ungeprüft* in den Kennzahlen —
  nicht als „ohne Befund".
- **Die Version neben der Formatvorlage ist erschlossen, nicht abgelesen.**
  Bei Mosquitto, radvd, Shell In A Box und netcat stammt sie aus der Nähe
  eines Ankers, nicht aus einer zusammenhängenden Angabe. Das Verfahren ist
  eng geführt — die Zeichenkette muss vollständig eine Versionsnummer sein,
  und die nächstgelegene gewinnt —, bleibt aber ein Indizienschluss. Deshalb
  Güte *wahrscheinlich*, und der Nachweis nennt Anker und Abstand.
- **Statisch einkompilierter Code ohne Versionszeichenkette bleibt unsichtbar.**
  Gegen diesen Fall hilft nur eine Angabe des Herstellers.
- **Verschlüsselte oder signiert-verschlossene Firmware** lässt sich nicht
  entpacken.
- **Die aktuelle Fassung einer Komponente wird nicht selbst ermittelt.** Sie
  steht im Regelkatalog und altert dort. Deshalb läuft überall ein Prüfdatum
  mit, und ein Eintrag, der älter ist als das geprüfte Gerät, wird als
  überholter *Eintrag* gemeldet und nicht als veraltetes Gerät. Wer den
  Rückstand als Kennzahl verwendet, muss den Katalog pflegen.
- **Fehlender Stack-Schutz kann auch heissen: nichts zu schützen.** Die
  Härtungsprüfung erkennt beide Massnahmen am Symbol; ein Programm ohne
  passenden Funktionsaufruf trägt keines. Der Wert ist als Quote über viele
  Dateien belastbar, im Einzelfall ist er ein Hinweis.
- **Der Kernel-Relevanzfilter holt bewusst nicht alles.** Er verwirft nur, wo
  die Abwesenheit eines Subsystems belegt ist — aber die verworfenen Einträge
  sind danach auch nicht mehr im Bericht nachweisbar, sondern nur noch als
  Zählung sichtbar. Wer eine lückenlose Historie braucht, schaltet ihn ab
  (`KERNEL_RELEVANCE_FILTER=false`) und gleicht einmal mit `--no-filter` ab.
- **Nicht ausgelesen werden derzeit** JFFS2, UBI/UBIFS und ext-Dateisysteme.
  Sie werden erkannt und im Bericht als solche vermerkt, ihr Inhalt fliesst
  aber nicht in die Stückliste ein.

## Sicherheit

- Anmeldung ausschliesslich über Entra ID (Authorization Code + PKCE,
  Signaturprüfung gegen JWKS, Nonce als Replay-Schutz, Tenant-Bindung).
- CSRF-Token auf allen schreibenden Formularen, `SameSite`-Cookies,
  strikte Content-Security-Policy, HSTS.
- Die CSP kommt **ohne `unsafe-inline`** aus: CSS und JavaScript gehören nach
  `public/assets/`. Ein `style`-Attribut oder ein `<style>`-Block im Template
  wäre wirkungslos — der Browser verwirft ihn stillschweigend. Werte, die erst
  zur Laufzeit feststehen (etwa der Fortschrittsbalken), reicht das Template
  als `data-`-Attribut weiter und `app.js` setzt sie.
- Hochgeladene Dateien landen ausserhalb des Webroots und werden unter ihrer
  SHA-256-Summe abgelegt — der vom Benutzer gelieferte Dateiname wird nie Teil
  eines Pfades.
- Archiveinträge werden gegen Pfadausbrüche geprüft; die Entpack-Engine hat
  Obergrenzen für Verschachtelungstiefe, Datenmenge und Dateianzahl.
- Alle Datenbankzugriffe über Prepared Statements ohne Emulation.
- IP-Adressen im Protokoll nur als HMAC-Hash.

`AUTH_DEV_BYPASS=true` meldet jeden Aufruf als Administrator an und ist
ausschliesslich für die lokale Entwicklung gedacht. `bin/install.php` meldet
es als Problem, sobald `APP_ENV=production` gesetzt ist.

## Verhältnis zu den anderen barox-Werkzeugen

- **barox-cve** (Meldeportal, CRA) veröffentlicht Sicherheitsmitteilungen nach
  aussen. `appsec` liefert die Grundlage dafür: welche Komponente in welchem
  Firmware-Stand betroffen ist.
- **barox-firmware** verwaltet die Firmware-Auslieferung. Die Verbindung geht in
  beide Richtungen: appsec holt sich von dort die zu prüfenden Firmware-Stände
  (Auswahl statt Upload, siehe *4a*), und das Portal holt sich umgekehrt die
  freigegebenen Befunde (`findings.in_report = 1`) für seine Release Notes aus
  der appsec-Datenbank (`includes/appsec.php`). Die hier erzeugten SBOM-Dateien
  lassen sich dort als Begleitdokument hinterlegen.
