Spaces:
Paused
Paused
Upload folder using huggingface_hub
Browse files- README.md +31 -6
- app.py +864 -0
- requirements.txt +2 -0
README.md
CHANGED
|
@@ -1,12 +1,37 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: SBOM Generator & Validator
|
| 3 |
+
emoji: "\U0001F4E6"
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: "4.44.0"
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
tags:
|
| 12 |
+
- cybersecurity
|
| 13 |
+
- sbom
|
| 14 |
+
- software-supply-chain
|
| 15 |
+
- cyclonedx
|
| 16 |
+
- spdx
|
| 17 |
+
- compliance
|
| 18 |
+
- ayinedjimi-consultants
|
| 19 |
---
|
| 20 |
|
| 21 |
+
# SBOM Generator & Validator
|
| 22 |
+
|
| 23 |
+
Generate and validate Software Bill of Materials (SBOM) documents in CycloneDX and SPDX formats.
|
| 24 |
+
|
| 25 |
+
## Features
|
| 26 |
+
|
| 27 |
+
- **SBOM Generator**: Parse requirements.txt, package.json, pom.xml, or go.mod and generate SBOM in CycloneDX JSON/XML or SPDX JSON/Tag-Value formats
|
| 28 |
+
- **SBOM Validator**: Validate SBOM documents against format specifications and NTIA minimum elements
|
| 29 |
+
- **Regulatory Requirements**: Overview of SBOM regulations (CRA, NIS 2, DORA, EO 14028, FDA, PCI DSS 4.0)
|
| 30 |
+
- **Format Comparison**: Side-by-side comparison of CycloneDX, SPDX, and SWID formats
|
| 31 |
+
- **Tool Ecosystem**: Reference of 15+ SBOM tools for generation, analysis, scanning, and management
|
| 32 |
+
|
| 33 |
+
## Links
|
| 34 |
+
|
| 35 |
+
- [AYI-NEDJIMI Consultants](https://ayinedjimi-consultants.fr)
|
| 36 |
+
- [SBOM 2026 Guide](https://ayinedjimi-consultants.fr/articles/conformite/sbom-2026-obligation-securite.html)
|
| 37 |
+
- [Cyber Resilience Act 2026](https://ayinedjimi-consultants.fr/articles/conformite/cyber-resilience-act-2026.html)
|
app.py
ADDED
|
@@ -0,0 +1,864 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SBOM Generator & Validator - Interactive Gradio Space
|
| 3 |
+
Powered by AYI-NEDJIMI Consultants | https://ayinedjimi-consultants.fr
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import json
|
| 8 |
+
import hashlib
|
| 9 |
+
import uuid
|
| 10 |
+
import re
|
| 11 |
+
from datetime import datetime, timezone
|
| 12 |
+
|
| 13 |
+
# ---------------------------------------------------------------------------
|
| 14 |
+
# Translations
|
| 15 |
+
# ---------------------------------------------------------------------------
|
| 16 |
+
TR = {
|
| 17 |
+
"fr": {
|
| 18 |
+
"title": "Generateur & Validateur SBOM",
|
| 19 |
+
"subtitle": "Creez et validez vos Software Bill of Materials (SBOM) conformes CycloneDX & SPDX",
|
| 20 |
+
"lang_btn": "English",
|
| 21 |
+
"tab_gen": "Generateur SBOM",
|
| 22 |
+
"tab_val": "Validateur SBOM",
|
| 23 |
+
"tab_reg": "Exigences reglementaires",
|
| 24 |
+
"tab_cmp": "Comparaison de formats",
|
| 25 |
+
"tab_tools": "Ecosysteme outils",
|
| 26 |
+
"tab_res": "Ressources",
|
| 27 |
+
"input_label": "Contenu du fichier de dependances",
|
| 28 |
+
"input_placeholder": "Collez ici le contenu de requirements.txt, package.json, pom.xml ou go.mod ...",
|
| 29 |
+
"format_label": "Format de sortie",
|
| 30 |
+
"generate_btn": "Generer le SBOM",
|
| 31 |
+
"output_label": "Document SBOM genere",
|
| 32 |
+
"stats_label": "Statistiques",
|
| 33 |
+
"val_input_label": "Document SBOM a valider",
|
| 34 |
+
"val_input_placeholder": "Collez ici un document SBOM (CycloneDX JSON/XML ou SPDX JSON/Tag-Value) ...",
|
| 35 |
+
"validate_btn": "Valider le SBOM",
|
| 36 |
+
"val_result_label": "Resultat de la validation",
|
| 37 |
+
"val_details_label": "Details et recommandations",
|
| 38 |
+
"no_input": "Veuillez fournir un contenu de fichier de dependances.",
|
| 39 |
+
"no_sbom": "Veuillez fournir un document SBOM a valider.",
|
| 40 |
+
"parsed_ok": "composants detectes",
|
| 41 |
+
"gen_ok": "SBOM genere avec succes",
|
| 42 |
+
"components": "Composants",
|
| 43 |
+
"licenses": "Distribution des licences",
|
| 44 |
+
"format_version": "Version du format",
|
| 45 |
+
"score": "Score de conformite",
|
| 46 |
+
"result_pass": "SUCCES - Le SBOM est conforme",
|
| 47 |
+
"result_fail": "ECHEC - Le SBOM presente des problemes",
|
| 48 |
+
"missing": "Champs manquants",
|
| 49 |
+
"recommendations": "Recommandations",
|
| 50 |
+
"ntia_title": "Checklist NTIA Minimum Elements",
|
| 51 |
+
"reg_title": "Exigences SBOM par regulation",
|
| 52 |
+
"cmp_title": "Comparaison des formats SBOM",
|
| 53 |
+
"tools_title": "Ecosysteme des outils SBOM",
|
| 54 |
+
"res_title": "Ressources et articles",
|
| 55 |
+
"footer": "Propulse par",
|
| 56 |
+
},
|
| 57 |
+
"en": {
|
| 58 |
+
"title": "SBOM Generator & Validator",
|
| 59 |
+
"subtitle": "Create and validate your Software Bill of Materials (SBOM) compliant with CycloneDX & SPDX",
|
| 60 |
+
"lang_btn": "Francais",
|
| 61 |
+
"tab_gen": "SBOM Generator",
|
| 62 |
+
"tab_val": "SBOM Validator",
|
| 63 |
+
"tab_reg": "Regulatory Requirements",
|
| 64 |
+
"tab_cmp": "Format Comparison",
|
| 65 |
+
"tab_tools": "Tool Ecosystem",
|
| 66 |
+
"tab_res": "Resources",
|
| 67 |
+
"input_label": "Dependency file content",
|
| 68 |
+
"input_placeholder": "Paste requirements.txt, package.json, pom.xml or go.mod content here ...",
|
| 69 |
+
"format_label": "Output format",
|
| 70 |
+
"generate_btn": "Generate SBOM",
|
| 71 |
+
"output_label": "Generated SBOM document",
|
| 72 |
+
"stats_label": "Statistics",
|
| 73 |
+
"val_input_label": "SBOM document to validate",
|
| 74 |
+
"val_input_placeholder": "Paste a SBOM document (CycloneDX JSON/XML or SPDX JSON/Tag-Value) here ...",
|
| 75 |
+
"validate_btn": "Validate SBOM",
|
| 76 |
+
"val_result_label": "Validation result",
|
| 77 |
+
"val_details_label": "Details and recommendations",
|
| 78 |
+
"no_input": "Please provide dependency file content.",
|
| 79 |
+
"no_sbom": "Please provide an SBOM document to validate.",
|
| 80 |
+
"parsed_ok": "components detected",
|
| 81 |
+
"gen_ok": "SBOM generated successfully",
|
| 82 |
+
"components": "Components",
|
| 83 |
+
"licenses": "License distribution",
|
| 84 |
+
"format_version": "Format version",
|
| 85 |
+
"score": "Compliance score",
|
| 86 |
+
"result_pass": "PASS - SBOM is compliant",
|
| 87 |
+
"result_fail": "FAIL - SBOM has issues",
|
| 88 |
+
"missing": "Missing fields",
|
| 89 |
+
"recommendations": "Recommendations",
|
| 90 |
+
"ntia_title": "NTIA Minimum Elements Checklist",
|
| 91 |
+
"reg_title": "SBOM requirements by regulation",
|
| 92 |
+
"cmp_title": "SBOM Format Comparison",
|
| 93 |
+
"tools_title": "SBOM Tool Ecosystem",
|
| 94 |
+
"res_title": "Resources and articles",
|
| 95 |
+
"footer": "Powered by",
|
| 96 |
+
},
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
KNOWN_LICENSES = {
|
| 100 |
+
"MIT": "MIT", "Apache-2.0": "Apache-2.0", "BSD-3-Clause": "BSD-3-Clause",
|
| 101 |
+
"BSD-2-Clause": "BSD-2-Clause", "GPL-3.0-only": "GPL-3.0-only",
|
| 102 |
+
"GPL-2.0-only": "GPL-2.0-only", "LGPL-3.0-only": "LGPL-3.0-only",
|
| 103 |
+
"ISC": "ISC", "MPL-2.0": "MPL-2.0", "AGPL-3.0-only": "AGPL-3.0-only",
|
| 104 |
+
"Unlicense": "Unlicense", "0BSD": "0BSD",
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
DEFAULT_LICENSE_MAP = {
|
| 108 |
+
"flask": "BSD-3-Clause", "django": "BSD-3-Clause", "requests": "Apache-2.0",
|
| 109 |
+
"numpy": "BSD-3-Clause", "pandas": "BSD-3-Clause", "fastapi": "MIT",
|
| 110 |
+
"gradio": "Apache-2.0", "express": "MIT", "react": "MIT", "lodash": "MIT",
|
| 111 |
+
"axios": "MIT", "vue": "MIT", "typescript": "Apache-2.0", "webpack": "MIT",
|
| 112 |
+
"spring-boot-starter-web": "Apache-2.0", "junit-jupiter": "EPL-2.0",
|
| 113 |
+
"lombok": "MIT", "gin": "MIT", "cobra": "Apache-2.0", "logrus": "MIT",
|
| 114 |
+
"pytest": "MIT", "black": "MIT", "uvicorn": "BSD-3-Clause",
|
| 115 |
+
"sqlalchemy": "MIT", "pydantic": "MIT", "boto3": "Apache-2.0",
|
| 116 |
+
"tensorflow": "Apache-2.0", "torch": "BSD-3-Clause", "scipy": "BSD-3-Clause",
|
| 117 |
+
"scikit-learn": "BSD-3-Clause", "matplotlib": "PSF-2.0",
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
# ---------------------------------------------------------------------------
|
| 121 |
+
# Parsers
|
| 122 |
+
# ---------------------------------------------------------------------------
|
| 123 |
+
|
| 124 |
+
def _sim_hash(name: str, version: str) -> str:
|
| 125 |
+
return hashlib.sha256(f"{name}@{version}".encode()).hexdigest()
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def _guess_license(name: str) -> str:
|
| 129 |
+
key = name.lower().replace("_", "-")
|
| 130 |
+
return DEFAULT_LICENSE_MAP.get(key, "MIT")
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def parse_requirements(text: str):
|
| 134 |
+
components = []
|
| 135 |
+
for line in text.strip().splitlines():
|
| 136 |
+
line = line.strip()
|
| 137 |
+
if not line or line.startswith("#"):
|
| 138 |
+
continue
|
| 139 |
+
m = re.match(r"^([A-Za-z0-9_\-\.]+)\s*[=~<>!]=?\s*([0-9][^\s,;]*)?", line)
|
| 140 |
+
if m:
|
| 141 |
+
name = m.group(1)
|
| 142 |
+
version = m.group(2) or "0.0.0"
|
| 143 |
+
components.append({"name": name, "version": version,
|
| 144 |
+
"license": _guess_license(name), "type": "library",
|
| 145 |
+
"purl": f"pkg:pypi/{name}@{version}"})
|
| 146 |
+
return components
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def parse_package_json(text: str):
|
| 150 |
+
components = []
|
| 151 |
+
try:
|
| 152 |
+
data = json.loads(text)
|
| 153 |
+
except json.JSONDecodeError:
|
| 154 |
+
return components
|
| 155 |
+
deps = {}
|
| 156 |
+
deps.update(data.get("dependencies", {}))
|
| 157 |
+
deps.update(data.get("devDependencies", {}))
|
| 158 |
+
for name, ver in deps.items():
|
| 159 |
+
version = re.sub(r"[^0-9\.]", "", ver) or "0.0.0"
|
| 160 |
+
components.append({"name": name, "version": version,
|
| 161 |
+
"license": _guess_license(name), "type": "library",
|
| 162 |
+
"purl": f"pkg:npm/{name}@{version}"})
|
| 163 |
+
return components
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def parse_pom_xml(text: str):
|
| 167 |
+
components = []
|
| 168 |
+
pattern = r"<dependency>\s*<groupId>([^<]+)</groupId>\s*<artifactId>([^<]+)</artifactId>\s*<version>([^<]+)</version>"
|
| 169 |
+
for m in re.finditer(pattern, text, re.DOTALL):
|
| 170 |
+
group, artifact, version = m.group(1), m.group(2), m.group(3)
|
| 171 |
+
components.append({"name": f"{group}:{artifact}", "version": version,
|
| 172 |
+
"license": _guess_license(artifact), "type": "library",
|
| 173 |
+
"purl": f"pkg:maven/{group}/{artifact}@{version}"})
|
| 174 |
+
return components
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def parse_go_mod(text: str):
|
| 178 |
+
components = []
|
| 179 |
+
for line in text.strip().splitlines():
|
| 180 |
+
line = line.strip()
|
| 181 |
+
m = re.match(r"^([\w\.\-/]+)\s+(v?[\d\.]+)", line)
|
| 182 |
+
if m and "/" in m.group(1):
|
| 183 |
+
mod = m.group(1)
|
| 184 |
+
version = m.group(2)
|
| 185 |
+
short = mod.split("/")[-1]
|
| 186 |
+
components.append({"name": mod, "version": version,
|
| 187 |
+
"license": _guess_license(short), "type": "library",
|
| 188 |
+
"purl": f"pkg:golang/{mod}@{version}"})
|
| 189 |
+
return components
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def auto_parse(text: str):
|
| 193 |
+
text = text.strip()
|
| 194 |
+
if not text:
|
| 195 |
+
return []
|
| 196 |
+
if text.lstrip().startswith("{"):
|
| 197 |
+
comps = parse_package_json(text)
|
| 198 |
+
if comps:
|
| 199 |
+
return comps
|
| 200 |
+
if "<dependency>" in text:
|
| 201 |
+
comps = parse_pom_xml(text)
|
| 202 |
+
if comps:
|
| 203 |
+
return comps
|
| 204 |
+
if "module " in text or "require (" in text or "go " in text:
|
| 205 |
+
comps = parse_go_mod(text)
|
| 206 |
+
if comps:
|
| 207 |
+
return comps
|
| 208 |
+
return parse_requirements(text)
|
| 209 |
+
|
| 210 |
+
# ---------------------------------------------------------------------------
|
| 211 |
+
# SBOM generators
|
| 212 |
+
# ---------------------------------------------------------------------------
|
| 213 |
+
NOW_ISO = lambda: datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def gen_cyclonedx_json(components):
|
| 217 |
+
bom = {
|
| 218 |
+
"bomFormat": "CycloneDX",
|
| 219 |
+
"specVersion": "1.5",
|
| 220 |
+
"serialNumber": f"urn:uuid:{uuid.uuid4()}",
|
| 221 |
+
"version": 1,
|
| 222 |
+
"metadata": {
|
| 223 |
+
"timestamp": NOW_ISO(),
|
| 224 |
+
"tools": [{"vendor": "AYI-NEDJIMI Consultants", "name": "sbom-generator", "version": "1.0.0"}],
|
| 225 |
+
"component": {"type": "application", "name": "my-application", "version": "1.0.0"},
|
| 226 |
+
},
|
| 227 |
+
"components": [],
|
| 228 |
+
"dependencies": [{"ref": "my-application", "dependsOn": []}],
|
| 229 |
+
}
|
| 230 |
+
for c in components:
|
| 231 |
+
comp = {
|
| 232 |
+
"type": c["type"],
|
| 233 |
+
"name": c["name"],
|
| 234 |
+
"version": c["version"],
|
| 235 |
+
"purl": c["purl"],
|
| 236 |
+
"licenses": [{"license": {"id": c["license"]}}],
|
| 237 |
+
"hashes": [{"alg": "SHA-256", "content": _sim_hash(c["name"], c["version"])}],
|
| 238 |
+
}
|
| 239 |
+
bom["components"].append(comp)
|
| 240 |
+
bom["dependencies"][0]["dependsOn"].append(c["purl"])
|
| 241 |
+
return json.dumps(bom, indent=2)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def gen_cyclonedx_xml(components):
|
| 245 |
+
ts = NOW_ISO()
|
| 246 |
+
serial = f"urn:uuid:{uuid.uuid4()}"
|
| 247 |
+
lines = [
|
| 248 |
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
| 249 |
+
'<bom xmlns="http://cyclonedx.org/schema/bom/1.5" serialNumber="' + serial + '" version="1">',
|
| 250 |
+
" <metadata>",
|
| 251 |
+
f" <timestamp>{ts}</timestamp>",
|
| 252 |
+
" <tools><tool><vendor>AYI-NEDJIMI Consultants</vendor><name>sbom-generator</name><version>1.0.0</version></tool></tools>",
|
| 253 |
+
' <component type="application"><name>my-application</name><version>1.0.0</version></component>',
|
| 254 |
+
" </metadata>",
|
| 255 |
+
" <components>",
|
| 256 |
+
]
|
| 257 |
+
refs = []
|
| 258 |
+
for c in components:
|
| 259 |
+
h = _sim_hash(c["name"], c["version"])
|
| 260 |
+
lines.append(f' <component type="{c["type"]}">')
|
| 261 |
+
lines.append(f" <name>{c['name']}</name>")
|
| 262 |
+
lines.append(f" <version>{c['version']}</version>")
|
| 263 |
+
lines.append(f" <purl>{c['purl']}</purl>")
|
| 264 |
+
lines.append(f' <licenses><license><id>{c["license"]}</id></license></licenses>')
|
| 265 |
+
lines.append(f' <hashes><hash alg="SHA-256">{h}</hash></hashes>')
|
| 266 |
+
lines.append(" </component>")
|
| 267 |
+
refs.append(c["purl"])
|
| 268 |
+
lines.append(" </components>")
|
| 269 |
+
lines.append(" <dependencies>")
|
| 270 |
+
lines.append(' <dependency ref="my-application">')
|
| 271 |
+
for r in refs:
|
| 272 |
+
lines.append(f' <dependency ref="{r}"/>')
|
| 273 |
+
lines.append(" </dependency>")
|
| 274 |
+
lines.append(" </dependencies>")
|
| 275 |
+
lines.append("</bom>")
|
| 276 |
+
return "\n".join(lines)
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def gen_spdx_json(components):
|
| 280 |
+
doc_ns = f"https://spdx.org/spdxdocs/sbom-generator-{uuid.uuid4()}"
|
| 281 |
+
doc = {
|
| 282 |
+
"spdxVersion": "SPDX-2.3",
|
| 283 |
+
"dataLicense": "CC0-1.0",
|
| 284 |
+
"SPDXID": "SPDXRef-DOCUMENT",
|
| 285 |
+
"name": "my-application",
|
| 286 |
+
"documentNamespace": doc_ns,
|
| 287 |
+
"creationInfo": {
|
| 288 |
+
"created": NOW_ISO(),
|
| 289 |
+
"creators": ["Tool: sbom-generator-1.0.0", "Organization: AYI-NEDJIMI Consultants"],
|
| 290 |
+
},
|
| 291 |
+
"packages": [],
|
| 292 |
+
"relationships": [],
|
| 293 |
+
}
|
| 294 |
+
for i, c in enumerate(components):
|
| 295 |
+
spdx_id = f"SPDXRef-Package-{i+1}"
|
| 296 |
+
pkg = {
|
| 297 |
+
"SPDXID": spdx_id,
|
| 298 |
+
"name": c["name"],
|
| 299 |
+
"versionInfo": c["version"],
|
| 300 |
+
"downloadLocation": "https://example.com/download",
|
| 301 |
+
"supplier": "Organization: Open Source",
|
| 302 |
+
"licenseConcluded": c["license"],
|
| 303 |
+
"licenseDeclared": c["license"],
|
| 304 |
+
"copyrightText": "NOASSERTION",
|
| 305 |
+
"externalRefs": [
|
| 306 |
+
{"referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": c["purl"]}
|
| 307 |
+
],
|
| 308 |
+
"checksums": [{"algorithm": "SHA256", "checksumValue": _sim_hash(c["name"], c["version"])}],
|
| 309 |
+
}
|
| 310 |
+
doc["packages"].append(pkg)
|
| 311 |
+
doc["relationships"].append({
|
| 312 |
+
"spdxElementId": "SPDXRef-DOCUMENT",
|
| 313 |
+
"relatedSpdxElement": spdx_id,
|
| 314 |
+
"relationshipType": "DESCRIBES",
|
| 315 |
+
})
|
| 316 |
+
return json.dumps(doc, indent=2)
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
def gen_spdx_tv(components):
|
| 320 |
+
ts = NOW_ISO()
|
| 321 |
+
doc_ns = f"https://spdx.org/spdxdocs/sbom-generator-{uuid.uuid4()}"
|
| 322 |
+
lines = [
|
| 323 |
+
"SPDXVersion: SPDX-2.3",
|
| 324 |
+
"DataLicense: CC0-1.0",
|
| 325 |
+
"SPDXID: SPDXRef-DOCUMENT",
|
| 326 |
+
"DocumentName: my-application",
|
| 327 |
+
f"DocumentNamespace: {doc_ns}",
|
| 328 |
+
f"Creator: Tool: sbom-generator-1.0.0",
|
| 329 |
+
f"Creator: Organization: AYI-NEDJIMI Consultants",
|
| 330 |
+
f"Created: {ts}",
|
| 331 |
+
"",
|
| 332 |
+
]
|
| 333 |
+
for i, c in enumerate(components):
|
| 334 |
+
sid = f"SPDXRef-Package-{i+1}"
|
| 335 |
+
lines += [
|
| 336 |
+
f"PackageName: {c['name']}",
|
| 337 |
+
f"SPDXID: {sid}",
|
| 338 |
+
f"PackageVersion: {c['version']}",
|
| 339 |
+
"PackageDownloadLocation: https://example.com/download",
|
| 340 |
+
"PackageSupplier: Organization: Open Source",
|
| 341 |
+
f"PackageLicenseConcluded: {c['license']}",
|
| 342 |
+
f"PackageLicenseDeclared: {c['license']}",
|
| 343 |
+
"PackageCopyrightText: NOASSERTION",
|
| 344 |
+
f"PackageChecksum: SHA256: {_sim_hash(c['name'], c['version'])}",
|
| 345 |
+
f"ExternalRef: PACKAGE-MANAGER purl {c['purl']}",
|
| 346 |
+
f"Relationship: SPDXRef-DOCUMENT DESCRIBES {sid}",
|
| 347 |
+
"",
|
| 348 |
+
]
|
| 349 |
+
return "\n".join(lines)
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
FORMAT_FUNCS = {
|
| 353 |
+
"CycloneDX JSON": gen_cyclonedx_json,
|
| 354 |
+
"CycloneDX XML": gen_cyclonedx_xml,
|
| 355 |
+
"SPDX JSON": gen_spdx_json,
|
| 356 |
+
"SPDX Tag-Value": gen_spdx_tv,
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
FORMAT_VERSIONS = {
|
| 360 |
+
"CycloneDX JSON": "CycloneDX 1.5",
|
| 361 |
+
"CycloneDX XML": "CycloneDX 1.5",
|
| 362 |
+
"SPDX JSON": "SPDX 2.3",
|
| 363 |
+
"SPDX Tag-Value": "SPDX 2.3",
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
# ---------------------------------------------------------------------------
|
| 367 |
+
# Generate handler
|
| 368 |
+
# ---------------------------------------------------------------------------
|
| 369 |
+
|
| 370 |
+
def generate_sbom(text: str, fmt: str, lang: str):
|
| 371 |
+
t = TR[lang]
|
| 372 |
+
if not text or not text.strip():
|
| 373 |
+
return t["no_input"], "", None
|
| 374 |
+
components = auto_parse(text)
|
| 375 |
+
if not components:
|
| 376 |
+
return t["no_input"], "", None
|
| 377 |
+
sbom_text = FORMAT_FUNCS[fmt](components)
|
| 378 |
+
lic_dist = {}
|
| 379 |
+
for c in components:
|
| 380 |
+
lic_dist[c["license"]] = lic_dist.get(c["license"], 0) + 1
|
| 381 |
+
lic_lines = ", ".join(f"{k}: {v}" for k, v in sorted(lic_dist.items(), key=lambda x: -x[1]))
|
| 382 |
+
stats = (
|
| 383 |
+
f"{t['gen_ok']}\n\n"
|
| 384 |
+
f"**{t['components']}**: {len(components)} {t['parsed_ok']}\n\n"
|
| 385 |
+
f"**{t['licenses']}**: {lic_lines}\n\n"
|
| 386 |
+
f"**{t['format_version']}**: {FORMAT_VERSIONS[fmt]}"
|
| 387 |
+
)
|
| 388 |
+
ext = {"CycloneDX JSON": ".json", "CycloneDX XML": ".xml",
|
| 389 |
+
"SPDX JSON": ".json", "SPDX Tag-Value": ".spdx"}[fmt]
|
| 390 |
+
tmp_path = f"/tmp/sbom{ext}"
|
| 391 |
+
with open(tmp_path, "w") as f:
|
| 392 |
+
f.write(sbom_text)
|
| 393 |
+
return sbom_text, stats, tmp_path
|
| 394 |
+
|
| 395 |
+
# ---------------------------------------------------------------------------
|
| 396 |
+
# Validator
|
| 397 |
+
# ---------------------------------------------------------------------------
|
| 398 |
+
|
| 399 |
+
def validate_sbom(text: str, lang: str):
|
| 400 |
+
t = TR[lang]
|
| 401 |
+
if not text or not text.strip():
|
| 402 |
+
return t["no_sbom"], ""
|
| 403 |
+
text = text.strip()
|
| 404 |
+
issues = []
|
| 405 |
+
checks = {}
|
| 406 |
+
detected = "Unknown"
|
| 407 |
+
components_found = 0
|
| 408 |
+
score = 0
|
| 409 |
+
total = 10
|
| 410 |
+
|
| 411 |
+
# Try CycloneDX JSON
|
| 412 |
+
if text.lstrip().startswith("{") and "bomFormat" in text:
|
| 413 |
+
detected = "CycloneDX JSON"
|
| 414 |
+
try:
|
| 415 |
+
data = json.loads(text)
|
| 416 |
+
except json.JSONDecodeError:
|
| 417 |
+
return "Invalid JSON", ""
|
| 418 |
+
checks["bomFormat"] = data.get("bomFormat") == "CycloneDX"
|
| 419 |
+
checks["specVersion"] = data.get("specVersion", "") in ("1.4", "1.5", "1.6")
|
| 420 |
+
checks["serialNumber"] = bool(data.get("serialNumber"))
|
| 421 |
+
checks["metadata.timestamp"] = bool(data.get("metadata", {}).get("timestamp"))
|
| 422 |
+
checks["metadata.tools"] = bool(data.get("metadata", {}).get("tools"))
|
| 423 |
+
checks["components"] = isinstance(data.get("components"), list) and len(data.get("components", [])) > 0
|
| 424 |
+
components_found = len(data.get("components", []))
|
| 425 |
+
comps = data.get("components", [])
|
| 426 |
+
all_have_name = all("name" in c for c in comps)
|
| 427 |
+
all_have_version = all("version" in c for c in comps)
|
| 428 |
+
all_have_purl = all("purl" in c for c in comps)
|
| 429 |
+
all_have_license = all(c.get("licenses") for c in comps)
|
| 430 |
+
all_have_hash = all(c.get("hashes") for c in comps)
|
| 431 |
+
checks["component.name"] = all_have_name
|
| 432 |
+
checks["component.version"] = all_have_version
|
| 433 |
+
checks["component.purl"] = all_have_purl
|
| 434 |
+
checks["component.license"] = all_have_license
|
| 435 |
+
checks["component.hash"] = all_have_hash
|
| 436 |
+
checks["dependencies"] = isinstance(data.get("dependencies"), list)
|
| 437 |
+
|
| 438 |
+
# Try SPDX JSON
|
| 439 |
+
elif text.lstrip().startswith("{") and "spdxVersion" in text:
|
| 440 |
+
detected = "SPDX JSON"
|
| 441 |
+
try:
|
| 442 |
+
data = json.loads(text)
|
| 443 |
+
except json.JSONDecodeError:
|
| 444 |
+
return "Invalid JSON", ""
|
| 445 |
+
checks["spdxVersion"] = data.get("spdxVersion", "").startswith("SPDX-2.")
|
| 446 |
+
checks["dataLicense"] = data.get("dataLicense") == "CC0-1.0"
|
| 447 |
+
checks["SPDXID"] = data.get("SPDXID") == "SPDXRef-DOCUMENT"
|
| 448 |
+
checks["documentNamespace"] = bool(data.get("documentNamespace"))
|
| 449 |
+
checks["creationInfo"] = bool(data.get("creationInfo", {}).get("created"))
|
| 450 |
+
checks["creators"] = bool(data.get("creationInfo", {}).get("creators"))
|
| 451 |
+
pkgs = data.get("packages", [])
|
| 452 |
+
components_found = len(pkgs)
|
| 453 |
+
checks["packages"] = len(pkgs) > 0
|
| 454 |
+
checks["package.name"] = all("name" in p for p in pkgs)
|
| 455 |
+
checks["package.version"] = all("versionInfo" in p for p in pkgs)
|
| 456 |
+
checks["package.supplier"] = all("supplier" in p for p in pkgs)
|
| 457 |
+
checks["package.license"] = all("licenseConcluded" in p for p in pkgs)
|
| 458 |
+
checks["relationships"] = isinstance(data.get("relationships"), list)
|
| 459 |
+
|
| 460 |
+
# Try SPDX Tag-Value
|
| 461 |
+
elif "SPDXVersion:" in text:
|
| 462 |
+
detected = "SPDX Tag-Value"
|
| 463 |
+
checks["SPDXVersion"] = bool(re.search(r"SPDXVersion:\s*SPDX-2\.\d", text))
|
| 464 |
+
checks["DataLicense"] = "DataLicense: CC0-1.0" in text
|
| 465 |
+
checks["SPDXID"] = "SPDXID: SPDXRef-DOCUMENT" in text
|
| 466 |
+
checks["DocumentNamespace"] = "DocumentNamespace:" in text
|
| 467 |
+
checks["Creator"] = "Creator:" in text
|
| 468 |
+
checks["Created"] = "Created:" in text
|
| 469 |
+
pkg_names = re.findall(r"PackageName:\s*(.+)", text)
|
| 470 |
+
components_found = len(pkg_names)
|
| 471 |
+
checks["packages"] = components_found > 0
|
| 472 |
+
checks["PackageVersion"] = "PackageVersion:" in text
|
| 473 |
+
checks["PackageSupplier"] = "PackageSupplier:" in text
|
| 474 |
+
checks["PackageLicense"] = "PackageLicenseConcluded:" in text
|
| 475 |
+
checks["PackageChecksum"] = "PackageChecksum:" in text
|
| 476 |
+
|
| 477 |
+
# Try CycloneDX XML
|
| 478 |
+
elif "<bom" in text and "cyclonedx" in text.lower():
|
| 479 |
+
detected = "CycloneDX XML"
|
| 480 |
+
checks["namespace"] = "cyclonedx.org" in text
|
| 481 |
+
checks["serialNumber"] = 'serialNumber="' in text
|
| 482 |
+
checks["metadata"] = "<metadata>" in text
|
| 483 |
+
checks["timestamp"] = "<timestamp>" in text
|
| 484 |
+
checks["tools"] = "<tools>" in text
|
| 485 |
+
checks["components"] = "<components>" in text
|
| 486 |
+
comp_matches = re.findall(r"<component", text)
|
| 487 |
+
components_found = len(comp_matches)
|
| 488 |
+
checks["component.name"] = "<name>" in text
|
| 489 |
+
checks["component.version"] = "<version>" in text
|
| 490 |
+
checks["component.purl"] = "<purl>" in text
|
| 491 |
+
checks["component.license"] = "<license>" in text or "<licenses>" in text
|
| 492 |
+
checks["component.hash"] = "<hash" in text
|
| 493 |
+
else:
|
| 494 |
+
return "Unrecognized SBOM format. Supported: CycloneDX JSON/XML, SPDX JSON/Tag-Value.", ""
|
| 495 |
+
|
| 496 |
+
passed = sum(1 for v in checks.values() if v)
|
| 497 |
+
total = len(checks)
|
| 498 |
+
pct = int(100 * passed / total) if total else 0
|
| 499 |
+
is_pass = pct >= 80
|
| 500 |
+
|
| 501 |
+
# NTIA minimum elements
|
| 502 |
+
ntia = {
|
| 503 |
+
"Supplier Name": checks.get("package.supplier", checks.get("PackageSupplier", checks.get("metadata.tools", False))),
|
| 504 |
+
"Component Name": checks.get("component.name", checks.get("package.name", checks.get("packages", False))),
|
| 505 |
+
"Component Version": checks.get("component.version", checks.get("package.version", checks.get("PackageVersion", False))),
|
| 506 |
+
"Unique Identifier": checks.get("component.purl", checks.get("SPDXID", checks.get("serialNumber", False))),
|
| 507 |
+
"Dependency Relationship": checks.get("dependencies", checks.get("relationships", False)),
|
| 508 |
+
"Author of SBOM Data": checks.get("metadata.tools", checks.get("creators", checks.get("Creator", False))),
|
| 509 |
+
"Timestamp": checks.get("metadata.timestamp", checks.get("creationInfo", checks.get("Created", False))),
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
# Build result
|
| 513 |
+
status_icon = "PASS" if is_pass else "FAIL"
|
| 514 |
+
result_msg = t["result_pass"] if is_pass else t["result_fail"]
|
| 515 |
+
result = f"**{status_icon}** - {result_msg}\n\n"
|
| 516 |
+
result += f"**Format**: {detected}\n"
|
| 517 |
+
result += f"**{t['components']}**: {components_found}\n"
|
| 518 |
+
result += f"**{t['score']}**: {pct}% ({passed}/{total})\n"
|
| 519 |
+
|
| 520 |
+
# Details
|
| 521 |
+
details = "### Validation checks\n\n"
|
| 522 |
+
for k, v in checks.items():
|
| 523 |
+
mark = "PASS" if v else "FAIL"
|
| 524 |
+
details += f"- [{mark}] {k}\n"
|
| 525 |
+
|
| 526 |
+
failed = [k for k, v in checks.items() if not v]
|
| 527 |
+
if failed:
|
| 528 |
+
details += f"\n### {t['missing']}\n\n"
|
| 529 |
+
for f_item in failed:
|
| 530 |
+
details += f"- {f_item}\n"
|
| 531 |
+
|
| 532 |
+
details += f"\n### {t['ntia_title']}\n\n"
|
| 533 |
+
for k, v in ntia.items():
|
| 534 |
+
mark = "PASS" if v else "FAIL"
|
| 535 |
+
details += f"- [{mark}] {k}\n"
|
| 536 |
+
|
| 537 |
+
if not is_pass:
|
| 538 |
+
details += f"\n### {t['recommendations']}\n\n"
|
| 539 |
+
if not checks.get("component.hash", checks.get("PackageChecksum", checks.get("component.hash", True))):
|
| 540 |
+
details += "- Add SHA-256 checksums to all components\n"
|
| 541 |
+
if not checks.get("component.license", checks.get("package.license", checks.get("PackageLicense", True))):
|
| 542 |
+
details += "- Add SPDX license identifiers to all components\n"
|
| 543 |
+
if not checks.get("component.purl", checks.get("SPDXID", True)):
|
| 544 |
+
details += "- Add Package URLs (purl) for unique identification\n"
|
| 545 |
+
if not checks.get("dependencies", checks.get("relationships", True)):
|
| 546 |
+
details += "- Include dependency relationships\n"
|
| 547 |
+
|
| 548 |
+
return result, details
|
| 549 |
+
|
| 550 |
+
# ---------------------------------------------------------------------------
|
| 551 |
+
# Static content builders
|
| 552 |
+
# ---------------------------------------------------------------------------
|
| 553 |
+
|
| 554 |
+
def build_regulations(lang):
|
| 555 |
+
if lang == "fr":
|
| 556 |
+
return """## Exigences SBOM par regulation
|
| 557 |
+
|
| 558 |
+
| Regulation | Exigence | Format attendu | Echeance | Sanction | Perimetre |
|
| 559 |
+
|---|---|---|---|---|---|
|
| 560 |
+
| **EU Cyber Resilience Act (CRA)** | SBOM obligatoire pour tout produit contenant des elements numeriques | CycloneDX ou SPDX | 2027 (application) | Jusqu'a 15M EUR ou 2,5% du CA mondial | Tous produits avec elements numeriques vendus dans l'UE |
|
| 561 |
+
| **NIS 2** | Securite de la chaine d'approvisionnement, inventaire logiciel | Non specifie (CycloneDX/SPDX recommande) | Octobre 2024 (transposition) | Jusqu'a 10M EUR ou 2% du CA | Entites essentielles et importantes (17 secteurs) |
|
| 562 |
+
| **DORA** | Gestion des risques lies aux tiers TIC, registre des actifs | Non specifie | Janvier 2025 | Sanctions administratives | Entites financieres de l'UE |
|
| 563 |
+
| **US Executive Order 14028** | SBOM pour tout logiciel vendu au gouvernement federal | SPDX ou CycloneDX (NTIA minimum elements) | En vigueur | Exclusion des marches federaux | Fournisseurs du gouvernement federal US |
|
| 564 |
+
| **FDA (Medical Devices)** | SBOM obligatoire pour les dispositifs medicaux connectes | CycloneDX ou SPDX | En vigueur (2023) | Refus de mise sur le marche | Dispositifs medicaux avec composants logiciels |
|
| 565 |
+
| **PCI DSS 4.0** | Inventaire des composants logiciels, gestion des vulnerabilites | Non specifie | Mars 2025 (exigences futures) | Non-conformite PCI | Organisations traitant des donnees de cartes de paiement |
|
| 566 |
+
"""
|
| 567 |
+
return """## SBOM Requirements by Regulation
|
| 568 |
+
|
| 569 |
+
| Regulation | Requirement | Expected Format | Deadline | Penalty | Scope |
|
| 570 |
+
|---|---|---|---|---|---|
|
| 571 |
+
| **EU Cyber Resilience Act (CRA)** | Mandatory SBOM for all products with digital elements | CycloneDX or SPDX | 2027 (enforcement) | Up to 15M EUR or 2.5% global turnover | All products with digital elements sold in the EU |
|
| 572 |
+
| **NIS 2** | Supply chain security, software inventory | Not specified (CycloneDX/SPDX recommended) | October 2024 (transposition) | Up to 10M EUR or 2% turnover | Essential and important entities (17 sectors) |
|
| 573 |
+
| **DORA** | ICT third-party risk management, asset registry | Not specified | January 2025 | Administrative sanctions | EU financial entities |
|
| 574 |
+
| **US Executive Order 14028** | SBOM for all software sold to federal government | SPDX or CycloneDX (NTIA minimum elements) | In force | Exclusion from federal contracts | US federal government suppliers |
|
| 575 |
+
| **FDA (Medical Devices)** | Mandatory SBOM for connected medical devices | CycloneDX or SPDX | In force (2023) | Market authorization refusal | Medical devices with software components |
|
| 576 |
+
| **PCI DSS 4.0** | Software component inventory, vulnerability management | Not specified | March 2025 (future requirements) | PCI non-compliance | Organizations processing payment card data |
|
| 577 |
+
"""
|
| 578 |
+
|
| 579 |
+
|
| 580 |
+
def build_comparison(lang):
|
| 581 |
+
if lang == "fr":
|
| 582 |
+
return """## Comparaison des formats SBOM
|
| 583 |
+
|
| 584 |
+
| Critere | CycloneDX | SPDX | SWID |
|
| 585 |
+
|---|---|---|---|
|
| 586 |
+
| **Organisation** | OWASP | Linux Foundation | ISO/IEC 19770-2 |
|
| 587 |
+
| **Version actuelle** | 1.6 | 2.3 | 2016 |
|
| 588 |
+
| **Formats** | JSON, XML, Protobuf | JSON, RDF, Tag-Value, XLSX | XML |
|
| 589 |
+
| **Licence du schema** | Apache-2.0 | CC-BY-3.0 | ISO Standard |
|
| 590 |
+
| **Focus principal** | Securite, vulnerabilites | Conformite licences | Identification logicielle |
|
| 591 |
+
| **Composants imbriques** | Oui (natif) | Oui (via relations) | Limite |
|
| 592 |
+
| **Vulnerabilites (VEX)** | Integre (CycloneDX VEX) | Via SPDX Security | Non |
|
| 593 |
+
| **Services & API** | Oui | Non | Non |
|
| 594 |
+
| **Licences SPDX** | Oui | Oui (natif) | Non |
|
| 595 |
+
| **PURL (Package URL)** | Oui (natif) | Oui | Non |
|
| 596 |
+
| **Signature numerique** | Oui (JSF, XML Sig) | Non | Oui (XML Sig) |
|
| 597 |
+
| **Machine readable** | Oui | Oui | Oui |
|
| 598 |
+
| **Adoption industrie** | Elevee (securite) | Elevee (open source) | Faible |
|
| 599 |
+
| **NTIA conforme** | Oui | Oui | Partiel |
|
| 600 |
+
| **CRA conforme** | Oui | Oui | Non recommande |
|
| 601 |
+
|
| 602 |
+
### Recommandations par cas d'usage
|
| 603 |
+
|
| 604 |
+
- **Securite et gestion des vulnerabilites** : CycloneDX (VEX integre, focus securite)
|
| 605 |
+
- **Conformite open source et licences** : SPDX (standard Linux Foundation, focus licences)
|
| 606 |
+
- **Inventaire IT et gestion des actifs** : SWID (standard ISO, identification)
|
| 607 |
+
- **Conformite CRA / NIS 2** : CycloneDX ou SPDX (les deux sont acceptes)
|
| 608 |
+
- **Marches publics US** : SPDX ou CycloneDX (NTIA minimum elements)
|
| 609 |
+
"""
|
| 610 |
+
return """## SBOM Format Comparison
|
| 611 |
+
|
| 612 |
+
| Criteria | CycloneDX | SPDX | SWID |
|
| 613 |
+
|---|---|---|---|
|
| 614 |
+
| **Organization** | OWASP | Linux Foundation | ISO/IEC 19770-2 |
|
| 615 |
+
| **Current version** | 1.6 | 2.3 | 2016 |
|
| 616 |
+
| **Formats** | JSON, XML, Protobuf | JSON, RDF, Tag-Value, XLSX | XML |
|
| 617 |
+
| **Schema license** | Apache-2.0 | CC-BY-3.0 | ISO Standard |
|
| 618 |
+
| **Primary focus** | Security, vulnerabilities | License compliance | Software identification |
|
| 619 |
+
| **Nested components** | Yes (native) | Yes (via relationships) | Limited |
|
| 620 |
+
| **Vulnerabilities (VEX)** | Built-in (CycloneDX VEX) | Via SPDX Security | No |
|
| 621 |
+
| **Services & API** | Yes | No | No |
|
| 622 |
+
| **SPDX licenses** | Yes | Yes (native) | No |
|
| 623 |
+
| **PURL (Package URL)** | Yes (native) | Yes | No |
|
| 624 |
+
| **Digital signature** | Yes (JSF, XML Sig) | No | Yes (XML Sig) |
|
| 625 |
+
| **Machine readable** | Yes | Yes | Yes |
|
| 626 |
+
| **Industry adoption** | High (security) | High (open source) | Low |
|
| 627 |
+
| **NTIA compliant** | Yes | Yes | Partial |
|
| 628 |
+
| **CRA compliant** | Yes | Yes | Not recommended |
|
| 629 |
+
|
| 630 |
+
### Recommendations by Use Case
|
| 631 |
+
|
| 632 |
+
- **Security and vulnerability management**: CycloneDX (built-in VEX, security focus)
|
| 633 |
+
- **Open source and license compliance**: SPDX (Linux Foundation standard, license focus)
|
| 634 |
+
- **IT inventory and asset management**: SWID (ISO standard, identification)
|
| 635 |
+
- **CRA / NIS 2 compliance**: CycloneDX or SPDX (both accepted)
|
| 636 |
+
- **US federal procurement**: SPDX or CycloneDX (NTIA minimum elements)
|
| 637 |
+
"""
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
def build_tools(lang):
|
| 641 |
+
if lang == "fr":
|
| 642 |
+
header = "## Ecosysteme des outils SBOM\n\n"
|
| 643 |
+
else:
|
| 644 |
+
header = "## SBOM Tool Ecosystem\n\n"
|
| 645 |
+
|
| 646 |
+
cat_gen = "Generation" if lang == "fr" else "Generation"
|
| 647 |
+
cat_ana = "Analyse" if lang == "fr" else "Analysis"
|
| 648 |
+
cat_scan = "Scan de vulnerabilites" if lang == "fr" else "Vulnerability Scanning"
|
| 649 |
+
cat_mgmt = "Gestion" if lang == "fr" else "Management"
|
| 650 |
+
|
| 651 |
+
tools = [
|
| 652 |
+
(cat_gen, "Syft", "SBOM generation from container images and filesystems", "CycloneDX, SPDX", "Open Source (Apache-2.0)", "https://github.com/anchore/syft"),
|
| 653 |
+
(cat_gen, "cdxgen", "CycloneDX SBOM generator for many languages", "CycloneDX", "Open Source (Apache-2.0)", "https://github.com/CycloneDX/cdxgen"),
|
| 654 |
+
(cat_gen, "Microsoft sbom-tool", "SBOM generation tool by Microsoft", "SPDX", "Open Source (MIT)", "https://github.com/microsoft/sbom-tool"),
|
| 655 |
+
(cat_gen, "SPDX Tools", "Official SPDX format tools and libraries", "SPDX", "Open Source", "https://github.com/spdx/tools-java"),
|
| 656 |
+
(cat_ana, "Grype", "Vulnerability scanner for container images and SBOM", "CycloneDX, SPDX", "Open Source (Apache-2.0)", "https://github.com/anchore/grype"),
|
| 657 |
+
(cat_ana, "Dependency-Track", "Intelligent component analysis platform", "CycloneDX", "Open Source (Apache-2.0)", "https://dependencytrack.org/"),
|
| 658 |
+
(cat_ana, "GUAC", "Graph for Understanding Artifact Composition", "CycloneDX, SPDX", "Open Source (Apache-2.0)", "https://guac.sh/"),
|
| 659 |
+
(cat_ana, "OSS Review Toolkit", "Suite for open source review and compliance", "SPDX, CycloneDX", "Open Source (Apache-2.0)", "https://github.com/oss-review-toolkit/ort"),
|
| 660 |
+
(cat_scan, "Trivy", "All-in-one security scanner (containers, IaC, SBOM)", "CycloneDX, SPDX", "Open Source (Apache-2.0)", "https://trivy.dev/"),
|
| 661 |
+
(cat_scan, "Snyk", "Developer-first security platform", "CycloneDX, SPDX", "Freemium / Enterprise", "https://snyk.io/"),
|
| 662 |
+
(cat_scan, "Black Duck", "Software composition analysis (SCA)", "SPDX, CycloneDX", "Commercial", "https://www.blackducksoftware.com/"),
|
| 663 |
+
(cat_scan, "Mend (WhiteSource)", "Open source security and compliance", "CycloneDX, SPDX", "Freemium / Enterprise", "https://www.mend.io/"),
|
| 664 |
+
(cat_mgmt, "OWASP Dependency-Track", "Component analysis and SBOM management", "CycloneDX", "Open Source (Apache-2.0)", "https://dependencytrack.org/"),
|
| 665 |
+
(cat_mgmt, "JFrog Xray", "Universal artifact analysis and SBOM management", "CycloneDX, SPDX", "Commercial", "https://jfrog.com/xray/"),
|
| 666 |
+
(cat_mgmt, "Sonatype Nexus Lifecycle", "Software supply chain management", "CycloneDX, SPDX", "Commercial", "https://www.sonatype.com/products/open-source-security-dependency-management"),
|
| 667 |
+
]
|
| 668 |
+
|
| 669 |
+
if lang == "fr":
|
| 670 |
+
table = "| Categorie | Outil | Description | Formats | Tarification | Lien |\n"
|
| 671 |
+
else:
|
| 672 |
+
table = "| Category | Tool | Description | Formats | Pricing | Link |\n"
|
| 673 |
+
table += "|---|---|---|---|---|---|\n"
|
| 674 |
+
for cat, name, desc, fmts, price, link in tools:
|
| 675 |
+
table += f"| {cat} | **{name}** | {desc} | {fmts} | {price} | [Site]({link}) |\n"
|
| 676 |
+
|
| 677 |
+
return header + table
|
| 678 |
+
|
| 679 |
+
|
| 680 |
+
def build_resources(lang):
|
| 681 |
+
if lang == "fr":
|
| 682 |
+
return """## Ressources et articles
|
| 683 |
+
|
| 684 |
+
### AYI-NEDJIMI Consultants - Expertise conformite et cybersecurite
|
| 685 |
+
|
| 686 |
+
- [SBOM 2026 : obligation et securite de la chaine logicielle](https://ayinedjimi-consultants.fr/articles/conformite/sbom-2026-obligation-securite.html) - Guide complet sur les obligations SBOM a venir et les bonnes pratiques de securite de la chaine d'approvisionnement logicielle.
|
| 687 |
+
|
| 688 |
+
- [Cyber Resilience Act 2026 : se preparer a la conformite](https://ayinedjimi-consultants.fr/articles/conformite/cyber-resilience-act-2026.html) - Tout ce qu'il faut savoir sur le CRA europeen et comment preparer votre organisation.
|
| 689 |
+
|
| 690 |
+
- [AYI-NEDJIMI Consultants - Accueil](https://ayinedjimi-consultants.fr) - Cabinet de conseil specialise en conformite reglementaire, cybersecurite et gouvernance des donnees.
|
| 691 |
+
|
| 692 |
+
### Standards et specifications
|
| 693 |
+
|
| 694 |
+
- [CycloneDX Specification](https://cyclonedx.org/specification/overview/) - Specification officielle du format CycloneDX (OWASP).
|
| 695 |
+
- [SPDX Specification](https://spdx.github.io/spdx-spec/v2.3/) - Specification officielle du format SPDX (Linux Foundation).
|
| 696 |
+
- [NTIA SBOM Minimum Elements](https://www.ntia.gov/page/software-bill-materials) - Elements minimaux NTIA pour les SBOM.
|
| 697 |
+
- [EU Cyber Resilience Act](https://digital-strategy.ec.europa.eu/en/policies/cyber-resilience-act) - Texte officiel du CRA europeen.
|
| 698 |
+
|
| 699 |
+
### Communaute
|
| 700 |
+
|
| 701 |
+
- [OWASP Software Component Verification Standard](https://owasp.org/www-project-software-component-verification-standard/) - Standard OWASP pour la verification des composants logiciels.
|
| 702 |
+
- [OpenSSF Scorecard](https://securityscorecards.dev/) - Evaluation automatisee de la securite des projets open source.
|
| 703 |
+
"""
|
| 704 |
+
return """## Resources and Articles
|
| 705 |
+
|
| 706 |
+
### AYI-NEDJIMI Consultants - Compliance and Cybersecurity Expertise
|
| 707 |
+
|
| 708 |
+
- [SBOM 2026: Obligation and Software Supply Chain Security](https://ayinedjimi-consultants.fr/articles/conformite/sbom-2026-obligation-securite.html) - Comprehensive guide on upcoming SBOM obligations and software supply chain security best practices.
|
| 709 |
+
|
| 710 |
+
- [Cyber Resilience Act 2026: Preparing for Compliance](https://ayinedjimi-consultants.fr/articles/conformite/cyber-resilience-act-2026.html) - Everything you need to know about the European CRA and how to prepare your organization.
|
| 711 |
+
|
| 712 |
+
- [AYI-NEDJIMI Consultants - Home](https://ayinedjimi-consultants.fr) - Consulting firm specialized in regulatory compliance, cybersecurity, and data governance.
|
| 713 |
+
|
| 714 |
+
### Standards and Specifications
|
| 715 |
+
|
| 716 |
+
- [CycloneDX Specification](https://cyclonedx.org/specification/overview/) - Official CycloneDX format specification (OWASP).
|
| 717 |
+
- [SPDX Specification](https://spdx.github.io/spdx-spec/v2.3/) - Official SPDX format specification (Linux Foundation).
|
| 718 |
+
- [NTIA SBOM Minimum Elements](https://www.ntia.gov/page/software-bill-materials) - NTIA minimum elements for SBOMs.
|
| 719 |
+
- [EU Cyber Resilience Act](https://digital-strategy.ec.europa.eu/en/policies/cyber-resilience-act) - Official EU CRA text.
|
| 720 |
+
|
| 721 |
+
### Community
|
| 722 |
+
|
| 723 |
+
- [OWASP Software Component Verification Standard](https://owasp.org/www-project-software-component-verification-standard/) - OWASP standard for software component verification.
|
| 724 |
+
- [OpenSSF Scorecard](https://securityscorecards.dev/) - Automated security assessment for open source projects.
|
| 725 |
+
"""
|
| 726 |
+
|
| 727 |
+
|
| 728 |
+
# ---------------------------------------------------------------------------
|
| 729 |
+
# Gradio UI
|
| 730 |
+
# ---------------------------------------------------------------------------
|
| 731 |
+
|
| 732 |
+
FOOTER_HTML = (
|
| 733 |
+
'<div style="text-align:center; margin-top:24px; padding:16px; '
|
| 734 |
+
'border-top:1px solid #ddd; color:#666; font-size:0.9em;">'
|
| 735 |
+
'Powered by <a href="https://ayinedjimi-consultants.fr" target="_blank">'
|
| 736 |
+
'AYI-NEDJIMI Consultants</a> | '
|
| 737 |
+
'<a href="https://ayinedjimi-consultants.fr/articles/conformite/'
|
| 738 |
+
'sbom-2026-obligation-securite.html" target="_blank">SBOM 2026 Guide</a> | '
|
| 739 |
+
'<a href="https://ayinedjimi-consultants.fr/articles/conformite/'
|
| 740 |
+
'cyber-resilience-act-2026.html" target="_blank">Cyber Resilience Act</a>'
|
| 741 |
+
'</div>'
|
| 742 |
+
)
|
| 743 |
+
|
| 744 |
+
|
| 745 |
+
def build_app():
|
| 746 |
+
with gr.Blocks(
|
| 747 |
+
title="SBOM Generator & Validator",
|
| 748 |
+
theme=gr.themes.Soft(),
|
| 749 |
+
css=".footer-link a { color: #2563eb; text-decoration: none; }"
|
| 750 |
+
) as demo:
|
| 751 |
+
|
| 752 |
+
lang_state = gr.State("fr")
|
| 753 |
+
|
| 754 |
+
gr.Markdown("# Generateur & Validateur SBOM")
|
| 755 |
+
gr.Markdown("Creez et validez vos Software Bill of Materials (SBOM) conformes CycloneDX & SPDX")
|
| 756 |
+
|
| 757 |
+
lang_btn = gr.Button("English", variant="secondary", elem_id="lang-btn")
|
| 758 |
+
|
| 759 |
+
# ------------------------------------------------------------------
|
| 760 |
+
# Tab 1: Generator
|
| 761 |
+
# ------------------------------------------------------------------
|
| 762 |
+
with gr.Tab("Generateur SBOM"):
|
| 763 |
+
with gr.Row():
|
| 764 |
+
with gr.Column():
|
| 765 |
+
gen_input = gr.Textbox(
|
| 766 |
+
label="Contenu du fichier de dependances",
|
| 767 |
+
placeholder="Collez ici le contenu de requirements.txt, package.json, pom.xml ou go.mod ...",
|
| 768 |
+
lines=12,
|
| 769 |
+
)
|
| 770 |
+
gen_format = gr.Dropdown(
|
| 771 |
+
choices=["CycloneDX JSON", "CycloneDX XML", "SPDX JSON", "SPDX Tag-Value"],
|
| 772 |
+
value="CycloneDX JSON",
|
| 773 |
+
label="Format de sortie",
|
| 774 |
+
)
|
| 775 |
+
gen_btn = gr.Button("Generer le SBOM", variant="primary")
|
| 776 |
+
with gr.Column():
|
| 777 |
+
gen_stats = gr.Markdown(label="Statistiques")
|
| 778 |
+
gen_output = gr.Textbox(label="Document SBOM genere", lines=20, show_copy_button=True)
|
| 779 |
+
gen_file = gr.File(label="Telecharger le SBOM")
|
| 780 |
+
|
| 781 |
+
gen_btn.click(
|
| 782 |
+
fn=generate_sbom,
|
| 783 |
+
inputs=[gen_input, gen_format, lang_state],
|
| 784 |
+
outputs=[gen_output, gen_stats, gen_file],
|
| 785 |
+
)
|
| 786 |
+
|
| 787 |
+
# ------------------------------------------------------------------
|
| 788 |
+
# Tab 2: Validator
|
| 789 |
+
# ------------------------------------------------------------------
|
| 790 |
+
with gr.Tab("Validateur SBOM"):
|
| 791 |
+
with gr.Row():
|
| 792 |
+
with gr.Column():
|
| 793 |
+
val_input = gr.Textbox(
|
| 794 |
+
label="Document SBOM a valider",
|
| 795 |
+
placeholder="Collez ici un document SBOM (CycloneDX JSON/XML ou SPDX JSON/Tag-Value) ...",
|
| 796 |
+
lines=15,
|
| 797 |
+
)
|
| 798 |
+
val_btn = gr.Button("Valider le SBOM", variant="primary")
|
| 799 |
+
with gr.Column():
|
| 800 |
+
val_result = gr.Markdown(label="Resultat de la validation")
|
| 801 |
+
val_details = gr.Markdown(label="Details et recommandations")
|
| 802 |
+
|
| 803 |
+
val_btn.click(
|
| 804 |
+
fn=validate_sbom,
|
| 805 |
+
inputs=[val_input, lang_state],
|
| 806 |
+
outputs=[val_result, val_details],
|
| 807 |
+
)
|
| 808 |
+
|
| 809 |
+
# ------------------------------------------------------------------
|
| 810 |
+
# Tab 3: Regulations
|
| 811 |
+
# ------------------------------------------------------------------
|
| 812 |
+
with gr.Tab("Exigences reglementaires"):
|
| 813 |
+
reg_md = gr.Markdown(value=build_regulations("fr"))
|
| 814 |
+
|
| 815 |
+
# ------------------------------------------------------------------
|
| 816 |
+
# Tab 4: Format Comparison
|
| 817 |
+
# ------------------------------------------------------------------
|
| 818 |
+
with gr.Tab("Comparaison de formats"):
|
| 819 |
+
cmp_md = gr.Markdown(value=build_comparison("fr"))
|
| 820 |
+
|
| 821 |
+
# ------------------------------------------------------------------
|
| 822 |
+
# Tab 5: Tool Ecosystem
|
| 823 |
+
# ------------------------------------------------------------------
|
| 824 |
+
with gr.Tab("Ecosysteme outils"):
|
| 825 |
+
tools_md = gr.Markdown(value=build_tools("fr"))
|
| 826 |
+
|
| 827 |
+
# ------------------------------------------------------------------
|
| 828 |
+
# Tab 6: Resources
|
| 829 |
+
# ------------------------------------------------------------------
|
| 830 |
+
with gr.Tab("Ressources"):
|
| 831 |
+
res_md = gr.Markdown(value=build_resources("fr"))
|
| 832 |
+
|
| 833 |
+
# ------------------------------------------------------------------
|
| 834 |
+
# Footer
|
| 835 |
+
# ------------------------------------------------------------------
|
| 836 |
+
gr.HTML(FOOTER_HTML)
|
| 837 |
+
|
| 838 |
+
# ------------------------------------------------------------------
|
| 839 |
+
# Language toggle
|
| 840 |
+
# ------------------------------------------------------------------
|
| 841 |
+
def toggle_language(current_lang):
|
| 842 |
+
new_lang = "en" if current_lang == "fr" else "fr"
|
| 843 |
+
t = TR[new_lang]
|
| 844 |
+
return (
|
| 845 |
+
new_lang,
|
| 846 |
+
t["lang_btn"],
|
| 847 |
+
build_regulations(new_lang),
|
| 848 |
+
build_comparison(new_lang),
|
| 849 |
+
build_tools(new_lang),
|
| 850 |
+
build_resources(new_lang),
|
| 851 |
+
)
|
| 852 |
+
|
| 853 |
+
lang_btn.click(
|
| 854 |
+
fn=toggle_language,
|
| 855 |
+
inputs=[lang_state],
|
| 856 |
+
outputs=[lang_state, lang_btn, reg_md, cmp_md, tools_md, res_md],
|
| 857 |
+
)
|
| 858 |
+
|
| 859 |
+
return demo
|
| 860 |
+
|
| 861 |
+
|
| 862 |
+
if __name__ == "__main__":
|
| 863 |
+
demo = build_app()
|
| 864 |
+
demo.launch(server_name="0.0.0.0", server_port=7860)
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==4.44.0
|
| 2 |
+
huggingface_hub==0.24.7
|