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
« 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"""
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
15from .auth import is_authenticated, get_auth_headers, get_backend_url
17console = Console()
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.
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
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]")
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()
54 # Build query params
55 params = {
56 "page": page,
57 "limit": limit
58 }
59 if severity:
60 params["severity"] = severity.upper()
62 response = httpx.get(
63 f"{backend_url}/scans",
64 headers=headers,
65 params=params,
66 timeout=10.0
67 )
69 if response.status_code != 200:
70 console.print(f"[red]✗ Failed to fetch scans: {response.status_code}[/red]")
71 return
73 data = response.json()
74 scans = data.get("scans", [])
75 total = data.get("total", 0)
76 pages = data.get("pages", 0)
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
83 # Create table
84 table = Table(title=f"Scan History (Page {page}/{pages}, Total: {total})")
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)
95 for scan in scans:
96 # Format ID (first 8 chars)
97 scan_id = scan.get("id", "")[:8]
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]
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")
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)
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)
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 )
137 console.print()
138 console.print(table)
139 console.print()
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]")
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]")
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]")
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()
165 response = httpx.get(
166 f"{backend_url}/scans/{scan_id}",
167 headers=headers,
168 timeout=10.0
169 )
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
179 scan = response.json()
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 ))
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)
203 summary_table = Table(title="Findings Summary")
204 summary_table.add_column("Severity", style="bold")
205 summary_table.add_column("Count", justify="right")
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]")
214 console.print()
215 console.print(summary_table)
217 # Display detailed findings if available
218 results = scan.get('results', {})
219 findings = results.get('findings', [])
221 if findings:
222 console.print()
223 console.print(Panel("[bold]Detailed Findings[/bold]", style="cyan"))
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')
230 severity_color = {
231 'CRITICAL': 'red',
232 'HIGH': 'yellow',
233 'MEDIUM': 'blue',
234 'LOW': 'green',
235 'INFO': 'dim'
236 }.get(severity, 'white')
238 console.print(f"\n[{severity_color}]━[/] [bold]{i}. [{severity_color}]{severity}[/{severity_color}] - {title}[/bold]")
239 console.print(f" {description}")
241 if len(findings) > 10:
242 console.print(f"\n[dim]... and {len(findings) - 10} more findings[/dim]")
244 console.print()
245 console.print("[dim]Tip: Export full report with 'alprina report --scan-id " + scan_id + "'[/dim]")
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]")