Aller au contenu
Sécurité medium
🔐 Alerte sécurité — Incident supply chain Trivy : lire mon analyse de l'attaque

OBOM : inventorier ce qui tourne vraiment en production

28 min de lecture

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
AspectSBOM (build-time)OBOM (runtime)
MomentÀ la compilation/packagingEn production
SourceManifestes (package.json, go.mod, etc.)Processus en cours d’exécution
DépendancesDéclarées et transitivesEffectivement chargées
ConfigurationValeurs par défautConfiguration active
Modules optionnelsListés comme optionnelsActivés ou non
PluginsPotentielsRéellement chargés
# app.py - Application avec chargement dynamique
import os
import importlib
# Le driver de base de données dépend de l'environnement
db_driver = os.getenv("DB_DRIVER", "sqlite")
db_module = importlib.import_module(f"databases.{db_driver}")
# Les plugins sont chargés depuis un dossier
for 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.0
databases-sqlite==0.9.0
databases-postgres==0.9.0 # dépendance extras
plugin-analytics==1.2.0 # plugin disponible
plugin-cache==2.0.0 # plugin disponible

Note : 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.0
databases-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 postgres
libpq.so.5 # ← Librairie C chargée par psycopg2

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

Les 5 catégories d'informations capturées par un OBOM : librairies, modules, configuration, services, système
CatégorieExemplesPourquoi c’est important
Librairies chargéeslibssl.so.3, libc.so.6, libpq.so.5CVE dans les libs C/système
Modules activésPlugins WordPress, extensions PythonCode actif non visible au build
Configuration activeTLS_MIN_VERSION=1.2, DEBUG=falseParamètres de sécurité effectifs
Services connectéspostgres://db:5432, redis://cache:6379Dépendances réseau
État systèmeKernel 5.15, glibc 2.35Vulnérabilités système

La méthode la plus directe pour lister les librairies chargées par un processus :

Fenêtre de terminal
# Lister les librairies chargées par un processus
cat /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
collect-runtime-libs.sh
#!/bin/bash
# Collecte les librairies chargées pour un processus
PID=$1
if [ -z "$PID" ]; then
echo "Usage: $0 <PID>"
exit 1
fi
echo "=== Runtime libraries for PID $PID ==="
echo ""
# Nom du processus
PROC_NAME=$(cat /proc/$PID/comm)
echo "Process: $PROC_NAME"
echo ""
# Librairies .so uniques
echo "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

Pour les applications Python, listez les modules effectivement importés :

runtime_modules.py
import sys
import 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 :

Fenêtre de terminal
# Via un shell dans le conteneur
kubectl exec -it myapp-pod -- python -c "
import sys, json
mods = {k: getattr(v, '__file__', 'builtin') for k, v in sys.modules.items()}
print(json.dumps(mods, indent=2))
"

Pour les applications Node.js :

runtime-deps.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));

Les variables d’environnement définissent souvent le comportement runtime :

Fenêtre de terminal
# Lister les variables d'environnement d'un processus
cat /proc/$(pgrep -x python3)/environ | tr '\0' '\n' | grep -E '^(DB_|API_|FEATURE_|DEBUG|LOG_LEVEL)'
# Dans un conteneur Kubernetes
kubectl exec myapp-pod -- env | grep -E '^(DB_|API_|FEATURE_)'
Fenêtre de terminal
# Lister les processus dans un conteneur
docker 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 actives
docker 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 --installed

Scanner 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 :

Fenêtre de terminal
# 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écution
CONTAINER_ID=$(docker ps -qf "name=myapp")
docker export $CONTAINER_ID | syft /dev/stdin -o cyclonedx-json > runtime-sbom.json
collect-obom.sh
#!/bin/bash
# Prototype de collecte partielle pour un conteneur
# Utile pour démonstration ou expérimentation, insuffisant comme OBOM de production
CONTAINER=$1
OUTPUT=${2:-obom.json}
if [ -z "$CONTAINER" ]; then
echo "Usage: $0 <container_name_or_id> [output.json]"
exit 1
fi
echo "Collecting OBOM for container: $CONTAINER"
# Créer le JSON de sortie
cat > "$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ème
echo " 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/null
elif 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/null
fi
' | 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"

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.

Fenêtre de terminal
npm install -g @cyclonedx/cdxgen
Fenêtre de terminal
# Pour un projet Node.js avec détection des dépendances runtime
cdxgen -t nodejs -o obom.json --deep
# Pour une image Docker avec analyse approfondie
cdxgen -t docker -o obom.json my-image:latest --deep
# Inclure les informations d'environnement
cdxgen -t nodejs -o obom.json --include-env
collect-cluster-obom.sh
#!/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 pods
kubectl 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"
done
done
echo "Collection complete. Files in: $OUTPUT_DIR"

Ces outils ont des rôles différents — ne les confondez pas :

OutilCatégorieFonction
TrivyScan de compositionAnalyse image/filesystem/k8s, pas inventaire runtime fin
SyftScan de compositionSBOM depuis images/filesystems
SysdigTélémétrie runtimeObservation runtime via agent eBPF
FalcoDétection comportementaleAlertes sur comportements anormaux, pas OBOM
KSOCVisibilité KubernetesRuntime visibility via agent

Workflow de corrélation SBOM et OBOM : comparaison build time vs runtime pour identifier les composants fantômes

#!/usr/bin/env python3
"""compare-sbom-obom.py - Compare SBOM et OBOM pour détecter les écarts."""
import json
import sys
from 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 :

Fenêtre de terminal
python compare-sbom-obom.py sbom.json obom.json

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.

Fenêtre de terminal
# 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 ?"
EnvironnementFréquenceRaison
ProductionQuotidien ou après chaque déploiementDétection rapide des dérives
StagingÀ chaque déploiementValidation avant prod
Dev/TestOptionnelMoins critique
✅ Collecter❌ Ne pas collecter
Noms des packagesValeurs des secrets
VersionsDonnées utilisateur
Chemins des librairiesContenu des fichiers de config sensibles
Noms des variables d’envValeurs des variables d’env secrètes
Connexions réseau (hosts)Credentials dans les connexions
  1. À chaque déploiement

    Générez un OBOM snapshot après le démarrage de l’application :

    # Kubernetes Job post-deploy
    apiVersion: batch/v1
    kind: Job
    metadata:
    name: collect-obom
    spec:
    template:
    spec:
    containers:
    - name: collector
    image: obom-collector:latest
    command: ["./collect-obom.sh", "myapp-pod"]
  2. Quotidiennement

    Collecte planifiée pour détecter les dérives (plugins activés, libs mises à jour) :

    apiVersion: batch/v1
    kind: CronJob
    metadata:
    name: daily-obom
    spec:
    schedule: "0 2 * * *" # 2h du matin
    jobTemplate:
    # ...
  3. Sur incident de sécurité

    Collecte immédiate pour répondre à “est-ce qu’on utilise cette lib vulnérable ?”

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”).

LimitationExplication
Intégrité de la chaîne de buildL’OBOM observe, il ne prouve pas l’origine
Absence de dérive futureSnapshot à un instant T, pas surveillance continue
Couverture complèteDépend de la méthode de collecte et des privilèges
Exploitabilité d’une CVEPrésence ≠ vulnérabilité exploitable (c’est le rôle du VEX)
Conformité à lui seulUn élément parmi d’autres dans une posture de sécurité
Détection automatique de toutPlugins/comportements très dynamiques peuvent échapper

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

Situez-vous sur cette échelle pour planifier votre progression :

  • SBOM d’image finale
  • Scan de vulnérabilités
  • Inventaire packages OS
  • Snapshot runtime (libs chargées, variables d’env)
  • Comparaison SBOM / snapshot manual
  • Alertes sur écarts majeurs
  • Introspection langage (Python/Node/Java)
  • Corrélation multi-source automatisée
  • Enrichissement vulnérabilités
  • Stockage historisé
  • OBOM structuré et normalisé
  • Corrélation continue (GUAC ou équivalent)
  • Knowledge graph interrogeable
  • Décision de risque intégrée au workflow

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

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

VEX

Documentez l’exploitabilité des vulnérabilités détectées Lire le guide VEX

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn