Coverage for src/alprina_cli/sbom_command.py: 0%
98 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +0100
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +0100
1"""
2SBOM Command - Software Bill of Materials generation.
3Generates SBOMs in CycloneDX and/or SPDX formats.
4"""
6from pathlib import Path
7from rich.console import Console
8from rich.panel import Panel
9from rich.table import Table
10from loguru import logger
12from .services.sbom_generator import get_sbom_generator
14console = Console()
17def sbom_command(
18 project_path: str,
19 format: str = "cyclonedx",
20 output: str = None,
21 output_format: str = "json"
22):
23 """
24 Generate Software Bill of Materials (SBOM).
26 Args:
27 project_path: Path to project directory
28 format: cyclonedx, spdx, or both
29 output: Output file path (optional)
30 output_format: json, xml, yaml, or tag-value
31 """
32 console.print(Panel(
33 f"📦 Software Bill of Materials Generator\n\n"
34 f"Project: [bold]{project_path}[/bold]\n"
35 f"Format: [cyan]{format.upper()}[/cyan]\n"
36 f"Output Format: [cyan]{output_format}[/cyan]",
37 title="Alprina SBOM",
38 border_style="cyan"
39 ))
41 generator = get_sbom_generator()
43 try:
44 if format.lower() == "cyclonedx":
45 _generate_cyclonedx(generator, project_path, output, output_format)
46 elif format.lower() == "spdx":
47 _generate_spdx(generator, project_path, output, output_format)
48 elif format.lower() == "both":
49 _generate_both(generator, project_path)
50 else:
51 console.print(f"[red]Unknown format: {format}[/red]")
52 console.print("Valid formats: cyclonedx, spdx, both")
53 return
55 except Exception as e:
56 console.print(f"[red]SBOM generation failed: {e}[/red]")
57 logger.error(f"SBOM error: {e}", exc_info=True)
60def _generate_cyclonedx(generator, project_path: str, output: str, output_format: str):
61 """Generate CycloneDX SBOM."""
62 console.print("\n[bold]🔄 Generating CycloneDX SBOM...[/bold]")
63 console.print("[dim]This may take a minute for large projects...[/dim]\n")
65 result = generator.generate_cyclonedx(project_path, output, output_format)
67 if not result["success"]:
68 _handle_error(result)
69 return
71 console.print("[green]✓ CycloneDX SBOM generated successfully![/green]\n")
73 # Show summary
74 _display_summary("CycloneDX 1.5 (OWASP Security-Focused)", result)
77def _generate_spdx(generator, project_path: str, output: str, output_format: str):
78 """Generate SPDX SBOM."""
79 console.print("\n[bold]🔄 Generating SPDX SBOM...[/bold]")
80 console.print("[dim]This may take a minute for large projects...[/dim]\n")
82 result = generator.generate_spdx(project_path, output, output_format)
84 if not result["success"]:
85 _handle_error(result)
86 return
88 console.print("[green]✓ SPDX SBOM generated successfully![/green]\n")
90 # Show summary
91 _display_summary("SPDX 2.3 (ISO/IEC 5962:2021)", result)
94def _generate_both(generator, project_path: str):
95 """Generate both CycloneDX and SPDX SBOMs."""
96 console.print("\n[bold]🔄 Generating both CycloneDX and SPDX SBOMs...[/bold]")
97 console.print("[dim]This may take a few minutes...[/dim]\n")
99 results = generator.generate_both(project_path)
101 if not results["success"]:
102 console.print("[yellow]⚠️ Some SBOMs failed to generate[/yellow]\n")
104 # Show results for each format
105 for format_result in results["formats"]:
106 format_name = format_result.get("format", "Unknown")
108 if format_result["success"]:
109 console.print(f"[green]✓ {format_name} SBOM generated![/green]")
110 _display_summary(format_name, format_result)
111 console.print()
112 else:
113 console.print(f"[red]✗ {format_name} SBOM failed[/red]")
114 console.print(f" Error: {format_result.get('error', 'Unknown error')}\n")
117def _display_summary(format_name: str, result: dict):
118 """Display SBOM summary table."""
119 summary = result.get("summary", {})
121 table = Table(title=f"{format_name} Summary", show_header=False, box=None)
123 table.add_row("📄 Output File:", f"[cyan]{result.get('output_file')}[/cyan]")
124 table.add_row("📊 Format:", f"[cyan]{result.get('output_format', 'json')}[/cyan]")
126 if "iso_standard" in result:
127 table.add_row("🏆 Standard:", f"[cyan]{result['iso_standard']}[/cyan]")
129 # Format-specific metrics
130 if "total_components" in summary:
131 table.add_row("📦 Components:", f"[bold]{summary['total_components']}[/bold]")
132 if summary.get("direct_dependencies"):
133 table.add_row(" Direct Dependencies:", str(summary["direct_dependencies"]))
134 if summary.get("transitive_dependencies"):
135 table.add_row(" Transitive Dependencies:", str(summary["transitive_dependencies"]))
137 if "total_packages" in summary:
138 table.add_row("📦 Packages:", f"[bold]{summary['total_packages']}[/bold]")
140 if summary.get("files_analyzed"):
141 table.add_row("📁 Files Analyzed:", str(summary["files_analyzed"]))
143 if summary.get("vulnerabilities"):
144 vuln_color = "red" if summary["vulnerabilities"] > 0 else "green"
145 table.add_row(
146 "🚨 Vulnerabilities:",
147 f"[{vuln_color}]{summary['vulnerabilities']}[/{vuln_color}]"
148 )
150 if summary.get("unique_licenses"):
151 table.add_row("📜 Unique Licenses:", str(summary["unique_licenses"]))
153 console.print(table)
155 # Show top licenses
156 if summary.get("licenses"):
157 console.print("\n[bold]📜 Top Licenses:[/bold]")
158 for i, license in enumerate(summary["licenses"][:5], 1):
159 console.print(f" {i}. {license}")
161 if len(summary["licenses"]) > 5:
162 console.print(f" ... and {len(summary['licenses']) - 5} more")
165def _handle_error(result: dict):
166 """Handle SBOM generation errors."""
167 error = result.get("error", "Unknown error")
168 tool = result.get("tool", "tool")
170 console.print(f"[red]✗ SBOM generation failed[/red]")
171 console.print(f"[red]Error: {error}[/red]\n")
173 if "not installed" in error.lower():
174 console.print("[yellow]🔧 Installation Required[/yellow]\n")
176 if "install_command" in result:
177 console.print(f"Install {tool}:")
178 console.print(f" [cyan]{result['install_command']}[/cyan]\n")
180 if "install_url" in result:
181 console.print(f"Documentation: {result['install_url']}")
183 if result.get("description"):
184 console.print(f"\n{result['description']}")