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

1""" 

2SBOM Command - Software Bill of Materials generation. 

3Generates SBOMs in CycloneDX and/or SPDX formats. 

4""" 

5 

6from pathlib import Path 

7from rich.console import Console 

8from rich.panel import Panel 

9from rich.table import Table 

10from loguru import logger 

11 

12from .services.sbom_generator import get_sbom_generator 

13 

14console = Console() 

15 

16 

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

25 

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

40 

41 generator = get_sbom_generator() 

42 

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 

54 

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) 

58 

59 

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") 

64 

65 result = generator.generate_cyclonedx(project_path, output, output_format) 

66 

67 if not result["success"]: 

68 _handle_error(result) 

69 return 

70 

71 console.print("[green]✓ CycloneDX SBOM generated successfully![/green]\n") 

72 

73 # Show summary 

74 _display_summary("CycloneDX 1.5 (OWASP Security-Focused)", result) 

75 

76 

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") 

81 

82 result = generator.generate_spdx(project_path, output, output_format) 

83 

84 if not result["success"]: 

85 _handle_error(result) 

86 return 

87 

88 console.print("[green]✓ SPDX SBOM generated successfully![/green]\n") 

89 

90 # Show summary 

91 _display_summary("SPDX 2.3 (ISO/IEC 5962:2021)", result) 

92 

93 

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") 

98 

99 results = generator.generate_both(project_path) 

100 

101 if not results["success"]: 

102 console.print("[yellow]⚠️ Some SBOMs failed to generate[/yellow]\n") 

103 

104 # Show results for each format 

105 for format_result in results["formats"]: 

106 format_name = format_result.get("format", "Unknown") 

107 

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") 

115 

116 

117def _display_summary(format_name: str, result: dict): 

118 """Display SBOM summary table.""" 

119 summary = result.get("summary", {}) 

120 

121 table = Table(title=f"{format_name} Summary", show_header=False, box=None) 

122 

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]") 

125 

126 if "iso_standard" in result: 

127 table.add_row("🏆 Standard:", f"[cyan]{result['iso_standard']}[/cyan]") 

128 

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"])) 

136 

137 if "total_packages" in summary: 

138 table.add_row("📦 Packages:", f"[bold]{summary['total_packages']}[/bold]") 

139 

140 if summary.get("files_analyzed"): 

141 table.add_row("📁 Files Analyzed:", str(summary["files_analyzed"])) 

142 

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 ) 

149 

150 if summary.get("unique_licenses"): 

151 table.add_row("📜 Unique Licenses:", str(summary["unique_licenses"])) 

152 

153 console.print(table) 

154 

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}") 

160 

161 if len(summary["licenses"]) > 5: 

162 console.print(f" ... and {len(summary['licenses']) - 5} more") 

163 

164 

165def _handle_error(result: dict): 

166 """Handle SBOM generation errors.""" 

167 error = result.get("error", "Unknown error") 

168 tool = result.get("tool", "tool") 

169 

170 console.print(f"[red]✗ SBOM generation failed[/red]") 

171 console.print(f"[red]Error: {error}[/red]\n") 

172 

173 if "not installed" in error.lower(): 

174 console.print("[yellow]🔧 Installation Required[/yellow]\n") 

175 

176 if "install_command" in result: 

177 console.print(f"Install {tool}:") 

178 console.print(f" [cyan]{result['install_command']}[/cyan]\n") 

179 

180 if "install_url" in result: 

181 console.print(f"Documentation: {result['install_url']}") 

182 

183 if result.get("description"): 

184 console.print(f"\n{result['description']}")