Coverage for src/alprina_cli/history.py: 10%

134 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2025-11-14 11:27 +0100

1""" 

2History module for Alprina CLI. 

3Displays scan history and detailed scan results. 

4""" 

5 

6import httpx 

7from typing import Optional 

8from rich.console import Console 

9from rich.table import Table 

10from rich.panel import Panel 

11from rich.syntax import Syntax 

12import json 

13from datetime import datetime 

14 

15from .auth import is_authenticated, get_auth_headers, get_backend_url 

16 

17console = Console() 

18 

19 

20def history_command( 

21 scan_id: Optional[str] = None, 

22 limit: int = 20, 

23 severity: Optional[str] = None, 

24 page: int = 1 

25): 

26 """ 

27 Display scan history or specific scan details. 

28 

29 Args: 

30 scan_id: Specific scan ID to view details 

31 limit: Number of scans to display per page 

32 severity: Filter by severity (critical, high, medium, low, info) 

33 page: Page number for pagination 

34 """ 

35 if not is_authenticated(): 

36 console.print("[red]✗ Please login first: alprina auth login[/red]") 

37 return 

38 

39 try: 

40 if scan_id: 

41 _display_scan_details(scan_id) 

42 else: 

43 _display_scan_list(page=page, limit=limit, severity=severity) 

44 except Exception as e: 

45 console.print(f"[red]✗ Error: {e}[/red]") 

46 

47 

48def _display_scan_list(page: int = 1, limit: int = 20, severity: Optional[str] = None): 

49 """Display list of scans in a table.""" 

50 try: 

51 headers = get_auth_headers() 

52 backend_url = get_backend_url() 

53 

54 # Build query params 

55 params = { 

56 "page": page, 

57 "limit": limit 

58 } 

59 if severity: 

60 params["severity"] = severity.upper() 

61 

62 response = httpx.get( 

63 f"{backend_url}/scans", 

64 headers=headers, 

65 params=params, 

66 timeout=10.0 

67 ) 

68 

69 if response.status_code != 200: 

70 console.print(f"[red]✗ Failed to fetch scans: {response.status_code}[/red]") 

71 return 

72 

73 data = response.json() 

74 scans = data.get("scans", []) 

75 total = data.get("total", 0) 

76 pages = data.get("pages", 0) 

77 

78 if not scans: 

79 console.print("[yellow]No scans found[/yellow]") 

80 console.print("Run [bold]alprina scan <target>[/bold] to create your first scan") 

81 return 

82 

83 # Create table 

84 table = Table(title=f"Scan History (Page {page}/{pages}, Total: {total})") 

85 

86 table.add_column("ID", style="cyan", no_wrap=True, width=12) 

87 table.add_column("Date", style="dim", width=19) 

88 table.add_column("Target", style="bold") 

89 table.add_column("Type", justify="center", width=8) 

90 table.add_column("Findings", justify="right", width=10) 

91 table.add_column("Critical", justify="center", style="red", width=8) 

92 table.add_column("High", justify="center", style="yellow", width=8) 

93 table.add_column("Status", justify="center", width=10) 

94 

95 for scan in scans: 

96 # Format ID (first 8 chars) 

97 scan_id = scan.get("id", "")[:8] 

98 

99 # Format date 

100 created_at = scan.get("created_at", "") 

101 try: 

102 dt = datetime.fromisoformat(created_at.replace("Z", "+00:00")) 

103 date_str = dt.strftime("%Y-%m-%d %H:%M") 

104 except: 

105 date_str = created_at[:16] 

106 

107 # Get data 

108 target = scan.get("target", "") 

109 scan_type = scan.get("scan_type", "") 

110 findings = scan.get("findings_count", 0) 

111 critical = scan.get("critical_count", 0) 

112 high = scan.get("high_count", 0) 

113 status = scan.get("status", "unknown") 

114 

115 # Color code status 

116 status_style = { 

117 "completed": "[green]completed[/green]", 

118 "running": "[yellow]running[/yellow]", 

119 "failed": "[red]failed[/red]" 

120 }.get(status, status) 

121 

122 # Color code critical/high counts 

123 critical_str = f"[red]{critical}[/red]" if critical > 0 else str(critical) 

124 high_str = f"[yellow]{high}[/yellow]" if high > 0 else str(high) 

125 

126 table.add_row( 

127 scan_id, 

128 date_str, 

129 target, 

130 scan_type, 

131 str(findings), 

132 critical_str, 

133 high_str, 

134 status_style 

135 ) 

136 

137 console.print() 

138 console.print(table) 

139 console.print() 

140 

141 # Show pagination info 

142 if pages > 1: 

143 console.print(f"[dim]Showing page {page} of {pages}[/dim]") 

144 if page < pages: 

145 console.print(f"[dim]Next page: alprina history --page {page + 1}[/dim]") 

146 

147 console.print() 

148 console.print("[dim]View details: alprina history --scan-id <ID>[/dim]") 

149 if severity: 

150 console.print(f"[dim]Filtered by: {severity.upper()} severity[/dim]") 

151 

152 except httpx.ConnectError: 

153 console.print(f"[red]✗ Could not connect to backend[/red]") 

154 console.print("[yellow]Make sure the API server is running[/yellow]") 

155 except Exception as e: 

156 console.print(f"[red]✗ Error: {e}[/red]") 

157 

158 

159def _display_scan_details(scan_id: str): 

160 """Display detailed scan results.""" 

161 try: 

162 headers = get_auth_headers() 

163 backend_url = get_backend_url() 

164 

165 response = httpx.get( 

166 f"{backend_url}/scans/{scan_id}", 

167 headers=headers, 

168 timeout=10.0 

169 ) 

170 

171 if response.status_code == 404: 

172 console.print(f"[red]✗ Scan not found: {scan_id}[/red]") 

173 console.print("[yellow]Tip: Use 'alprina history' to see available scans[/yellow]") 

174 return 

175 elif response.status_code != 200: 

176 console.print(f"[red]✗ Failed to fetch scan: {response.status_code}[/red]") 

177 return 

178 

179 scan = response.json() 

180 

181 # Display scan overview 

182 console.print() 

183 console.print(Panel( 

184 f"[bold]Scan Details[/bold]\n\n" 

185 f"ID: [cyan]{scan.get('id')}[/cyan]\n" 

186 f"Target: [bold]{scan.get('target')}[/bold]\n" 

187 f"Type: {scan.get('scan_type')}\n" 

188 f"Profile: {scan.get('profile')}\n" 

189 f"Status: {scan.get('status')}\n" 

190 f"Started: {scan.get('started_at', 'N/A')}\n" 

191 f"Completed: {scan.get('completed_at', 'N/A')}", 

192 title="📊 Scan Overview" 

193 )) 

194 

195 # Display findings summary 

196 findings_count = scan.get('findings_count', 0) 

197 critical = scan.get('critical_count', 0) 

198 high = scan.get('high_count', 0) 

199 medium = scan.get('medium_count', 0) 

200 low = scan.get('low_count', 0) 

201 info = scan.get('info_count', 0) 

202 

203 summary_table = Table(title="Findings Summary") 

204 summary_table.add_column("Severity", style="bold") 

205 summary_table.add_column("Count", justify="right") 

206 

207 summary_table.add_row("[red]CRITICAL[/red]", f"[red]{critical}[/red]" if critical > 0 else "0") 

208 summary_table.add_row("[yellow]HIGH[/yellow]", f"[yellow]{high}[/yellow]" if high > 0 else "0") 

209 summary_table.add_row("[blue]MEDIUM[/blue]", f"[blue]{medium}[/blue]" if medium > 0 else "0") 

210 summary_table.add_row("[green]LOW[/green]", str(low)) 

211 summary_table.add_row("[dim]INFO[/dim]", str(info)) 

212 summary_table.add_row("[bold]TOTAL[/bold]", f"[bold]{findings_count}[/bold]") 

213 

214 console.print() 

215 console.print(summary_table) 

216 

217 # Display detailed findings if available 

218 results = scan.get('results', {}) 

219 findings = results.get('findings', []) 

220 

221 if findings: 

222 console.print() 

223 console.print(Panel("[bold]Detailed Findings[/bold]", style="cyan")) 

224 

225 for i, finding in enumerate(findings[:10], 1): # Show first 10 

226 severity = finding.get('severity', 'UNKNOWN') 

227 title = finding.get('title', 'No title') 

228 description = finding.get('description', 'No description') 

229 

230 severity_color = { 

231 'CRITICAL': 'red', 

232 'HIGH': 'yellow', 

233 'MEDIUM': 'blue', 

234 'LOW': 'green', 

235 'INFO': 'dim' 

236 }.get(severity, 'white') 

237 

238 console.print(f"\n[{severity_color}]━[/] [bold]{i}. [{severity_color}]{severity}[/{severity_color}] - {title}[/bold]") 

239 console.print(f" {description}") 

240 

241 if len(findings) > 10: 

242 console.print(f"\n[dim]... and {len(findings) - 10} more findings[/dim]") 

243 

244 console.print() 

245 console.print("[dim]Tip: Export full report with 'alprina report --scan-id " + scan_id + "'[/dim]") 

246 

247 except httpx.ConnectError: 

248 console.print(f"[red]✗ Could not connect to backend[/red]") 

249 except Exception as e: 

250 console.print(f"[red]✗ Error: {e}[/red]")