Spaces:
Paused
Paused
| """ | |
| SBOM Generator & Validator - Interactive Gradio Space | |
| Powered by AYI-NEDJIMI Consultants | https://ayinedjimi-consultants.fr | |
| """ | |
| import gradio as gr | |
| import json | |
| import hashlib | |
| import uuid | |
| import re | |
| from datetime import datetime, timezone | |
| # --------------------------------------------------------------------------- | |
| # Translations | |
| # --------------------------------------------------------------------------- | |
| TR = { | |
| "fr": { | |
| "title": "Generateur & Validateur SBOM", | |
| "subtitle": "Creez et validez vos Software Bill of Materials (SBOM) conformes CycloneDX & SPDX", | |
| "lang_btn": "English", | |
| "tab_gen": "Generateur SBOM", | |
| "tab_val": "Validateur SBOM", | |
| "tab_reg": "Exigences reglementaires", | |
| "tab_cmp": "Comparaison de formats", | |
| "tab_tools": "Ecosysteme outils", | |
| "tab_res": "Ressources", | |
| "input_label": "Contenu du fichier de dependances", | |
| "input_placeholder": "Collez ici le contenu de requirements.txt, package.json, pom.xml ou go.mod ...", | |
| "format_label": "Format de sortie", | |
| "generate_btn": "Generer le SBOM", | |
| "output_label": "Document SBOM genere", | |
| "stats_label": "Statistiques", | |
| "val_input_label": "Document SBOM a valider", | |
| "val_input_placeholder": "Collez ici un document SBOM (CycloneDX JSON/XML ou SPDX JSON/Tag-Value) ...", | |
| "validate_btn": "Valider le SBOM", | |
| "val_result_label": "Resultat de la validation", | |
| "val_details_label": "Details et recommandations", | |
| "no_input": "Veuillez fournir un contenu de fichier de dependances.", | |
| "no_sbom": "Veuillez fournir un document SBOM a valider.", | |
| "parsed_ok": "composants detectes", | |
| "gen_ok": "SBOM genere avec succes", | |
| "components": "Composants", | |
| "licenses": "Distribution des licences", | |
| "format_version": "Version du format", | |
| "score": "Score de conformite", | |
| "result_pass": "SUCCES - Le SBOM est conforme", | |
| "result_fail": "ECHEC - Le SBOM presente des problemes", | |
| "missing": "Champs manquants", | |
| "recommendations": "Recommandations", | |
| "ntia_title": "Checklist NTIA Minimum Elements", | |
| "reg_title": "Exigences SBOM par regulation", | |
| "cmp_title": "Comparaison des formats SBOM", | |
| "tools_title": "Ecosysteme des outils SBOM", | |
| "res_title": "Ressources et articles", | |
| "footer": "Propulse par", | |
| }, | |
| "en": { | |
| "title": "SBOM Generator & Validator", | |
| "subtitle": "Create and validate your Software Bill of Materials (SBOM) compliant with CycloneDX & SPDX", | |
| "lang_btn": "Francais", | |
| "tab_gen": "SBOM Generator", | |
| "tab_val": "SBOM Validator", | |
| "tab_reg": "Regulatory Requirements", | |
| "tab_cmp": "Format Comparison", | |
| "tab_tools": "Tool Ecosystem", | |
| "tab_res": "Resources", | |
| "input_label": "Dependency file content", | |
| "input_placeholder": "Paste requirements.txt, package.json, pom.xml or go.mod content here ...", | |
| "format_label": "Output format", | |
| "generate_btn": "Generate SBOM", | |
| "output_label": "Generated SBOM document", | |
| "stats_label": "Statistics", | |
| "val_input_label": "SBOM document to validate", | |
| "val_input_placeholder": "Paste a SBOM document (CycloneDX JSON/XML or SPDX JSON/Tag-Value) here ...", | |
| "validate_btn": "Validate SBOM", | |
| "val_result_label": "Validation result", | |
| "val_details_label": "Details and recommendations", | |
| "no_input": "Please provide dependency file content.", | |
| "no_sbom": "Please provide an SBOM document to validate.", | |
| "parsed_ok": "components detected", | |
| "gen_ok": "SBOM generated successfully", | |
| "components": "Components", | |
| "licenses": "License distribution", | |
| "format_version": "Format version", | |
| "score": "Compliance score", | |
| "result_pass": "PASS - SBOM is compliant", | |
| "result_fail": "FAIL - SBOM has issues", | |
| "missing": "Missing fields", | |
| "recommendations": "Recommendations", | |
| "ntia_title": "NTIA Minimum Elements Checklist", | |
| "reg_title": "SBOM requirements by regulation", | |
| "cmp_title": "SBOM Format Comparison", | |
| "tools_title": "SBOM Tool Ecosystem", | |
| "res_title": "Resources and articles", | |
| "footer": "Powered by", | |
| }, | |
| } | |
| KNOWN_LICENSES = { | |
| "MIT": "MIT", "Apache-2.0": "Apache-2.0", "BSD-3-Clause": "BSD-3-Clause", | |
| "BSD-2-Clause": "BSD-2-Clause", "GPL-3.0-only": "GPL-3.0-only", | |
| "GPL-2.0-only": "GPL-2.0-only", "LGPL-3.0-only": "LGPL-3.0-only", | |
| "ISC": "ISC", "MPL-2.0": "MPL-2.0", "AGPL-3.0-only": "AGPL-3.0-only", | |
| "Unlicense": "Unlicense", "0BSD": "0BSD", | |
| } | |
| DEFAULT_LICENSE_MAP = { | |
| "flask": "BSD-3-Clause", "django": "BSD-3-Clause", "requests": "Apache-2.0", | |
| "numpy": "BSD-3-Clause", "pandas": "BSD-3-Clause", "fastapi": "MIT", | |
| "gradio": "Apache-2.0", "express": "MIT", "react": "MIT", "lodash": "MIT", | |
| "axios": "MIT", "vue": "MIT", "typescript": "Apache-2.0", "webpack": "MIT", | |
| "spring-boot-starter-web": "Apache-2.0", "junit-jupiter": "EPL-2.0", | |
| "lombok": "MIT", "gin": "MIT", "cobra": "Apache-2.0", "logrus": "MIT", | |
| "pytest": "MIT", "black": "MIT", "uvicorn": "BSD-3-Clause", | |
| "sqlalchemy": "MIT", "pydantic": "MIT", "boto3": "Apache-2.0", | |
| "tensorflow": "Apache-2.0", "torch": "BSD-3-Clause", "scipy": "BSD-3-Clause", | |
| "scikit-learn": "BSD-3-Clause", "matplotlib": "PSF-2.0", | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Parsers | |
| # --------------------------------------------------------------------------- | |
| def _sim_hash(name: str, version: str) -> str: | |
| return hashlib.sha256(f"{name}@{version}".encode()).hexdigest() | |
| def _guess_license(name: str) -> str: | |
| key = name.lower().replace("_", "-") | |
| return DEFAULT_LICENSE_MAP.get(key, "MIT") | |
| def parse_requirements(text: str): | |
| components = [] | |
| for line in text.strip().splitlines(): | |
| line = line.strip() | |
| if not line or line.startswith("#"): | |
| continue | |
| m = re.match(r"^([A-Za-z0-9_\-\.]+)\s*[=~<>!]=?\s*([0-9][^\s,;]*)?", line) | |
| if m: | |
| name = m.group(1) | |
| version = m.group(2) or "0.0.0" | |
| components.append({"name": name, "version": version, | |
| "license": _guess_license(name), "type": "library", | |
| "purl": f"pkg:pypi/{name}@{version}"}) | |
| return components | |
| def parse_package_json(text: str): | |
| components = [] | |
| try: | |
| data = json.loads(text) | |
| except json.JSONDecodeError: | |
| return components | |
| deps = {} | |
| deps.update(data.get("dependencies", {})) | |
| deps.update(data.get("devDependencies", {})) | |
| for name, ver in deps.items(): | |
| version = re.sub(r"[^0-9\.]", "", ver) or "0.0.0" | |
| components.append({"name": name, "version": version, | |
| "license": _guess_license(name), "type": "library", | |
| "purl": f"pkg:npm/{name}@{version}"}) | |
| return components | |
| def parse_pom_xml(text: str): | |
| components = [] | |
| pattern = r"<dependency>\s*<groupId>([^<]+)</groupId>\s*<artifactId>([^<]+)</artifactId>\s*<version>([^<]+)</version>" | |
| for m in re.finditer(pattern, text, re.DOTALL): | |
| group, artifact, version = m.group(1), m.group(2), m.group(3) | |
| components.append({"name": f"{group}:{artifact}", "version": version, | |
| "license": _guess_license(artifact), "type": "library", | |
| "purl": f"pkg:maven/{group}/{artifact}@{version}"}) | |
| return components | |
| def parse_go_mod(text: str): | |
| components = [] | |
| for line in text.strip().splitlines(): | |
| line = line.strip() | |
| m = re.match(r"^([\w\.\-/]+)\s+(v?[\d\.]+)", line) | |
| if m and "/" in m.group(1): | |
| mod = m.group(1) | |
| version = m.group(2) | |
| short = mod.split("/")[-1] | |
| components.append({"name": mod, "version": version, | |
| "license": _guess_license(short), "type": "library", | |
| "purl": f"pkg:golang/{mod}@{version}"}) | |
| return components | |
| def auto_parse(text: str): | |
| text = text.strip() | |
| if not text: | |
| return [] | |
| if text.lstrip().startswith("{"): | |
| comps = parse_package_json(text) | |
| if comps: | |
| return comps | |
| if "<dependency>" in text: | |
| comps = parse_pom_xml(text) | |
| if comps: | |
| return comps | |
| if "module " in text or "require (" in text or "go " in text: | |
| comps = parse_go_mod(text) | |
| if comps: | |
| return comps | |
| return parse_requirements(text) | |
| # --------------------------------------------------------------------------- | |
| # SBOM generators | |
| # --------------------------------------------------------------------------- | |
| NOW_ISO = lambda: datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") | |
| def gen_cyclonedx_json(components): | |
| bom = { | |
| "bomFormat": "CycloneDX", | |
| "specVersion": "1.5", | |
| "serialNumber": f"urn:uuid:{uuid.uuid4()}", | |
| "version": 1, | |
| "metadata": { | |
| "timestamp": NOW_ISO(), | |
| "tools": [{"vendor": "AYI-NEDJIMI Consultants", "name": "sbom-generator", "version": "1.0.0"}], | |
| "component": {"type": "application", "name": "my-application", "version": "1.0.0"}, | |
| }, | |
| "components": [], | |
| "dependencies": [{"ref": "my-application", "dependsOn": []}], | |
| } | |
| for c in components: | |
| comp = { | |
| "type": c["type"], | |
| "name": c["name"], | |
| "version": c["version"], | |
| "purl": c["purl"], | |
| "licenses": [{"license": {"id": c["license"]}}], | |
| "hashes": [{"alg": "SHA-256", "content": _sim_hash(c["name"], c["version"])}], | |
| } | |
| bom["components"].append(comp) | |
| bom["dependencies"][0]["dependsOn"].append(c["purl"]) | |
| return json.dumps(bom, indent=2) | |
| def gen_cyclonedx_xml(components): | |
| ts = NOW_ISO() | |
| serial = f"urn:uuid:{uuid.uuid4()}" | |
| lines = [ | |
| '<?xml version="1.0" encoding="UTF-8"?>', | |
| '<bom xmlns="http://cyclonedx.org/schema/bom/1.5" serialNumber="' + serial + '" version="1">', | |
| " <metadata>", | |
| f" <timestamp>{ts}</timestamp>", | |
| " <tools><tool><vendor>AYI-NEDJIMI Consultants</vendor><name>sbom-generator</name><version>1.0.0</version></tool></tools>", | |
| ' <component type="application"><name>my-application</name><version>1.0.0</version></component>', | |
| " </metadata>", | |
| " <components>", | |
| ] | |
| refs = [] | |
| for c in components: | |
| h = _sim_hash(c["name"], c["version"]) | |
| lines.append(f' <component type="{c["type"]}">') | |
| lines.append(f" <name>{c['name']}</name>") | |
| lines.append(f" <version>{c['version']}</version>") | |
| lines.append(f" <purl>{c['purl']}</purl>") | |
| lines.append(f' <licenses><license><id>{c["license"]}</id></license></licenses>') | |
| lines.append(f' <hashes><hash alg="SHA-256">{h}</hash></hashes>') | |
| lines.append(" </component>") | |
| refs.append(c["purl"]) | |
| lines.append(" </components>") | |
| lines.append(" <dependencies>") | |
| lines.append(' <dependency ref="my-application">') | |
| for r in refs: | |
| lines.append(f' <dependency ref="{r}"/>') | |
| lines.append(" </dependency>") | |
| lines.append(" </dependencies>") | |
| lines.append("</bom>") | |
| return "\n".join(lines) | |
| def gen_spdx_json(components): | |
| doc_ns = f"https://spdx.org/spdxdocs/sbom-generator-{uuid.uuid4()}" | |
| doc = { | |
| "spdxVersion": "SPDX-2.3", | |
| "dataLicense": "CC0-1.0", | |
| "SPDXID": "SPDXRef-DOCUMENT", | |
| "name": "my-application", | |
| "documentNamespace": doc_ns, | |
| "creationInfo": { | |
| "created": NOW_ISO(), | |
| "creators": ["Tool: sbom-generator-1.0.0", "Organization: AYI-NEDJIMI Consultants"], | |
| }, | |
| "packages": [], | |
| "relationships": [], | |
| } | |
| for i, c in enumerate(components): | |
| spdx_id = f"SPDXRef-Package-{i+1}" | |
| pkg = { | |
| "SPDXID": spdx_id, | |
| "name": c["name"], | |
| "versionInfo": c["version"], | |
| "downloadLocation": "https://example.com/download", | |
| "supplier": "Organization: Open Source", | |
| "licenseConcluded": c["license"], | |
| "licenseDeclared": c["license"], | |
| "copyrightText": "NOASSERTION", | |
| "externalRefs": [ | |
| {"referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": c["purl"]} | |
| ], | |
| "checksums": [{"algorithm": "SHA256", "checksumValue": _sim_hash(c["name"], c["version"])}], | |
| } | |
| doc["packages"].append(pkg) | |
| doc["relationships"].append({ | |
| "spdxElementId": "SPDXRef-DOCUMENT", | |
| "relatedSpdxElement": spdx_id, | |
| "relationshipType": "DESCRIBES", | |
| }) | |
| return json.dumps(doc, indent=2) | |
| def gen_spdx_tv(components): | |
| ts = NOW_ISO() | |
| doc_ns = f"https://spdx.org/spdxdocs/sbom-generator-{uuid.uuid4()}" | |
| lines = [ | |
| "SPDXVersion: SPDX-2.3", | |
| "DataLicense: CC0-1.0", | |
| "SPDXID: SPDXRef-DOCUMENT", | |
| "DocumentName: my-application", | |
| f"DocumentNamespace: {doc_ns}", | |
| f"Creator: Tool: sbom-generator-1.0.0", | |
| f"Creator: Organization: AYI-NEDJIMI Consultants", | |
| f"Created: {ts}", | |
| "", | |
| ] | |
| for i, c in enumerate(components): | |
| sid = f"SPDXRef-Package-{i+1}" | |
| lines += [ | |
| f"PackageName: {c['name']}", | |
| f"SPDXID: {sid}", | |
| f"PackageVersion: {c['version']}", | |
| "PackageDownloadLocation: https://example.com/download", | |
| "PackageSupplier: Organization: Open Source", | |
| f"PackageLicenseConcluded: {c['license']}", | |
| f"PackageLicenseDeclared: {c['license']}", | |
| "PackageCopyrightText: NOASSERTION", | |
| f"PackageChecksum: SHA256: {_sim_hash(c['name'], c['version'])}", | |
| f"ExternalRef: PACKAGE-MANAGER purl {c['purl']}", | |
| f"Relationship: SPDXRef-DOCUMENT DESCRIBES {sid}", | |
| "", | |
| ] | |
| return "\n".join(lines) | |
| FORMAT_FUNCS = { | |
| "CycloneDX JSON": gen_cyclonedx_json, | |
| "CycloneDX XML": gen_cyclonedx_xml, | |
| "SPDX JSON": gen_spdx_json, | |
| "SPDX Tag-Value": gen_spdx_tv, | |
| } | |
| FORMAT_VERSIONS = { | |
| "CycloneDX JSON": "CycloneDX 1.5", | |
| "CycloneDX XML": "CycloneDX 1.5", | |
| "SPDX JSON": "SPDX 2.3", | |
| "SPDX Tag-Value": "SPDX 2.3", | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Generate handler | |
| # --------------------------------------------------------------------------- | |
| def generate_sbom(text: str, fmt: str, lang: str): | |
| t = TR[lang] | |
| if not text or not text.strip(): | |
| return t["no_input"], "", None | |
| components = auto_parse(text) | |
| if not components: | |
| return t["no_input"], "", None | |
| sbom_text = FORMAT_FUNCS[fmt](components) | |
| lic_dist = {} | |
| for c in components: | |
| lic_dist[c["license"]] = lic_dist.get(c["license"], 0) + 1 | |
| lic_lines = ", ".join(f"{k}: {v}" for k, v in sorted(lic_dist.items(), key=lambda x: -x[1])) | |
| stats = ( | |
| f"{t['gen_ok']}\n\n" | |
| f"**{t['components']}**: {len(components)} {t['parsed_ok']}\n\n" | |
| f"**{t['licenses']}**: {lic_lines}\n\n" | |
| f"**{t['format_version']}**: {FORMAT_VERSIONS[fmt]}" | |
| ) | |
| ext = {"CycloneDX JSON": ".json", "CycloneDX XML": ".xml", | |
| "SPDX JSON": ".json", "SPDX Tag-Value": ".spdx"}[fmt] | |
| tmp_path = f"/tmp/sbom{ext}" | |
| with open(tmp_path, "w") as f: | |
| f.write(sbom_text) | |
| return sbom_text, stats, tmp_path | |
| # --------------------------------------------------------------------------- | |
| # Validator | |
| # --------------------------------------------------------------------------- | |
| def validate_sbom(text: str, lang: str): | |
| t = TR[lang] | |
| if not text or not text.strip(): | |
| return t["no_sbom"], "" | |
| text = text.strip() | |
| issues = [] | |
| checks = {} | |
| detected = "Unknown" | |
| components_found = 0 | |
| score = 0 | |
| total = 10 | |
| # Try CycloneDX JSON | |
| if text.lstrip().startswith("{") and "bomFormat" in text: | |
| detected = "CycloneDX JSON" | |
| try: | |
| data = json.loads(text) | |
| except json.JSONDecodeError: | |
| return "Invalid JSON", "" | |
| checks["bomFormat"] = data.get("bomFormat") == "CycloneDX" | |
| checks["specVersion"] = data.get("specVersion", "") in ("1.4", "1.5", "1.6") | |
| checks["serialNumber"] = bool(data.get("serialNumber")) | |
| checks["metadata.timestamp"] = bool(data.get("metadata", {}).get("timestamp")) | |
| checks["metadata.tools"] = bool(data.get("metadata", {}).get("tools")) | |
| checks["components"] = isinstance(data.get("components"), list) and len(data.get("components", [])) > 0 | |
| components_found = len(data.get("components", [])) | |
| comps = data.get("components", []) | |
| all_have_name = all("name" in c for c in comps) | |
| all_have_version = all("version" in c for c in comps) | |
| all_have_purl = all("purl" in c for c in comps) | |
| all_have_license = all(c.get("licenses") for c in comps) | |
| all_have_hash = all(c.get("hashes") for c in comps) | |
| checks["component.name"] = all_have_name | |
| checks["component.version"] = all_have_version | |
| checks["component.purl"] = all_have_purl | |
| checks["component.license"] = all_have_license | |
| checks["component.hash"] = all_have_hash | |
| checks["dependencies"] = isinstance(data.get("dependencies"), list) | |
| # Try SPDX JSON | |
| elif text.lstrip().startswith("{") and "spdxVersion" in text: | |
| detected = "SPDX JSON" | |
| try: | |
| data = json.loads(text) | |
| except json.JSONDecodeError: | |
| return "Invalid JSON", "" | |
| checks["spdxVersion"] = data.get("spdxVersion", "").startswith("SPDX-2.") | |
| checks["dataLicense"] = data.get("dataLicense") == "CC0-1.0" | |
| checks["SPDXID"] = data.get("SPDXID") == "SPDXRef-DOCUMENT" | |
| checks["documentNamespace"] = bool(data.get("documentNamespace")) | |
| checks["creationInfo"] = bool(data.get("creationInfo", {}).get("created")) | |
| checks["creators"] = bool(data.get("creationInfo", {}).get("creators")) | |
| pkgs = data.get("packages", []) | |
| components_found = len(pkgs) | |
| checks["packages"] = len(pkgs) > 0 | |
| checks["package.name"] = all("name" in p for p in pkgs) | |
| checks["package.version"] = all("versionInfo" in p for p in pkgs) | |
| checks["package.supplier"] = all("supplier" in p for p in pkgs) | |
| checks["package.license"] = all("licenseConcluded" in p for p in pkgs) | |
| checks["relationships"] = isinstance(data.get("relationships"), list) | |
| # Try SPDX Tag-Value | |
| elif "SPDXVersion:" in text: | |
| detected = "SPDX Tag-Value" | |
| checks["SPDXVersion"] = bool(re.search(r"SPDXVersion:\s*SPDX-2\.\d", text)) | |
| checks["DataLicense"] = "DataLicense: CC0-1.0" in text | |
| checks["SPDXID"] = "SPDXID: SPDXRef-DOCUMENT" in text | |
| checks["DocumentNamespace"] = "DocumentNamespace:" in text | |
| checks["Creator"] = "Creator:" in text | |
| checks["Created"] = "Created:" in text | |
| pkg_names = re.findall(r"PackageName:\s*(.+)", text) | |
| components_found = len(pkg_names) | |
| checks["packages"] = components_found > 0 | |
| checks["PackageVersion"] = "PackageVersion:" in text | |
| checks["PackageSupplier"] = "PackageSupplier:" in text | |
| checks["PackageLicense"] = "PackageLicenseConcluded:" in text | |
| checks["PackageChecksum"] = "PackageChecksum:" in text | |
| # Try CycloneDX XML | |
| elif "<bom" in text and "cyclonedx" in text.lower(): | |
| detected = "CycloneDX XML" | |
| checks["namespace"] = "cyclonedx.org" in text | |
| checks["serialNumber"] = 'serialNumber="' in text | |
| checks["metadata"] = "<metadata>" in text | |
| checks["timestamp"] = "<timestamp>" in text | |
| checks["tools"] = "<tools>" in text | |
| checks["components"] = "<components>" in text | |
| comp_matches = re.findall(r"<component", text) | |
| components_found = len(comp_matches) | |
| checks["component.name"] = "<name>" in text | |
| checks["component.version"] = "<version>" in text | |
| checks["component.purl"] = "<purl>" in text | |
| checks["component.license"] = "<license>" in text or "<licenses>" in text | |
| checks["component.hash"] = "<hash" in text | |
| else: | |
| return "Unrecognized SBOM format. Supported: CycloneDX JSON/XML, SPDX JSON/Tag-Value.", "" | |
| passed = sum(1 for v in checks.values() if v) | |
| total = len(checks) | |
| pct = int(100 * passed / total) if total else 0 | |
| is_pass = pct >= 80 | |
| # NTIA minimum elements | |
| ntia = { | |
| "Supplier Name": checks.get("package.supplier", checks.get("PackageSupplier", checks.get("metadata.tools", False))), | |
| "Component Name": checks.get("component.name", checks.get("package.name", checks.get("packages", False))), | |
| "Component Version": checks.get("component.version", checks.get("package.version", checks.get("PackageVersion", False))), | |
| "Unique Identifier": checks.get("component.purl", checks.get("SPDXID", checks.get("serialNumber", False))), | |
| "Dependency Relationship": checks.get("dependencies", checks.get("relationships", False)), | |
| "Author of SBOM Data": checks.get("metadata.tools", checks.get("creators", checks.get("Creator", False))), | |
| "Timestamp": checks.get("metadata.timestamp", checks.get("creationInfo", checks.get("Created", False))), | |
| } | |
| # Build result | |
| status_icon = "PASS" if is_pass else "FAIL" | |
| result_msg = t["result_pass"] if is_pass else t["result_fail"] | |
| result = f"**{status_icon}** - {result_msg}\n\n" | |
| result += f"**Format**: {detected}\n" | |
| result += f"**{t['components']}**: {components_found}\n" | |
| result += f"**{t['score']}**: {pct}% ({passed}/{total})\n" | |
| # Details | |
| details = "### Validation checks\n\n" | |
| for k, v in checks.items(): | |
| mark = "PASS" if v else "FAIL" | |
| details += f"- [{mark}] {k}\n" | |
| failed = [k for k, v in checks.items() if not v] | |
| if failed: | |
| details += f"\n### {t['missing']}\n\n" | |
| for f_item in failed: | |
| details += f"- {f_item}\n" | |
| details += f"\n### {t['ntia_title']}\n\n" | |
| for k, v in ntia.items(): | |
| mark = "PASS" if v else "FAIL" | |
| details += f"- [{mark}] {k}\n" | |
| if not is_pass: | |
| details += f"\n### {t['recommendations']}\n\n" | |
| if not checks.get("component.hash", checks.get("PackageChecksum", checks.get("component.hash", True))): | |
| details += "- Add SHA-256 checksums to all components\n" | |
| if not checks.get("component.license", checks.get("package.license", checks.get("PackageLicense", True))): | |
| details += "- Add SPDX license identifiers to all components\n" | |
| if not checks.get("component.purl", checks.get("SPDXID", True)): | |
| details += "- Add Package URLs (purl) for unique identification\n" | |
| if not checks.get("dependencies", checks.get("relationships", True)): | |
| details += "- Include dependency relationships\n" | |
| return result, details | |
| # --------------------------------------------------------------------------- | |
| # Static content builders | |
| # --------------------------------------------------------------------------- | |
| def build_regulations(lang): | |
| if lang == "fr": | |
| return """## Exigences SBOM par regulation | |
| | Regulation | Exigence | Format attendu | Echeance | Sanction | Perimetre | | |
| |---|---|---|---|---|---| | |
| | **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 | | |
| | **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) | | |
| | **DORA** | Gestion des risques lies aux tiers TIC, registre des actifs | Non specifie | Janvier 2025 | Sanctions administratives | Entites financieres de l'UE | | |
| | **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 | | |
| | **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 | | |
| | **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 | | |
| """ | |
| return """## SBOM Requirements by Regulation | |
| | Regulation | Requirement | Expected Format | Deadline | Penalty | Scope | | |
| |---|---|---|---|---|---| | |
| | **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 | | |
| | **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) | | |
| | **DORA** | ICT third-party risk management, asset registry | Not specified | January 2025 | Administrative sanctions | EU financial entities | | |
| | **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 | | |
| | **FDA (Medical Devices)** | Mandatory SBOM for connected medical devices | CycloneDX or SPDX | In force (2023) | Market authorization refusal | Medical devices with software components | | |
| | **PCI DSS 4.0** | Software component inventory, vulnerability management | Not specified | March 2025 (future requirements) | PCI non-compliance | Organizations processing payment card data | | |
| """ | |
| def build_comparison(lang): | |
| if lang == "fr": | |
| return """## Comparaison des formats SBOM | |
| | Critere | CycloneDX | SPDX | SWID | | |
| |---|---|---|---| | |
| | **Organisation** | OWASP | Linux Foundation | ISO/IEC 19770-2 | | |
| | **Version actuelle** | 1.6 | 2.3 | 2016 | | |
| | **Formats** | JSON, XML, Protobuf | JSON, RDF, Tag-Value, XLSX | XML | | |
| | **Licence du schema** | Apache-2.0 | CC-BY-3.0 | ISO Standard | | |
| | **Focus principal** | Securite, vulnerabilites | Conformite licences | Identification logicielle | | |
| | **Composants imbriques** | Oui (natif) | Oui (via relations) | Limite | | |
| | **Vulnerabilites (VEX)** | Integre (CycloneDX VEX) | Via SPDX Security | Non | | |
| | **Services & API** | Oui | Non | Non | | |
| | **Licences SPDX** | Oui | Oui (natif) | Non | | |
| | **PURL (Package URL)** | Oui (natif) | Oui | Non | | |
| | **Signature numerique** | Oui (JSF, XML Sig) | Non | Oui (XML Sig) | | |
| | **Machine readable** | Oui | Oui | Oui | | |
| | **Adoption industrie** | Elevee (securite) | Elevee (open source) | Faible | | |
| | **NTIA conforme** | Oui | Oui | Partiel | | |
| | **CRA conforme** | Oui | Oui | Non recommande | | |
| ### Recommandations par cas d'usage | |
| - **Securite et gestion des vulnerabilites** : CycloneDX (VEX integre, focus securite) | |
| - **Conformite open source et licences** : SPDX (standard Linux Foundation, focus licences) | |
| - **Inventaire IT et gestion des actifs** : SWID (standard ISO, identification) | |
| - **Conformite CRA / NIS 2** : CycloneDX ou SPDX (les deux sont acceptes) | |
| - **Marches publics US** : SPDX ou CycloneDX (NTIA minimum elements) | |
| """ | |
| return """## SBOM Format Comparison | |
| | Criteria | CycloneDX | SPDX | SWID | | |
| |---|---|---|---| | |
| | **Organization** | OWASP | Linux Foundation | ISO/IEC 19770-2 | | |
| | **Current version** | 1.6 | 2.3 | 2016 | | |
| | **Formats** | JSON, XML, Protobuf | JSON, RDF, Tag-Value, XLSX | XML | | |
| | **Schema license** | Apache-2.0 | CC-BY-3.0 | ISO Standard | | |
| | **Primary focus** | Security, vulnerabilities | License compliance | Software identification | | |
| | **Nested components** | Yes (native) | Yes (via relationships) | Limited | | |
| | **Vulnerabilities (VEX)** | Built-in (CycloneDX VEX) | Via SPDX Security | No | | |
| | **Services & API** | Yes | No | No | | |
| | **SPDX licenses** | Yes | Yes (native) | No | | |
| | **PURL (Package URL)** | Yes (native) | Yes | No | | |
| | **Digital signature** | Yes (JSF, XML Sig) | No | Yes (XML Sig) | | |
| | **Machine readable** | Yes | Yes | Yes | | |
| | **Industry adoption** | High (security) | High (open source) | Low | | |
| | **NTIA compliant** | Yes | Yes | Partial | | |
| | **CRA compliant** | Yes | Yes | Not recommended | | |
| ### Recommendations by Use Case | |
| - **Security and vulnerability management**: CycloneDX (built-in VEX, security focus) | |
| - **Open source and license compliance**: SPDX (Linux Foundation standard, license focus) | |
| - **IT inventory and asset management**: SWID (ISO standard, identification) | |
| - **CRA / NIS 2 compliance**: CycloneDX or SPDX (both accepted) | |
| - **US federal procurement**: SPDX or CycloneDX (NTIA minimum elements) | |
| """ | |
| def build_tools(lang): | |
| if lang == "fr": | |
| header = "## Ecosysteme des outils SBOM\n\n" | |
| else: | |
| header = "## SBOM Tool Ecosystem\n\n" | |
| cat_gen = "Generation" if lang == "fr" else "Generation" | |
| cat_ana = "Analyse" if lang == "fr" else "Analysis" | |
| cat_scan = "Scan de vulnerabilites" if lang == "fr" else "Vulnerability Scanning" | |
| cat_mgmt = "Gestion" if lang == "fr" else "Management" | |
| tools = [ | |
| (cat_gen, "Syft", "SBOM generation from container images and filesystems", "CycloneDX, SPDX", "Open Source (Apache-2.0)", "https://github.com/anchore/syft"), | |
| (cat_gen, "cdxgen", "CycloneDX SBOM generator for many languages", "CycloneDX", "Open Source (Apache-2.0)", "https://github.com/CycloneDX/cdxgen"), | |
| (cat_gen, "Microsoft sbom-tool", "SBOM generation tool by Microsoft", "SPDX", "Open Source (MIT)", "https://github.com/microsoft/sbom-tool"), | |
| (cat_gen, "SPDX Tools", "Official SPDX format tools and libraries", "SPDX", "Open Source", "https://github.com/spdx/tools-java"), | |
| (cat_ana, "Grype", "Vulnerability scanner for container images and SBOM", "CycloneDX, SPDX", "Open Source (Apache-2.0)", "https://github.com/anchore/grype"), | |
| (cat_ana, "Dependency-Track", "Intelligent component analysis platform", "CycloneDX", "Open Source (Apache-2.0)", "https://dependencytrack.org/"), | |
| (cat_ana, "GUAC", "Graph for Understanding Artifact Composition", "CycloneDX, SPDX", "Open Source (Apache-2.0)", "https://guac.sh/"), | |
| (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"), | |
| (cat_scan, "Trivy", "All-in-one security scanner (containers, IaC, SBOM)", "CycloneDX, SPDX", "Open Source (Apache-2.0)", "https://trivy.dev/"), | |
| (cat_scan, "Snyk", "Developer-first security platform", "CycloneDX, SPDX", "Freemium / Enterprise", "https://snyk.io/"), | |
| (cat_scan, "Black Duck", "Software composition analysis (SCA)", "SPDX, CycloneDX", "Commercial", "https://www.blackducksoftware.com/"), | |
| (cat_scan, "Mend (WhiteSource)", "Open source security and compliance", "CycloneDX, SPDX", "Freemium / Enterprise", "https://www.mend.io/"), | |
| (cat_mgmt, "OWASP Dependency-Track", "Component analysis and SBOM management", "CycloneDX", "Open Source (Apache-2.0)", "https://dependencytrack.org/"), | |
| (cat_mgmt, "JFrog Xray", "Universal artifact analysis and SBOM management", "CycloneDX, SPDX", "Commercial", "https://jfrog.com/xray/"), | |
| (cat_mgmt, "Sonatype Nexus Lifecycle", "Software supply chain management", "CycloneDX, SPDX", "Commercial", "https://www.sonatype.com/products/open-source-security-dependency-management"), | |
| ] | |
| if lang == "fr": | |
| table = "| Categorie | Outil | Description | Formats | Tarification | Lien |\n" | |
| else: | |
| table = "| Category | Tool | Description | Formats | Pricing | Link |\n" | |
| table += "|---|---|---|---|---|---|\n" | |
| for cat, name, desc, fmts, price, link in tools: | |
| table += f"| {cat} | **{name}** | {desc} | {fmts} | {price} | [Site]({link}) |\n" | |
| return header + table | |
| def build_resources(lang): | |
| if lang == "fr": | |
| return """## Ressources et articles | |
| ### AYI-NEDJIMI Consultants - Expertise conformite et cybersecurite | |
| - [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. | |
| - [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. | |
| - [AYI-NEDJIMI Consultants - Accueil](https://ayinedjimi-consultants.fr) - Cabinet de conseil specialise en conformite reglementaire, cybersecurite et gouvernance des donnees. | |
| ### Standards et specifications | |
| - [CycloneDX Specification](https://cyclonedx.org/specification/overview/) - Specification officielle du format CycloneDX (OWASP). | |
| - [SPDX Specification](https://spdx.github.io/spdx-spec/v2.3/) - Specification officielle du format SPDX (Linux Foundation). | |
| - [NTIA SBOM Minimum Elements](https://www.ntia.gov/page/software-bill-materials) - Elements minimaux NTIA pour les SBOM. | |
| - [EU Cyber Resilience Act](https://digital-strategy.ec.europa.eu/en/policies/cyber-resilience-act) - Texte officiel du CRA europeen. | |
| ### Communaute | |
| - [OWASP Software Component Verification Standard](https://owasp.org/www-project-software-component-verification-standard/) - Standard OWASP pour la verification des composants logiciels. | |
| - [OpenSSF Scorecard](https://securityscorecards.dev/) - Evaluation automatisee de la securite des projets open source. | |
| """ | |
| return """## Resources and Articles | |
| ### AYI-NEDJIMI Consultants - Compliance and Cybersecurity Expertise | |
| - [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. | |
| - [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. | |
| - [AYI-NEDJIMI Consultants - Home](https://ayinedjimi-consultants.fr) - Consulting firm specialized in regulatory compliance, cybersecurity, and data governance. | |
| ### Standards and Specifications | |
| - [CycloneDX Specification](https://cyclonedx.org/specification/overview/) - Official CycloneDX format specification (OWASP). | |
| - [SPDX Specification](https://spdx.github.io/spdx-spec/v2.3/) - Official SPDX format specification (Linux Foundation). | |
| - [NTIA SBOM Minimum Elements](https://www.ntia.gov/page/software-bill-materials) - NTIA minimum elements for SBOMs. | |
| - [EU Cyber Resilience Act](https://digital-strategy.ec.europa.eu/en/policies/cyber-resilience-act) - Official EU CRA text. | |
| ### Community | |
| - [OWASP Software Component Verification Standard](https://owasp.org/www-project-software-component-verification-standard/) - OWASP standard for software component verification. | |
| - [OpenSSF Scorecard](https://securityscorecards.dev/) - Automated security assessment for open source projects. | |
| """ | |
| # --------------------------------------------------------------------------- | |
| # Gradio UI | |
| # --------------------------------------------------------------------------- | |
| FOOTER_HTML = ( | |
| '<div style="text-align:center; margin-top:24px; padding:16px; ' | |
| 'border-top:1px solid #ddd; color:#666; font-size:0.9em;">' | |
| 'Powered by <a href="https://ayinedjimi-consultants.fr" target="_blank">' | |
| 'AYI-NEDJIMI Consultants</a> | ' | |
| '<a href="https://ayinedjimi-consultants.fr/articles/conformite/' | |
| 'sbom-2026-obligation-securite.html" target="_blank">SBOM 2026 Guide</a> | ' | |
| '<a href="https://ayinedjimi-consultants.fr/articles/conformite/' | |
| 'cyber-resilience-act-2026.html" target="_blank">Cyber Resilience Act</a>' | |
| '</div>' | |
| ) | |
| def build_app(): | |
| with gr.Blocks( | |
| title="SBOM Generator & Validator", | |
| theme=gr.themes.Soft(), | |
| css=".footer-link a { color: #2563eb; text-decoration: none; }" | |
| ) as demo: | |
| lang_state = gr.State("fr") | |
| gr.Markdown("# Generateur & Validateur SBOM") | |
| gr.Markdown("Creez et validez vos Software Bill of Materials (SBOM) conformes CycloneDX & SPDX") | |
| lang_btn = gr.Button("English", variant="secondary", elem_id="lang-btn") | |
| # ------------------------------------------------------------------ | |
| # Tab 1: Generator | |
| # ------------------------------------------------------------------ | |
| with gr.Tab("Generateur SBOM"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| gen_input = gr.Textbox( | |
| label="Contenu du fichier de dependances", | |
| placeholder="Collez ici le contenu de requirements.txt, package.json, pom.xml ou go.mod ...", | |
| lines=12, | |
| ) | |
| gen_format = gr.Dropdown( | |
| choices=["CycloneDX JSON", "CycloneDX XML", "SPDX JSON", "SPDX Tag-Value"], | |
| value="CycloneDX JSON", | |
| label="Format de sortie", | |
| ) | |
| gen_btn = gr.Button("Generer le SBOM", variant="primary") | |
| with gr.Column(): | |
| gen_stats = gr.Markdown(label="Statistiques") | |
| gen_output = gr.Textbox(label="Document SBOM genere", lines=20, show_copy_button=True) | |
| gen_file = gr.File(label="Telecharger le SBOM") | |
| gen_btn.click( | |
| fn=generate_sbom, | |
| inputs=[gen_input, gen_format, lang_state], | |
| outputs=[gen_output, gen_stats, gen_file], | |
| ) | |
| # ------------------------------------------------------------------ | |
| # Tab 2: Validator | |
| # ------------------------------------------------------------------ | |
| with gr.Tab("Validateur SBOM"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| val_input = gr.Textbox( | |
| label="Document SBOM a valider", | |
| placeholder="Collez ici un document SBOM (CycloneDX JSON/XML ou SPDX JSON/Tag-Value) ...", | |
| lines=15, | |
| ) | |
| val_btn = gr.Button("Valider le SBOM", variant="primary") | |
| with gr.Column(): | |
| val_result = gr.Markdown(label="Resultat de la validation") | |
| val_details = gr.Markdown(label="Details et recommandations") | |
| val_btn.click( | |
| fn=validate_sbom, | |
| inputs=[val_input, lang_state], | |
| outputs=[val_result, val_details], | |
| ) | |
| # ------------------------------------------------------------------ | |
| # Tab 3: Regulations | |
| # ------------------------------------------------------------------ | |
| with gr.Tab("Exigences reglementaires"): | |
| reg_md = gr.Markdown(value=build_regulations("fr")) | |
| # ------------------------------------------------------------------ | |
| # Tab 4: Format Comparison | |
| # ------------------------------------------------------------------ | |
| with gr.Tab("Comparaison de formats"): | |
| cmp_md = gr.Markdown(value=build_comparison("fr")) | |
| # ------------------------------------------------------------------ | |
| # Tab 5: Tool Ecosystem | |
| # ------------------------------------------------------------------ | |
| with gr.Tab("Ecosysteme outils"): | |
| tools_md = gr.Markdown(value=build_tools("fr")) | |
| # ------------------------------------------------------------------ | |
| # Tab 6: Resources | |
| # ------------------------------------------------------------------ | |
| with gr.Tab("Ressources"): | |
| res_md = gr.Markdown(value=build_resources("fr")) | |
| # ------------------------------------------------------------------ | |
| # Footer | |
| # ------------------------------------------------------------------ | |
| gr.HTML(FOOTER_HTML) | |
| # ------------------------------------------------------------------ | |
| # Language toggle | |
| # ------------------------------------------------------------------ | |
| def toggle_language(current_lang): | |
| new_lang = "en" if current_lang == "fr" else "fr" | |
| t = TR[new_lang] | |
| return ( | |
| new_lang, | |
| t["lang_btn"], | |
| build_regulations(new_lang), | |
| build_comparison(new_lang), | |
| build_tools(new_lang), | |
| build_resources(new_lang), | |
| ) | |
| lang_btn.click( | |
| fn=toggle_language, | |
| inputs=[lang_state], | |
| outputs=[lang_state, lang_btn, reg_md, cmp_md, tools_md, res_md], | |
| ) | |
| return demo | |
| if __name__ == "__main__": | |
| demo = build_app() | |
| demo.launch(server_name="0.0.0.0", server_port=7860) | |