16. Supply chain sécurité et SBOM#
La supply chain logicielle est devenue l’un des vecteurs d’attaque les plus redoutables. Des attaques comme SolarWinds ou XZ Utils ont démontré qu’un seul composant compromis peut affecter des milliers d’organisations. Ce chapitre approfondit la compréhension de ces menaces et les mécanismes de défense : SBOM, signatures d’artefacts, SLSA et politiques de dépendances.
Attaques supply chain : anatomie des incidents majeurs#
SolarWinds (2020)#
Mécanisme : les attaquants (APT29/Cozy Bear) ont compromis le pipeline de build de SolarWinds Orion en injectant le malware SUNBURST directement dans le code source avant compilation. Les binaires signés légitimement ont été distribués à ~18 000 organisations via les mises à jour officielles.
Leçons : la signature des artefacts ne suffit pas si le processus de build est compromis. La provenance du build (SLSA) et les builds hermétiques sont essentiels.
XZ Utils (2024)#
Mécanisme : un contributeur ayant passé deux ans à construire sa légitimité dans le projet open source liblzma (XZ Utils) a injecré une backdoor dans les fichiers de configuration autoconf, activée uniquement dans les packages de distribution (Debian, Red Hat). La backdoor ciblait sshd via le chargement dynamique de la bibliothèque.
Leçons : les attaques sur la chaîne de contribution humaine (social engineering de mainteneurs) sont difficiles à détecter. Les SBOM et la reproductibilité des builds permettent la vérification post-incident.
Typosquatting PyPI/npm#
Des paquets malveillants imitent des paquets légitimes avec des noms proches (requests vs request, urllib3 vs urlib3, lodash vs loadash). Ils exfiltrent des credentials ou installent des backdoors au moment du pip install / npm install.
Dependency Confusion#
Décrit par Alex Birsan (2021) : si un registre public contient un paquet avec le même nom qu’un paquet interne privé, les gestionnaires de paquets (pip, npm, gem) peuvent télécharger la version publique malveillante plutôt que la version interne légitime.
Remédiation : scoping des paquets (namespaces @company/package), configuration explicite des registres, vérification des hash.
Statistiques sur les attaques supply chain
Selon Sonatype (2023), les attaques ciblant les dépôts open source ont augmenté de 633 % sur 3 ans. Plus de 245 000 paquets malveillants ont été détectés sur PyPI, npm et Maven Central. La majorité exploite le typosquatting ou la dependency confusion.
SBOM : Software Bill of Materials#
Un SBOM est un inventaire exhaustif des composants d’un logiciel, analogues à une liste d’ingrédients. Il documente les bibliothèques, leurs versions, leurs licences et leurs relations de dépendances.
Formats standards#
SPDX 2.3 (Software Package Data Exchange — Linux Foundation) : format mature, adopté par la US Executive Order on Cybersecurity (2021), supporté par GitHub, Trivy, Syft.
{
"SPDXID": "SPDXRef-DOCUMENT",
"spdxVersion": "SPDX-2.3",
"creationInfo": {
"created": "2024-03-15T10:00:00Z",
"creators": ["Tool: syft-1.0.0", "Organization: MyOrg"]
},
"name": "myapp-v1.2.0",
"packages": [
{
"SPDXID": "SPDXRef-requests-2.31.0",
"name": "requests",
"versionInfo": "2.31.0",
"supplier": "Organization: Python Requests Team",
"downloadLocation": "https://pypi.org/project/requests/2.31.0/",
"filesAnalyzed": false,
"externalRefs": [
{
"referenceCategory": "SECURITY",
"referenceType": "cpe23Type",
"referenceLocator": "cpe:2.3:a:python:requests:2.31.0:*:*:*:*:*:*:*"
},
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:pypi/requests@2.31.0"
}
],
"licenseConcluded": "Apache-2.0"
}
]
}
CycloneDX 1.5 (OWASP) : format orienté sécurité, supporte les vulnérabilités, les services, les formules de build et les attestations.
Cas d’usage du SBOM#
Cas d’usage |
Description |
|---|---|
Réponse aux incidents |
Identifier immédiatement tous les composants affectés par une CVE (ex. Log4Shell) |
Conformité |
Vérifier les licences (GPL contamination, licences incompatibles) |
Audit réglementaire |
DORA, NIS2, US EO 14028 exigent des SBOM pour les logiciels critiques |
Gestion des risques |
Cartographier les composants EOL (End of Life) sans support de sécurité |
Signature d’artefacts : Sigstore, Cosign et in-toto#
Sigstore : l’infrastructure de signature open source#
Sigstore est une initiative (Linux Foundation, Google, Red Hat, Purdue University) qui simplifie la signature cryptographique des artefacts logiciels :
Cosign : signe et vérifie les images de conteneurs, les blobs et les attestations.
Fulcio : autorité de certification qui délivre des certificats de code-signing éphémères liés à une identité OIDC.
Rekor : journal de transparence immuable (similaire à Certificate Transparency) où toutes les signatures sont enregistrées.
Signature keyless avec Cosign#
# En CI (GitHub Actions) — signature automatique via l'identité OIDC du workflow
cosign sign --yes \
ghcr.io/myorg/myapp:v1.2.0@sha256:<digest>
# Vérification en production
cosign verify \
--certificate-identity="https://github.com/myorg/myapp/.github/workflows/release.yml@refs/tags/v1.2.0" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/myorg/myapp:v1.2.0@sha256:<digest>
Attestations in-toto#
in-toto est un framework qui définit une chaîne de custody (layout) : chaque étape du pipeline produit une attestation signée attestant ce qui a été produit et par qui.
{
"_type": "https://in-toto.io/Statement/v0.1",
"predicateType": "https://slsa.dev/provenance/v0.2",
"subject": [{
"name": "ghcr.io/myorg/myapp",
"digest": {"sha256": "abc123..."}
}],
"predicate": {
"builder": {"id": "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_go_slsa3.yml@v1.9.0"},
"buildType": "https://slsa.dev/provenance/v0.2",
"invocation": {
"configSource": {
"uri": "git+https://github.com/myorg/myapp@refs/tags/v1.2.0",
"digest": {"sha1": "def456..."},
"entryPoint": ".github/workflows/release.yml"
}
}
}
}
SLSA : niveaux de maturité de la supply chain#
SLSA (Supply-chain Levels for Software Artifacts) définit un cadre progressif de sécurisation du build :
Niveau |
Exigences |
Protège contre |
|---|---|---|
L1 |
Provenance documentée |
Erreurs accidentelles, manque de traçabilité |
L2 |
Build service authentifié + provenance signée |
Accès non autorisé au build service |
L3 |
Build hermétique + provenance non falsifiable |
Compromission du build service lui-même |
Workflow GitHub Actions SLSA L3#
name: Release SLSA L3
on:
push:
tags: ["v*"]
permissions:
id-token: write
contents: read
packages: write
attestations: write
jobs:
build:
uses: slsa-framework/slsa-github-generator/.github/workflows/builder_container_slsa3.yml@v1.9.0
with:
image: ghcr.io/${{ github.repository }}
digest: ${{ needs.build-image.outputs.digest }}
secrets:
registry-username: ${{ github.actor }}
registry-password: ${{ secrets.GITHUB_TOKEN }}
Audit des dépendances#
pip-audit (Python)#
# Auditer l'environnement courant
pip-audit
# Auditer un fichier requirements
pip-audit -r requirements.txt
# Sortie JSON
pip-audit --format json -r requirements.txt > audit.json
# Générer un SBOM CycloneDX
pip-audit --format cyclonedx-json > sbom.json
npm audit et cargo audit#
# Node.js
npm audit --audit-level high
npm audit fix # Mise à jour automatique des patchs
# Rust
cargo audit
cargo audit fix # Mise à jour automatique
Dependabot et Renovate#
Ces outils automatisent les mises à jour de dépendances via des pull requests :
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
groups:
security-updates:
applies-to: security-updates
patterns: ["*"]
Politique de mise à jour des dépendances
Adopter une politique de mise à jour explicite : patches de sécurité automatiques (Dependabot auto-merge), mises à jour mineures hebdomadaires en PR, mises à jour majeures trimestrielles avec revue manuelle. L’absence de politique génère une dette de sécurité croissante.
Chaîne de confiance bout en bout#
La chaîne de confiance idéale (SLSA L3) couvre chaque étape :
Commit signé (GPG)
→ Build hermétique (environnement isolé, inputs déclarés)
→ Provenance générée et signée (Cosign + Fulcio)
→ SBOM généré et signé
→ Image signée (Cosign)
→ Déploiement avec vérification (Kyverno/OPA)
Réponse à une compromission supply chain#
Quand une CVE affecte un composant populaire (ex. Log4Shell — log4j 2.x) :
Identification : interroger tous les SBOM de tous les artefacts déployés pour identifier ceux contenant le composant vulnérable.
Priorisation : les services exposés à Internet avec le composant vulnérable sont prioritaires.
Mitigation : patch, mise à jour ou blocage des vecteurs d’exploitation (WAF rules).
Vérification : rescan post-patch avec Trivy/Grype, mise à jour des SBOM.
Rétrospective : analyser pourquoi la vulnérabilité n’a pas été détectée plus tôt (absence de scan CI ?).
Visualisations#
Propagation de CVE dans un graphe de dépendances transitives#
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.0)
# Graphe de dépendances : paquet → liste de dépendances directes
dependances = {
"requests 2.28.0\n(CVE CRITIQUE)": ["urllib3 1.26.x", "certifi 2023.x"],
"urllib3 1.26.x": ["six 1.16.x"],
"certifi 2023.x": [],
"six 1.16.x": [],
"django 4.2.x": ["requests 2.28.0\n(CVE CRITIQUE)", "sqlparse 0.4.x"],
"sqlparse 0.4.x": [],
"fastapi 0.110.x": ["requests 2.28.0\n(CVE CRITIQUE)", "pydantic 2.x", "starlette 0.36.x"],
"pydantic 2.x": [],
"starlette 0.36.x": ["requests 2.28.0\n(CVE CRITIQUE)"],
"App A\n(django)": ["django 4.2.x"],
"App B\n(fastapi)": ["fastapi 0.110.x"],
"App C\n(custom)": ["sqlparse 0.4.x", "six 1.16.x"],
"App D\n(flask)": ["requests 2.28.0\n(CVE CRITIQUE)", "Werkzeug 3.x"],
"Werkzeug 3.x": [],
}
paquet_compromis = "requests 2.28.0\n(CVE CRITIQUE)"
# BFS pour trouver tous les paquets transitivement vulnérables
def find_affected(deps: dict, compromis: str) -> set:
affected = {compromis}
changed = True
while changed:
changed = False
for pkg, dep_list in deps.items():
if pkg not in affected:
if any(d in affected for d in dep_list):
affected.add(pkg)
changed = True
return affected
affected = find_affected(dependances, paquet_compromis)
G = nx.DiGraph()
for pkg, deps in dependances.items():
G.add_node(pkg)
for d in deps:
G.add_edge(pkg, d)
pos = {
"requests 2.28.0\n(CVE CRITIQUE)": (4, 4),
"urllib3 1.26.x": (2.5, 2.8),
"certifi 2023.x": (5.5, 2.8),
"six 1.16.x": (2.5, 1.5),
"django 4.2.x": (1.5, 5.5),
"fastapi 0.110.x": (4, 6.5),
"starlette 0.36.x": (6.5, 5.5),
"sqlparse 0.4.x": (0, 4),
"pydantic 2.x": (5.5, 7.5),
"Werkzeug 3.x": (7, 3.5),
"App A\n(django)": (0, 7),
"App B\n(fastapi)": (3.5, 8.5),
"App C\n(custom)": (-1, 2.5),
"App D\n(flask)": (7.5, 5.5),
}
node_colors = []
node_sizes = []
for node in G.nodes():
if node == paquet_compromis:
node_colors.append("#e74c3c")
node_sizes.append(3000)
elif node in affected:
if node.startswith("App"):
node_colors.append("#c0392b")
node_sizes.append(2500)
else:
node_colors.append("#e67e22")
node_sizes.append(1800)
else:
node_colors.append("#2ecc71")
node_sizes.append(1800)
fig, ax = plt.subplots(figsize=(14, 10))
ax.set_facecolor("#f8f9fa")
nx.draw_networkx_nodes(G, pos, node_color=node_colors, node_size=node_sizes,
ax=ax, alpha=0.88)
nx.draw_networkx_labels(G, pos, font_size=7.5, font_color="white",
font_weight="bold", ax=ax)
edge_colors = []
for u, v in G.edges():
if u in affected and v in affected:
edge_colors.append("#e74c3c")
else:
edge_colors.append("#95a5a6")
nx.draw_networkx_edges(G, pos, edge_color=edge_colors, arrows=True,
arrowsize=18, width=1.8, ax=ax,
connectionstyle="arc3,rad=0.05")
legend_patches = [
mpatches.Patch(color="#e74c3c", label=f"Paquet compromis (CVE)"),
mpatches.Patch(color="#e67e22", label="Dépendance transitivement vulnérable"),
mpatches.Patch(color="#c0392b", label="Application impactée"),
mpatches.Patch(color="#2ecc71", label="Composant non affecté"),
]
ax.legend(handles=legend_patches, loc="lower left", fontsize=9)
apps_affected = [n for n in affected if n.startswith("App")]
ax.set_title(f"Propagation CVE via dépendances transitives\n{len(affected)} composants affectés dont {len(apps_affected)} applications",
fontsize=13, pad=15)
ax.axis("off")
plt.savefig("cve_propagation_graph.png", dpi=100, bbox_inches="tight")
plt.show()
print(f"\nComposants affectés ({len(affected)}) :")
for p in sorted(affected):
prefix = " [APPLICATION]" if p.startswith("App") else " [DEPENDANCE] "
print(f"{prefix} {p.replace(chr(10), ' ')}")
Composants affectés (7) :
[APPLICATION] App A (django)
[APPLICATION] App B (fastapi)
[APPLICATION] App D (flask)
[DEPENDANCE] django 4.2.x
[DEPENDANCE] fastapi 0.110.x
[DEPENDANCE] requests 2.28.0 (CVE CRITIQUE)
[DEPENDANCE] starlette 0.36.x
Diff de deux SBOM — détection de changements#
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
import json
# SBOM synthétique v1.0 (avant mise à jour)
sbom_v1 = {
"name": "myapp",
"version": "1.0.0",
"packages": [
{"name": "requests", "version": "2.28.0", "license": "Apache-2.0"},
{"name": "urllib3", "version": "1.26.14", "license": "MIT"},
{"name": "certifi", "version": "2022.12.7","license": "MPL-2.0"},
{"name": "django", "version": "4.1.7", "license": "BSD-3-Clause"},
{"name": "sqlparse", "version": "0.4.3", "license": "BSD-3-Clause"},
{"name": "cryptography","version": "39.0.2", "license": "Apache-2.0"},
{"name": "pillow", "version": "9.4.0", "license": "HPND"},
]
}
# SBOM synthétique v1.1 (après mise à jour — inclut nouveaux paquets + suppressions)
sbom_v2 = {
"name": "myapp",
"version": "1.1.0",
"packages": [
{"name": "requests", "version": "2.31.0", "license": "Apache-2.0"}, # mis à jour
{"name": "urllib3", "version": "2.0.7", "license": "MIT"}, # mis à jour
{"name": "certifi", "version": "2023.11.17","license": "MPL-2.0"}, # mis à jour
{"name": "django", "version": "4.2.9", "license": "BSD-3-Clause"}, # mis à jour
{"name": "sqlparse", "version": "0.4.4", "license": "BSD-3-Clause"}, # mis à jour
{"name": "cryptography","version": "41.0.7", "license": "Apache-2.0"}, # mis à jour
# pillow supprimé
{"name": "pydantic", "version": "2.5.0", "license": "MIT"}, # ajouté
{"name": "httpx", "version": "0.26.0", "license": "BSD-3-Clause"}, # ajouté
{"name": "suspicious-pkg","version": "0.1.0","license": "UNKNOWN"}, # suspect !
]
}
def sbom_diff(s1: dict, s2: dict) -> dict:
pkgs1 = {p["name"]: p for p in s1["packages"]}
pkgs2 = {p["name"]: p for p in s2["packages"]}
added = {n: pkgs2[n] for n in pkgs2 if n not in pkgs1}
removed = {n: pkgs1[n] for n in pkgs1 if n not in pkgs2}
updated = {n: {"old": pkgs1[n], "new": pkgs2[n]}
for n in pkgs1 if n in pkgs2 and pkgs1[n]["version"] != pkgs2[n]["version"]}
unchanged = {n: pkgs1[n] for n in pkgs1 if n in pkgs2 and pkgs1[n]["version"] == pkgs2[n]["version"]}
return {"added": added, "removed": removed, "updated": updated, "unchanged": unchanged}
diff = sbom_diff(sbom_v1, sbom_v2)
# Visualisation
categories_diff = ["Ajoutés", "Supprimés", "Mis à jour", "Inchangés"]
counts = [len(diff["added"]), len(diff["removed"]), len(diff["updated"]), len(diff["unchanged"])]
couleurs_diff = ["#3498db", "#e74c3c", "#f39c12", "#2ecc71"]
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5))
bars = ax1.bar(categories_diff, counts, color=couleurs_diff, edgecolor="white", linewidth=1)
for bar, count in zip(bars, counts):
ax1.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.05,
str(count), ha="center", va="bottom", fontsize=13, fontweight="bold")
ax1.set_title(f"Diff SBOM : {sbom_v1['name']} {sbom_v1['version']} → {sbom_v2['version']}", fontsize=12)
ax1.set_ylabel("Nombre de paquets")
ax1.set_ylim(0, max(counts) + 1.5)
sns.despine(ax=ax1)
# Tableau des paquets ajoutés avec flags de risque
rows = []
flags = {"suspicious-pkg": "SUSPECT (licence UNKNOWN)", "httpx": "", "pydantic": ""}
for name, info in diff["added"].items():
flag = flags.get(name, "")
rows.append([name, info["version"], info["license"], flag if flag else "—"])
for name, info in diff["removed"].items():
rows.append([f"[supprimé] {name}", info["version"], info["license"], "Vérifier l'usage"])
if rows:
col_labels = ["Paquet", "Version", "Licence", "Alerte"]
table_data = rows
table = ax2.table(cellText=table_data, colLabels=col_labels,
loc="center", cellLoc="left")
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(1, 1.8)
for (row, col), cell in table.get_celld().items():
cell.set_edgecolor("#dee2e6")
if row == 0:
cell.set_facecolor("#2c3e50")
cell.set_text_props(color="white", fontweight="bold")
elif table_data[row - 1][3] not in ("—", "Vérifier l'usage") and table_data[row - 1][3]:
cell.set_facecolor("#fde8e8")
elif table_data[row - 1][0].startswith("[supprimé]"):
cell.set_facecolor("#fff3cd")
else:
cell.set_facecolor("#f8f9fa" if row % 2 == 0 else "white")
ax2.set_title("Paquets ajoutés et supprimés", fontsize=12, pad=15)
ax2.axis("off")
plt.suptitle("Analyse diff de deux SBOM — détection de changements entre versions", fontsize=13, y=1.02)
plt.savefig("sbom_diff.png", dpi=100, bbox_inches="tight")
plt.show()
print("\nRésumé du diff SBOM :")
print(f" Ajoutés : {list(diff['added'].keys())}")
print(f" Supprimés : {list(diff['removed'].keys())}")
print(f" Mis à jour: {[(k, v['old']['version'], '→', v['new']['version']) for k, v in diff['updated'].items()]}")
Résumé du diff SBOM :
Ajoutés : ['pydantic', 'httpx', 'suspicious-pkg']
Supprimés : ['pillow']
Mis à jour: [('requests', '2.28.0', '→', '2.31.0'), ('urllib3', '1.26.14', '→', '2.0.7'), ('certifi', '2022.12.7', '→', '2023.11.17'), ('django', '4.1.7', '→', '4.2.9'), ('sqlparse', '0.4.3', '→', '0.4.4'), ('cryptography', '39.0.2', '→', '41.0.7')]
Heatmap SLSA levels × propriétés de sécurité#
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
proprietes = [
"Provenance\ndocumentée",
"Build service\nauthentifié",
"Provenance\nsignée",
"Build\nhermétique",
"Inputs du build\ndéclarés",
"Environnement\nisolé",
"Revue de code\n(two-party)",
"Reproductibilité\ndu build",
]
niveaux = ["SLSA L1", "SLSA L2", "SLSA L3"]
# 0 = Non requis, 1 = Requis, 2 = Requis + vérifié automatiquement
data_slsa = np.array([
# L1 L2 L3
[1, 1, 2], # Provenance documentée
[0, 1, 2], # Build service authentifié
[0, 1, 2], # Provenance signée
[0, 0, 2], # Build hermétique
[0, 0, 2], # Inputs déclarés
[0, 0, 2], # Environnement isolé
[1, 1, 1], # Revue de code (recommandation, pas exigence stricte)
[0, 0, 1], # Reproductibilité
])
labels_slsa = {0: "Non requis", 1: "Requis", 2: "Requis + vérifié"}
cmap_slsa = ["#e8f4f8", "#f39c12", "#27ae60"]
df_slsa = pd.DataFrame(data_slsa, index=proprietes, columns=niveaux)
fig, ax = plt.subplots(figsize=(9, 7))
for i, row in enumerate(data_slsa):
for j, val in enumerate(row):
color = cmap_slsa[val]
ax.add_patch(plt.Rectangle([j - 0.5, i - 0.5], 1, 1, color=color, alpha=0.9))
text_color = "white" if val == 2 else ("#2c3e50" if val == 0 else "#2c3e50")
ax.text(j, i, labels_slsa[val], ha="center", va="center",
fontsize=9, color=text_color, fontweight="bold" if val > 0 else "normal")
ax.set_xticks(range(len(niveaux)))
ax.set_yticks(range(len(proprietes)))
ax.set_xticklabels(niveaux, fontsize=11, fontweight="bold")
ax.set_yticklabels(proprietes, fontsize=9)
ax.set_xlim(-0.5, len(niveaux) - 0.5)
ax.set_ylim(-0.5, len(proprietes) - 0.5)
ax.set_title("Matrice SLSA : niveaux de sécurité × propriétés du build", fontsize=13, pad=15)
ax.invert_yaxis()
legend_patches = [
mpatches.Patch(color=cmap_slsa[0], label="Non requis"),
mpatches.Patch(color=cmap_slsa[1], label="Requis"),
mpatches.Patch(color=cmap_slsa[2], label="Requis + vérifié automatiquement"),
]
ax.legend(handles=legend_patches, loc="lower right", bbox_to_anchor=(1.5, 0), fontsize=9)
sns.despine(left=True, bottom=True)
plt.savefig("slsa_heatmap.png", dpi=100, bbox_inches="tight")
plt.show()
Résumé#
Anatomie des attaques supply chain : SolarWinds (build compromis, binaires signés légitimement), XZ Utils (contribution malveillante sur 2 ans), typosquatting (noms proches de paquets légitimes), dependency confusion (collision de noms entre registres public et privé).
SBOM comme inventaire de sécurité : SPDX 2.3 et CycloneDX 1.5 permettent de documenter tous les composants avec leurs versions et licences. Indispensable pour répondre rapidement à un incident (Log4Shell → quelles applications sont affectées ?).
Sigstore/Cosign : la signature keyless via OIDC élimine la gestion des clés privées. Les signatures sont enregistrées dans Rekor (journal de transparence immuable), permettant un audit rétrospectif.
in-toto et provenance : les attestations in-toto documentent chaque étape du pipeline (qui a construit quoi, depuis quel commit, avec quels inputs), permettant de détecter des modifications non autorisées dans la chaîne de build.
SLSA L1 → L3 : progression de la documentation de provenance (L1) jusqu’au build hermétique vérifié automatiquement (L3), protégeant contre la compromission du build service lui-même.
Audit et politique de dépendances :
pip-audit,npm audit,cargo auditintégrés en CI bloquent les CVE connues. Dependabot et Renovate automatisent les mises à jour. Une politique explicite évite la dette de sécurité.Réponse aux incidents : le SBOM transforme « quels systèmes sont affectés par cette CVE ? » d’une tâche manuelle de plusieurs jours en une requête de quelques secondes. C’est l’argument décisif pour investir dans la génération systématique de SBOM.