Vous avez un SBOM parfait généré au build. Puis arrive une CVE critique. Vous vérifiez : “on n’utilise pas cette lib”. Sauf qu’en production, un plugin chargé dynamiquement l’utilise. Le SBOM décrivait ce qui avait été assemblé, pas l’état opérationnel réellement observé.
L’OBOM (Operations Bill of Materials) cherche à capturer cet état opérationnel : librairies chargées dynamiquement, modules activés selon l’environnement, configurations runtime, services connectés. C’est un complément au SBOM, pas un remplacement.
Dans ce guide, vous apprendrez :
- Pourquoi l’OBOM est nécessaire et ce qu’il capture de plus qu’un SBOM
- Comment collecter les informations runtime sur Linux et dans les conteneurs
- Comment générer un OBOM avec les outils disponibles
- Comment corréler SBOM et OBOM pour une visibilité complète
Prérequis
Section intitulée « Prérequis »- Lecture du guide SBOM : comprendre le Software Bill of Materials
- Lecture du guide XBOM : au-delà du SBOM
- Notions de base sur Linux et les conteneurs
SBOM vs OBOM : le décalage build/runtime
Section intitulée « SBOM vs OBOM : le décalage build/runtime »Ce que le SBOM capture… et ce qu’il manque
Section intitulée « Ce que le SBOM capture… et ce qu’il manque »| Aspect | SBOM (build-time) | OBOM (runtime) |
|---|---|---|
| Moment | À la compilation/packaging | En production |
| Source | Manifestes (package.json, go.mod, etc.) | Processus en cours d’exécution |
| Dépendances | Déclarées et transitives | Effectivement chargées |
| Configuration | Valeurs par défaut | Configuration active |
| Modules optionnels | Listés comme optionnels | Activés ou non |
| Plugins | Potentiels | Réellement chargés |
Exemple concret : une application Python
Section intitulée « Exemple concret : une application Python »# app.py - Application avec chargement dynamiqueimport osimport importlib
# Le driver de base de données dépend de l'environnementdb_driver = os.getenv("DB_DRIVER", "sqlite")db_module = importlib.import_module(f"databases.{db_driver}")
# Les plugins sont chargés depuis un dossierfor plugin in os.listdir("plugins/enabled"): importlib.import_module(f"plugins.{plugin}")Ce que le SBOM voit (inventaire produit à partir du build ou de l’environnement packagé) :
flask==3.0.0databases-sqlite==0.9.0databases-postgres==0.9.0 # dépendance extrasplugin-analytics==1.2.0 # plugin disponibleplugin-cache==2.0.0 # plugin disponibleNote : cet exemple est simplifié à des fins pédagogiques. En réalité, les sources de divergence sont plus variées (wheels, entry points, montages runtime, etc.).
Ce qui tourne en production (runtime réel) :
flask==3.0.0databases-postgres==0.9.0 # ← Celui-ci est activéplugin-cache==2.0.0 # ← Seul ce plugin est activépsycopg2==2.9.9 # ← Dépendance transitive de postgreslibpq.so.5 # ← Librairie C chargée par psycopg2Le SBOM liste databases-sqlite (défaut) mais la production utilise databases-postgres. Si une CVE touche psycopg2, un SBOM build-time peut ne pas suffire à dire si cette dépendance est réellement active dans cet environnement. L’OBOM aide alors à distinguer présence théorique et usage effectif.
Ce que l’OBOM capture
Section intitulée « Ce que l’OBOM capture »Les 5 catégories d’informations runtime
Section intitulée « Les 5 catégories d’informations runtime »Détail par catégorie
Section intitulée « Détail par catégorie »| Catégorie | Exemples | Pourquoi c’est important |
|---|---|---|
| Librairies chargées | libssl.so.3, libc.so.6, libpq.so.5 | CVE dans les libs C/système |
| Modules activés | Plugins WordPress, extensions Python | Code actif non visible au build |
| Configuration active | TLS_MIN_VERSION=1.2, DEBUG=false | Paramètres de sécurité effectifs |
| Services connectés | postgres://db:5432, redis://cache:6379 | Dépendances réseau |
| État système | Kernel 5.15, glibc 2.35 | Vulnérabilités système |
Collecter les informations runtime sur Linux
Section intitulée « Collecter les informations runtime sur Linux »Méthode 1 : Librairies partagées via /proc
Section intitulée « Méthode 1 : Librairies partagées via /proc »La méthode la plus directe pour lister les librairies chargées par un processus :
# Lister les librairies chargées par un processuscat /proc/$(pgrep -x python3)/maps | grep '\.so' | awk '{print $6}' | sort -u
# Exemple de sortie :/lib/x86_64-linux-gnu/libc.so.6/lib/x86_64-linux-gnu/libpthread.so.0/lib/x86_64-linux-gnu/libm.so.6/usr/lib/x86_64-linux-gnu/libssl.so.3/usr/lib/x86_64-linux-gnu/libcrypto.so.3/usr/lib/python3.11/lib-dynload/_ssl.cpython-311-x86_64-linux-gnu.so#!/bin/bash# Collecte les librairies chargées pour un processus
PID=$1if [ -z "$PID" ]; then echo "Usage: $0 <PID>" exit 1fi
echo "=== Runtime libraries for PID $PID ==="echo ""
# Nom du processusPROC_NAME=$(cat /proc/$PID/comm)echo "Process: $PROC_NAME"echo ""
# Librairies .so uniquesecho "Shared libraries:"cat /proc/$PID/maps | \ grep -E '\.so(\.[0-9]+)*$' | \ awk '{print $6}' | \ sort -u | \ while read lib; do # Afficher le chemin de la librairie if [ -f "$lib" ]; then echo " - $lib" fi done
# Fichiers ouverts (fd)echo ""echo "Open files (non-trivial):"ls -la /proc/$PID/fd 2>/dev/null | \ grep -v -E '(pipe|socket|anon_inode|/dev/)' | \ awk '{print $NF}' | \ tail -n +4# Librairies chargées via lsoflsof -p $(pgrep -x python3) | grep -E '\.so' | awk '{print $9}' | sort -u
# Fichiers ouverts (config, data)lsof -p $(pgrep -x python3) | grep -E '\.(conf|yaml|json|env)' | awk '{print $9}'
# Connexions réseaulsof -p $(pgrep -x python3) -i -n | grep -E '(ESTABLISHED|LISTEN)'Méthode 2 : Modules Python chargés
Section intitulée « Méthode 2 : Modules Python chargés »Pour les applications Python, listez les modules effectivement importés :
import sysimport json
def get_loaded_modules(): """Retourne les modules Python chargés avec leur chemin.""" modules = {} for name, module in sys.modules.items(): if hasattr(module, '__file__') and module.__file__: modules[name] = { "path": module.__file__, "version": getattr(module, '__version__', None) } return modules
if __name__ == "__main__": print(json.dumps(get_loaded_modules(), indent=2))Exemple d’exécution via kubectl exec :
# Via un shell dans le conteneurkubectl exec -it myapp-pod -- python -c "import sys, jsonmods = {k: getattr(v, '__file__', 'builtin') for k, v in sys.modules.items()}print(json.dumps(mods, indent=2))"Méthode 3 : Packages Node.js chargés
Section intitulée « Méthode 3 : Packages Node.js chargés »Pour les applications Node.js :
// Ajouter en fin de votre app ou via un endpoint debug
function getLoadedModules() { const modules = {}; for (const [path, mod] of Object.entries(require.cache)) { if (path.includes('node_modules')) { const parts = path.split('node_modules/'); const pkgPath = parts[parts.length - 1].split('/')[0]; if (!modules[pkgPath]) { try { const pkg = require(`${pkgPath}/package.json`); modules[pkgPath] = pkg.version; } catch { modules[pkgPath] = 'unknown'; } } } } return modules;}
console.log(JSON.stringify(getLoadedModules(), null, 2));Méthode 4 : Variables d’environnement actives
Section intitulée « Méthode 4 : Variables d’environnement actives »Les variables d’environnement définissent souvent le comportement runtime :
# Lister les variables d'environnement d'un processuscat /proc/$(pgrep -x python3)/environ | tr '\0' '\n' | grep -E '^(DB_|API_|FEATURE_|DEBUG|LOG_LEVEL)'
# Dans un conteneur Kuberneteskubectl exec myapp-pod -- env | grep -E '^(DB_|API_|FEATURE_)'Collecter les informations dans les conteneurs
Section intitulée « Collecter les informations dans les conteneurs »Scanner un conteneur en cours d’exécution
Section intitulée « Scanner un conteneur en cours d’exécution »# Lister les processus dans un conteneurdocker exec mycontainer ps aux
# Librairies chargées par le processus principal (PID 1 dans le conteneur)docker exec mycontainer cat /proc/1/maps | grep '\.so' | awk '{print $6}' | sort -u
# Connexions réseau activesdocker exec mycontainer netstat -tuln
# Packages installés (Debian/Ubuntu)docker exec mycontainer dpkg-query -W -f='${Package} ${Version}\n'
# Packages installés (Alpine)docker exec mycontainer apk list --installedScanner le filesystem d’un conteneur en cours d’exécution
Section intitulée « Scanner le filesystem d’un conteneur en cours d’exécution »Syft peut scanner une image Docker, mais aussi le filesystem exporté d’un conteneur :
# Scanner l'image (SBOM classique)syft myregistry/myapp:v1.0.0 -o cyclonedx-json > sbom.json
# Scanner le rootfs d'un conteneur en cours d'exécutionCONTAINER_ID=$(docker ps -qf "name=myapp")docker export $CONTAINER_ID | syft /dev/stdin -o cyclonedx-json > runtime-sbom.jsonScript de collecte OBOM complète
Section intitulée « Script de collecte OBOM complète »#!/bin/bash# Prototype de collecte partielle pour un conteneur# Utile pour démonstration ou expérimentation, insuffisant comme OBOM de production
CONTAINER=$1OUTPUT=${2:-obom.json}
if [ -z "$CONTAINER" ]; then echo "Usage: $0 <container_name_or_id> [output.json]" exit 1fi
echo "Collecting OBOM for container: $CONTAINER"
# Créer le JSON de sortiecat > "$OUTPUT" << EOF{ "bomFormat": "CycloneDX", "specVersion": "1.6", "version": 1, "metadata": { "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "component": { "name": "$CONTAINER", "type": "container" }, "properties": [ {"name": "obom:collection-method", "value": "runtime-inspection"} ] }, "components": [EOF
# Collecter les packages systèmeecho " Collecting system packages..."docker exec "$CONTAINER" sh -c 'if command -v dpkg-query >/dev/null; then dpkg-query -W -f "${Package}\t${Version}\n" 2>/dev/nullelif command -v apk >/dev/null; then apk list --installed 2>/dev/null | sed "s/-\([0-9]\)/ \1/"elif command -v rpm >/dev/null; then rpm -qa --queryformat "%{NAME}\t%{VERSION}\n" 2>/dev/nullfi' | while IFS=$'\t' read -r name version; do echo " {\"type\": \"library\", \"name\": \"$name\", \"version\": \"$version\", \"properties\": [{\"name\": \"source\", \"value\": \"os-package\"}]},"done >> "$OUTPUT"
# Collecter les librairies chargées du processus principal# Note : PID 1 n'est pas toujours le processus applicatif (peut être init/wrapper)echo " Collecting loaded libraries..."docker exec "$CONTAINER" cat /proc/1/maps 2>/dev/null | \ grep -E '\.so(\.[0-9]+)*$' | \ awk '{print $6}' | \ sort -u | \ while read lib; do name=$(basename "$lib" | sed 's/\.so.*//') echo " {\"type\": \"library\", \"name\": \"$name\", \"properties\": [{\"name\": \"source\", \"value\": \"loaded-library\"}, {\"name\": \"path\", \"value\": \"$lib\"}]}," done >> "$OUTPUT"
# Fermer le JSON (enlever la dernière virgule et fermer)sed -i '$ s/,$//' "$OUTPUT"echo " ]" >> "$OUTPUT"echo "}" >> "$OUTPUT"
echo "OBOM written to: $OUTPUT"Générer un OBOM avec cdxgen
Section intitulée « Générer un OBOM avec cdxgen »cdxgen est un générateur CycloneDX lié à l’écosystème OWASP/CycloneDX. Il propose des capacités utiles pour aller vers l’OBOM, notamment pour certaines images Linux et VMs via plugins, mais le support reste à valider selon le contexte.
Installation
Section intitulée « Installation »npm install -g @cyclonedx/cdxgenGénérer un OBOM
Section intitulée « Générer un OBOM »# Pour un projet Node.js avec détection des dépendances runtimecdxgen -t nodejs -o obom.json --deep
# Pour une image Docker avec analyse approfondiecdxgen -t docker -o obom.json my-image:latest --deep
# Inclure les informations d'environnementcdxgen -t nodejs -o obom.json --include-envKubernetes : collecter l’OBOM de tous les pods
Section intitulée « Kubernetes : collecter l’OBOM de tous les pods »Script de collecte à l’échelle du cluster
Section intitulée « Script de collecte à l’échelle du cluster »#!/bin/bash# Collecte l'inventaire runtime de tous les pods
NAMESPACE=${1:-default}OUTPUT_DIR=${2:-./obom-collection}
mkdir -p "$OUTPUT_DIR"
echo "Collecting OBOM for namespace: $NAMESPACE"
# Lister tous les podskubectl get pods -n "$NAMESPACE" -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[*].name}{"\n"}{end}' | \while IFS=$'\t' read -r pod containers; do for container in $containers; do echo "Processing: $pod / $container"
# Collecter les packages (si possible) kubectl exec -n "$NAMESPACE" "$pod" -c "$container" -- sh -c ' if command -v dpkg-query >/dev/null; then dpkg-query -W -f "${Package}\t${Version}\n" elif command -v apk >/dev/null; then apk list --installed 2>/dev/null fi ' 2>/dev/null > "$OUTPUT_DIR/${pod}_${container}_packages.txt"
# Collecter les librairies chargées kubectl exec -n "$NAMESPACE" "$pod" -c "$container" -- \ cat /proc/1/maps 2>/dev/null | \ grep -E '\.so' | awk '{print $6}' | sort -u \ > "$OUTPUT_DIR/${pod}_${container}_libs.txt"
# Collecter les connexions réseau kubectl exec -n "$NAMESPACE" "$pod" -c "$container" -- \ netstat -tuln 2>/dev/null \ > "$OUTPUT_DIR/${pod}_${container}_network.txt" donedone
echo "Collection complete. Files in: $OUTPUT_DIR"Avec des outils spécialisés
Section intitulée « Avec des outils spécialisés »Ces outils ont des rôles différents — ne les confondez pas :
| Outil | Catégorie | Fonction |
|---|---|---|
| Trivy | Scan de composition | Analyse image/filesystem/k8s, pas inventaire runtime fin |
| Syft | Scan de composition | SBOM depuis images/filesystems |
| Sysdig | Télémétrie runtime | Observation runtime via agent eBPF |
| Falco | Détection comportementale | Alertes sur comportements anormaux, pas OBOM |
| KSOC | Visibilité Kubernetes | Runtime visibility via agent |
Corréler SBOM et OBOM
Section intitulée « Corréler SBOM et OBOM »Workflow de corrélation
Section intitulée « Workflow de corrélation »Script de comparaison
Section intitulée « Script de comparaison »#!/usr/bin/env python3"""compare-sbom-obom.py - Compare SBOM et OBOM pour détecter les écarts."""
import jsonimport sysfrom collections import defaultdict
def load_bom(path): """Charge un BOM CycloneDX et extrait les composants.""" with open(path) as f: bom = json.load(f)
components = {} for comp in bom.get("components", []): name = comp.get("name", "") version = comp.get("version", "unknown") components[name] = version return components
def compare(sbom_path, obom_path): """Compare SBOM et OBOM, retourne les écarts.""" sbom = load_bom(sbom_path) obom = load_bom(obom_path)
sbom_names = set(sbom.keys()) obom_names = set(obom.keys())
results = { "sbom_only": list(sbom_names - obom_names), # Déclaré, non chargé "obom_only": list(obom_names - sbom_names), # Chargé, non déclaré "both": list(sbom_names & obom_names), # Match "version_mismatch": [] # Versions différentes }
# Vérifier les versions for name in results["both"]: if sbom[name] != obom[name]: results["version_mismatch"].append({ "name": name, "sbom_version": sbom[name], "obom_version": obom[name] })
return results
if __name__ == "__main__": if len(sys.argv) != 3: print("Usage: compare-sbom-obom.py sbom.json obom.json") sys.exit(1)
results = compare(sys.argv[1], sys.argv[2])
print("=== Composants SBOM-only (déclarés mais non chargés) ===") for name in results["sbom_only"][:10]: print(f" - {name}")
print("\n=== Composants OBOM-only (chargés mais non déclarés) ===") for name in results["obom_only"][:10]: print(f" ⚠️ {name}")
print(f"\n=== Résumé ===") print(f"Match: {len(results['both'])}") print(f"SBOM-only: {len(results['sbom_only'])}") print(f"OBOM-only: {len(results['obom_only'])} ← À investiguer") print(f"Version mismatch: {len(results['version_mismatch'])}")Usage :
python compare-sbom-obom.py sbom.json obom.jsonIntégrer avec GUAC
Section intitulée « Intégrer avec GUAC »GUAC (Graph for Understanding Artifact Composition) peut ingérer des documents CycloneDX et SPDX ainsi que d’autres métadonnées supply chain, ce qui en fait un bon candidat pour corréler SBOM, attestations, vulnérabilités et, potentiellement, des inventaires opérationnels modélisés de manière compatible. La profondeur réelle de cette corrélation dépend toutefois de la qualité et de la structure des documents ingérés.
Ingérer SBOM et OBOM dans GUAC
Section intitulée « Ingérer SBOM et OBOM dans GUAC »# Ingérer le SBOM (build-time)guacone collect files sbom.json
# Ingérer l'OBOM (runtime)guacone collect files obom.json
# Maintenant GUAC peut répondre à des requêtes comme :# "Quels composants sont en production mais pas dans le SBOM ?"# "Ce conteneur utilise-t-il une lib vulnérable à cette CVE ?"Bonnes pratiques OBOM
Section intitulée « Bonnes pratiques OBOM »Fréquence de collecte
Section intitulée « Fréquence de collecte »| Environnement | Fréquence | Raison |
|---|---|---|
| Production | Quotidien ou après chaque déploiement | Détection rapide des dérives |
| Staging | À chaque déploiement | Validation avant prod |
| Dev/Test | Optionnel | Moins critique |
Ce qu’il faut collecter (et ne pas collecter)
Section intitulée « Ce qu’il faut collecter (et ne pas collecter) »| ✅ Collecter | ❌ Ne pas collecter |
|---|---|
| Noms des packages | Valeurs des secrets |
| Versions | Données utilisateur |
| Chemins des librairies | Contenu des fichiers de config sensibles |
| Noms des variables d’env | Valeurs des variables d’env secrètes |
| Connexions réseau (hosts) | Credentials dans les connexions |
Automatisation recommandée
Section intitulée « Automatisation recommandée »-
À chaque déploiement
Générez un OBOM snapshot après le démarrage de l’application :
# Kubernetes Job post-deployapiVersion: batch/v1kind: Jobmetadata:name: collect-obomspec:template:spec:containers:- name: collectorimage: obom-collector:latestcommand: ["./collect-obom.sh", "myapp-pod"] -
Quotidiennement
Collecte planifiée pour détecter les dérives (plugins activés, libs mises à jour) :
apiVersion: batch/v1kind: CronJobmetadata:name: daily-obomspec:schedule: "0 2 * * *" # 2h du matinjobTemplate:# ... -
Sur incident de sécurité
Collecte immédiate pour répondre à “est-ce qu’on utilise cette lib vulnérable ?”
Ce qu’un OBOM ne résout pas
Section intitulée « Ce qu’un OBOM ne résout pas »L’OBOM est un complément précieux, mais il ne faut pas remplacer un mythe (“le SBOM suffit”) par un autre (“l’OBOM règle le runtime”).
Ce que l’OBOM ne garantit pas
Section intitulée « Ce que l’OBOM ne garantit pas »| Limitation | Explication |
|---|---|
| Intégrité de la chaîne de build | L’OBOM observe, il ne prouve pas l’origine |
| Absence de dérive future | Snapshot à un instant T, pas surveillance continue |
| Couverture complète | Dépend de la méthode de collecte et des privilèges |
| Exploitabilité d’une CVE | Présence ≠ vulnérabilité exploitable (c’est le rôle du VEX) |
| Conformité à lui seul | Un élément parmi d’autres dans une posture de sécurité |
| Détection automatique de tout | Plugins/comportements très dynamiques peuvent échapper |
L’OBOM complète, il ne remplace pas
Section intitulée « L’OBOM complète, il ne remplace pas »L’OBOM s’intègre dans un ensemble :
- SBOM : ce qui a été assemblé
- Attestations : preuves de provenance et d’intégrité
- VEX : exploitabilité des vulnérabilités
- Télémétrie runtime : observation continue
- OBOM : état opérationnel observé à un instant T
Niveaux de maturité
Section intitulée « Niveaux de maturité »Situez-vous sur cette échelle pour planifier votre progression :
Niveau 1 — Fondations
Section intitulée « Niveau 1 — Fondations »- SBOM d’image finale
- Scan de vulnérabilités
- Inventaire packages OS
Niveau 2 — Observation partielle
Section intitulée « Niveau 2 — Observation partielle »- Snapshot runtime (libs chargées, variables d’env)
- Comparaison SBOM / snapshot manual
- Alertes sur écarts majeurs
Niveau 3 — Corrélation
Section intitulée « Niveau 3 — Corrélation »- Introspection langage (Python/Node/Java)
- Corrélation multi-source automatisée
- Enrichissement vulnérabilités
- Stockage historisé
Niveau 4 — Décision pilotée
Section intitulée « Niveau 4 — Décision pilotée »- OBOM structuré et normalisé
- Corrélation continue (GUAC ou équivalent)
- Knowledge graph interrogeable
- Décision de risque intégrée au workflow
À retenir
Section intitulée « À retenir »Les 5 points essentiels de ce guide :
- SBOM et OBOM sont complémentaires — le SBOM décrit ce qui a été assemblé, l’OBOM observe l’état opérationnel réel
- Plusieurs méthodes de collecte :
/proc/<pid>/maps, introspection langage, scan de filesystem — chacune avec ses limites - La corrélation SBOM/OBOM révèle les écarts — composants “fantômes” absents du SBOM mais actifs en production
- L’OBOM n’est pas magique — c’est un snapshot partiel, pas une surveillance continue ni une preuve d’intégrité
- Automatisez la collecte — après chaque déploiement et quotidiennement, sans jamais collecter les secrets
Prochaines étapes
Section intitulée « Prochaines étapes »XBOM : au-delà du SBOM
Comprenez l’écosystème complet des BOM (SBOM, OBOM, CBOM, etc.) Lire le guide
GUAC
Corrélez SBOM, OBOM et vulnérabilités dans un graphe de connaissances Lire le guide GUAC
Syft
Générez des SBOM au build pour compléter vos OBOM Lire le guide Syft
VEX
Documentez l’exploitabilité des vulnérabilités détectées Lire le guide VEX