Coverage for src/alprina_cli/cli_interactive.py: 0%

248 statements  

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

1""" 

2Interactive CLI Mode 

3 

4Provides a REPL-style interface for the Alprina CLI with: 

5- Rich output formatting (colors, tables, syntax highlighting) 

6- Progress indicators for long-running operations 

7- Auto-completion and command history 

8- Context-aware help 

9 

10Context Engineering: 

11- Beautiful output doesn't inflate context 

12- Progress updates keep user informed 

13- Interactive mode for exploration 

14""" 

15 

16import sys 

17import asyncio 

18from typing import Optional, List, Dict, Any 

19from prompt_toolkit import PromptSession 

20from prompt_toolkit.completion import WordCompleter 

21from prompt_toolkit.history import FileHistory 

22from prompt_toolkit.styles import Style 

23from rich.console import Console 

24from rich.table import Table 

25from rich.panel import Panel 

26from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn 

27from rich.syntax import Syntax 

28from rich.markdown import Markdown 

29from rich import print as rprint 

30from rich.tree import Tree 

31from loguru import logger 

32 

33from alprina_cli.auth_system import get_auth_service, get_authz_service, Role 

34from alprina_cli.tools import ALL_TOOLS 

35 

36 

37class AlprinaInteractiveCLI: 

38 """ 

39 Interactive REPL for Alprina CLI. 

40 

41 Features: 

42 - Command auto-completion 

43 - Rich output formatting 

44 - Progress indicators 

45 - Command history 

46 - Context-aware help 

47 """ 

48 

49 def __init__(self): 

50 self.console = Console() 

51 self.auth_service = get_auth_service() 

52 self.authz_service = get_authz_service() 

53 self.current_user = None 

54 self.api_key = None 

55 

56 # Available commands 

57 self.commands = { 

58 "help": self.cmd_help, 

59 "login": self.cmd_login, 

60 "logout": self.cmd_logout, 

61 "whoami": self.cmd_whoami, 

62 "tools": self.cmd_tools, 

63 "scan": self.cmd_scan, 

64 "recon": self.cmd_recon, 

65 "vuln-scan": self.cmd_vuln_scan, 

66 "history": self.cmd_history, 

67 "clear": self.cmd_clear, 

68 "exit": self.cmd_exit, 

69 "quit": self.cmd_exit, 

70 } 

71 

72 # Setup prompt with auto-completion 

73 self.completer = WordCompleter(list(self.commands.keys()), ignore_case=True) 

74 

75 # Setup custom style 

76 self.style = Style.from_dict({ 

77 'prompt': '#00aa00 bold', 

78 'command': '#00aaff bold', 

79 }) 

80 

81 # Command history 

82 try: 

83 self.history = FileHistory('.alprina_history') 

84 except Exception: 

85 self.history = None 

86 

87 self.session = PromptSession( 

88 completer=self.completer, 

89 style=self.style, 

90 history=self.history 

91 ) 

92 

93 def show_banner(self): 

94 """Display welcome banner""" 

95 banner = """ 

96╔═══════════════════════════════════════════════════════════╗ 

97║ ║ 

98║ █████╗ ██╗ ██████╗ ██████╗ ██╗███╗ ██╗ █████╗ ║ 

99║ ██╔══██╗██║ ██╔══██╗██╔══██╗██║████╗ ██║██╔══██╗ ║ 

100║ ███████║██║ ██████╔╝██████╔╝██║██╔██╗ ██║███████║ ║ 

101║ ██╔══██║██║ ██╔═══╝ ██╔══██╗██║██║╚██╗██║██╔══██║ ║ 

102║ ██║ ██║███████╗██║ ██║ ██║██║██║ ╚████║██║ ██║ ║ 

103║ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ║ 

104║ ║ 

105║ Security Automation Platform - Interactive Mode ║ 

106║ Type 'help' to start ║ 

107║ ║ 

108╚═══════════════════════════════════════════════════════════╝ 

109 """ 

110 self.console.print(banner, style="bold cyan") 

111 

112 def get_prompt(self) -> str: 

113 """Get the command prompt string""" 

114 if self.current_user: 

115 username = self.current_user.username 

116 role = self.current_user.role.value 

117 return f"[bold green]alprina[/bold green] [{role}@{username}]> " 

118 return "[bold green]alprina[/bold green] [guest]> " 

119 

120 async def cmd_help(self, args: List[str]): 

121 """Show help information""" 

122 if args and args[0] in self.commands: 

123 # Show help for specific command 

124 cmd = args[0] 

125 self.console.print(f"\n[bold cyan]Help for '{cmd}':[/bold cyan]\n") 

126 # TODO: Add detailed help for each command 

127 self.console.print(f"Command: {cmd}") 

128 else: 

129 # Show general help 

130 table = Table(title="Available Commands", show_header=True, header_style="bold magenta") 

131 table.add_column("Command", style="cyan", no_wrap=True) 

132 table.add_column("Description") 

133 

134 table.add_row("help [command]", "Show this help or help for a specific command") 

135 table.add_row("login", "Authenticate with API key") 

136 table.add_row("logout", "Log out current user") 

137 table.add_row("whoami", "Show current user information") 

138 table.add_row("tools", "List available security tools") 

139 table.add_row("scan <target>", "Perform security scan") 

140 table.add_row("recon <target>", "Perform reconnaissance") 

141 table.add_row("vuln-scan <target>", "Perform vulnerability scan") 

142 table.add_row("history", "Show command history") 

143 table.add_row("clear", "Clear screen") 

144 table.add_row("exit/quit", "Exit interactive mode") 

145 

146 self.console.print(table) 

147 

148 async def cmd_login(self, args: List[str]): 

149 """Authenticate user""" 

150 if self.current_user: 

151 self.console.print("[yellow]Already logged in. Use 'logout' first.[/yellow]") 

152 return 

153 

154 if not args: 

155 # Interactive login 

156 from prompt_toolkit import prompt 

157 api_key = prompt("API Key: ", is_password=True) 

158 else: 

159 api_key = args[0] 

160 

161 # Authenticate 

162 with self.console.status("[bold green]Authenticating...", spinner="dots"): 

163 user = self.auth_service.authenticate(api_key) 

164 

165 if user: 

166 self.current_user = user 

167 self.api_key = api_key 

168 

169 # Show success message 

170 panel = Panel( 

171 f"[bold green]✓[/bold green] Authenticated as [cyan]{user.username}[/cyan]\n" 

172 f"Role: [yellow]{user.role.value}[/yellow]\n" 

173 f"Email: {user.email}", 

174 title="[bold green]Login Successful[/bold green]", 

175 border_style="green" 

176 ) 

177 self.console.print(panel) 

178 else: 

179 self.console.print("[bold red]✗ Authentication failed. Invalid API key.[/bold red]") 

180 

181 async def cmd_logout(self, args: List[str]): 

182 """Log out current user""" 

183 if not self.current_user: 

184 self.console.print("[yellow]Not logged in.[/yellow]") 

185 return 

186 

187 username = self.current_user.username 

188 self.current_user = None 

189 self.api_key = None 

190 

191 self.console.print(f"[green]Logged out {username}[/green]") 

192 

193 async def cmd_whoami(self, args: List[str]): 

194 """Show current user information""" 

195 if not self.current_user: 

196 self.console.print("[yellow]Not logged in. Use 'login' to authenticate.[/yellow]") 

197 return 

198 

199 user = self.current_user 

200 

201 # Get user permissions 

202 permissions = self.authz_service.get_user_permissions(user) 

203 

204 # Create info panel 

205 table = Table(show_header=False, box=None) 

206 table.add_column("Field", style="cyan bold") 

207 table.add_column("Value") 

208 

209 table.add_row("Username", user.username) 

210 table.add_row("Email", user.email) 

211 table.add_row("Role", user.role.value) 

212 table.add_row("User ID", user.user_id) 

213 table.add_row("Active", "✓ Yes" if user.is_active else "✗ No") 

214 table.add_row("Permissions", f"{len(permissions)} permissions") 

215 

216 panel = Panel(table, title="[bold cyan]Current User[/bold cyan]", border_style="cyan") 

217 self.console.print(panel) 

218 

219 # Show permissions 

220 if permissions: 

221 perm_tree = Tree("[bold cyan]Permissions[/bold cyan]") 

222 for perm in permissions: 

223 perm_tree.add(f"[green]✓[/green] {perm.value}") 

224 self.console.print(perm_tree) 

225 

226 async def cmd_tools(self, args: List[str]): 

227 """List available security tools""" 

228 table = Table(title="Available Security Tools", show_header=True, header_style="bold magenta") 

229 table.add_column("#", style="dim", width=4) 

230 table.add_column("Tool Name", style="cyan") 

231 table.add_column("Description") 

232 table.add_column("Access", justify="center") 

233 

234 for i, tool in enumerate(ALL_TOOLS, 1): 

235 # Check if user has access 

236 if self.current_user: 

237 has_access = self.authz_service.can_use_tool(self.current_user, tool.__class__.__name__) 

238 access_indicator = "[green]✓[/green]" if has_access else "[red]✗[/red]" 

239 else: 

240 access_indicator = "[dim]?[/dim]" 

241 

242 table.add_row( 

243 str(i), 

244 tool.name, 

245 tool.description, 

246 access_indicator 

247 ) 

248 

249 self.console.print(table) 

250 

251 if not self.current_user: 

252 self.console.print("\n[dim]Login to see your tool access permissions[/dim]") 

253 

254 async def cmd_scan(self, args: List[str]): 

255 """Perform security scan""" 

256 if not self.current_user: 

257 self.console.print("[yellow]Please login first using 'login' command[/yellow]") 

258 return 

259 

260 if not args: 

261 self.console.print("[yellow]Usage: scan <target> [ports][/yellow]") 

262 return 

263 

264 target = args[0] 

265 ports = args[1] if len(args) > 1 else "80,443" 

266 

267 # Check authorization 

268 if not self.authz_service.can_use_tool(self.current_user, "ScanTool"): 

269 self.console.print("[bold red]✗ Permission denied. You don't have access to ScanTool.[/bold red]") 

270 return 

271 

272 # Import and execute tool 

273 from alprina_cli.tools.security.scan import ScanTool, ScanParams 

274 

275 tool = ScanTool() 

276 params = ScanParams(target=target, scan_type="quick", ports=ports) 

277 

278 # Show progress 

279 with Progress( 

280 SpinnerColumn(), 

281 TextColumn("[progress.description]{task.description}"), 

282 BarColumn(), 

283 TimeElapsedColumn(), 

284 console=self.console 

285 ) as progress: 

286 task = progress.add_task(f"[cyan]Scanning {target}...", total=None) 

287 

288 # Execute scan 

289 result = await tool(params) 

290 

291 progress.update(task, completed=True) 

292 

293 # Display results 

294 if hasattr(result, 'content'): 

295 self.display_scan_results(target, result.content) 

296 else: 

297 self.console.print(f"[red]Scan failed: {result.message}[/red]") 

298 

299 async def cmd_recon(self, args: List[str]): 

300 """Perform reconnaissance""" 

301 if not self.current_user: 

302 self.console.print("[yellow]Please login first[/yellow]") 

303 return 

304 

305 if not args: 

306 self.console.print("[yellow]Usage: recon <target> [operation][/yellow]") 

307 return 

308 

309 target = args[0] 

310 operation = args[1] if len(args) > 1 else "whois" 

311 

312 # Import and execute 

313 from alprina_cli.tools.security.recon import ReconTool, ReconParams 

314 

315 tool = ReconTool() 

316 params = ReconParams(target=target, operation=operation, timeout=30) 

317 

318 with Progress( 

319 SpinnerColumn(), 

320 TextColumn("[progress.description]{task.description}"), 

321 console=self.console 

322 ) as progress: 

323 task = progress.add_task(f"[cyan]Reconnaissance on {target}...", total=None) 

324 result = await tool(params) 

325 progress.update(task, completed=True) 

326 

327 # Display results 

328 if hasattr(result, 'content'): 

329 panel = Panel( 

330 str(result.content), 

331 title=f"[bold cyan]Recon Results: {target}[/bold cyan]", 

332 border_style="cyan" 

333 ) 

334 self.console.print(panel) 

335 else: 

336 self.console.print(f"[red]Recon failed: {result.message}[/red]") 

337 

338 async def cmd_vuln_scan(self, args: List[str]): 

339 """Perform vulnerability scan""" 

340 if not self.current_user: 

341 self.console.print("[yellow]Please login first[/yellow]") 

342 return 

343 

344 if not args: 

345 self.console.print("[yellow]Usage: vuln-scan <target>[/yellow]") 

346 return 

347 

348 target = args[0] 

349 

350 # Import and execute 

351 from alprina_cli.tools.security.vuln_scan import VulnScanTool, VulnScanParams 

352 

353 tool = VulnScanTool() 

354 params = VulnScanParams(target=target, scan_type="web", depth="basic") 

355 

356 with Progress( 

357 SpinnerColumn(), 

358 TextColumn("[progress.description]{task.description}"), 

359 BarColumn(), 

360 TimeElapsedColumn(), 

361 console=self.console 

362 ) as progress: 

363 task = progress.add_task(f"[cyan]Vulnerability scanning {target}...", total=None) 

364 result = await tool(params) 

365 progress.update(task, completed=True) 

366 

367 # Display results 

368 if hasattr(result, 'content'): 

369 self.display_vuln_results(target, result.content) 

370 else: 

371 self.console.print(f"[red]Vulnerability scan failed: {result.message}[/red]") 

372 

373 def display_scan_results(self, target: str, results: Any): 

374 """Display scan results in a formatted table""" 

375 if isinstance(results, dict): 

376 table = Table(title=f"Scan Results: {target}", show_header=True, header_style="bold magenta") 

377 table.add_column("Property", style="cyan") 

378 table.add_column("Value") 

379 

380 for key, value in results.items(): 

381 table.add_row(str(key), str(value)) 

382 

383 self.console.print(table) 

384 else: 

385 panel = Panel( 

386 str(results), 

387 title=f"[bold cyan]Scan Results: {target}[/bold cyan]", 

388 border_style="cyan" 

389 ) 

390 self.console.print(panel) 

391 

392 def display_vuln_results(self, target: str, results: Any): 

393 """Display vulnerability scan results""" 

394 if isinstance(results, dict) and 'findings' in results: 

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

396 

397 table = Table(title=f"Vulnerabilities Found: {target}", show_header=True, header_style="bold red") 

398 table.add_column("#", style="dim", width=4) 

399 table.add_column("Severity", style="bold") 

400 table.add_column("Title") 

401 table.add_column("Description") 

402 

403 for i, finding in enumerate(findings, 1): 

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

405 severity_style = { 

406 'CRITICAL': '[bold red]CRITICAL[/bold red]', 

407 'HIGH': '[red]HIGH[/red]', 

408 'MEDIUM': '[yellow]MEDIUM[/yellow]', 

409 'LOW': '[blue]LOW[/blue]', 

410 'INFO': '[dim]INFO[/dim]' 

411 }.get(severity, severity) 

412 

413 table.add_row( 

414 str(i), 

415 severity_style, 

416 finding.get('title', 'N/A'), 

417 finding.get('description', 'N/A')[:50] + "..." 

418 ) 

419 

420 self.console.print(table) 

421 else: 

422 self.display_scan_results(target, results) 

423 

424 async def cmd_history(self, args: List[str]): 

425 """Show command history""" 

426 self.console.print("[dim]Command history feature coming soon...[/dim]") 

427 

428 async def cmd_clear(self, args: List[str]): 

429 """Clear screen""" 

430 self.console.clear() 

431 

432 async def cmd_exit(self, args: List[str]): 

433 """Exit interactive mode""" 

434 self.console.print("\n[cyan]Goodbye! Stay secure. 🔒[/cyan]\n") 

435 sys.exit(0) 

436 

437 async def execute_command(self, command_line: str): 

438 """Execute a command""" 

439 if not command_line.strip(): 

440 return 

441 

442 parts = command_line.strip().split() 

443 command = parts[0].lower() 

444 args = parts[1:] 

445 

446 if command in self.commands: 

447 try: 

448 await self.commands[command](args) 

449 except Exception as e: 

450 self.console.print(f"[bold red]Error executing command: {e}[/bold red]") 

451 logger.error(f"Command execution error: {e}", exc_info=True) 

452 else: 

453 self.console.print(f"[yellow]Unknown command: {command}. Type 'help' for available commands.[/yellow]") 

454 

455 async def run(self): 

456 """Run the interactive CLI""" 

457 self.show_banner() 

458 

459 # Show login prompt if not authenticated 

460 if not self.current_user: 

461 self.console.print("\n[dim]Tip: Use 'login' to authenticate or 'help' to see available commands[/dim]\n") 

462 

463 while True: 

464 try: 

465 # Get command from user 

466 command_line = await asyncio.get_event_loop().run_in_executor( 

467 None, 

468 lambda: self.session.prompt(self.get_prompt(), default="") 

469 ) 

470 

471 # Execute command 

472 await self.execute_command(command_line) 

473 

474 except KeyboardInterrupt: 

475 self.console.print("\n[dim]Use 'exit' or 'quit' to exit[/dim]") 

476 continue 

477 except EOFError: 

478 break 

479 except Exception as e: 

480 self.console.print(f"[bold red]Error: {e}[/bold red]") 

481 logger.error(f"Interactive CLI error: {e}", exc_info=True) 

482 

483 

484async def main(): 

485 """Main entry point for interactive CLI""" 

486 cli = AlprinaInteractiveCLI() 

487 await cli.run() 

488 

489 

490if __name__ == "__main__": 

491 asyncio.run(main())