diff --git a/src/guarddoc/cli.py b/src/guarddoc/cli.py index f9e745f..fa14fb7 100644 --- a/src/guarddoc/cli.py +++ b/src/guarddoc/cli.py @@ -6,14 +6,12 @@ from typing import Annotated import typer -from rich.console import Console -from rich.panel import Panel -from rich.table import Table from guarddoc.core.config import AppConfig, get_config_path from guarddoc.core.i18n import Language -from guarddoc.core.models import ScanResult, Severity +from guarddoc.core.models import ScanResult from guarddoc.core.services import build_engine, scan_single_file +from guarddoc.views import console, render_batch_results, render_single_result from guarddoc.watcher import start_watcher HELP_DESCRIPTION = """ @@ -55,8 +53,6 @@ config_app = typer.Typer(help="Manage GuardDoc persistent configuration.") app.add_typer(config_app, name="config") -console = Console() - def get_app_version() -> str: try: @@ -67,7 +63,8 @@ def get_app_version() -> str: def version_callback(value: bool) -> None: if value: - console.print("[bold cyan]GuardDoc[/bold cyan] version [bold green]{app_ver}[/bold green]") + app_ver = get_app_version() + console.print(f"[bold cyan]GuardDoc[/bold cyan] version [bold green]{app_ver}[/bold green]") raise typer.Exit() @@ -140,12 +137,11 @@ def scan( ) -> None: """Scans a file or directory for threats and suspicious patterns.""" cfg = AppConfig.load() - - # Determine language preference: CLI argument > Saved config - if lang is not None: - selected_lang = Language.EN if lang.lower().strip() == "en" else Language.PL - else: - selected_lang = cfg.lang + selected_lang = ( + (Language.EN if lang.lower().strip() == "en" else Language.PL) + if lang is not None + else cfg.lang + ) effective_rules_dir = rules_dir or Path(cfg.rules_dir) @@ -184,152 +180,10 @@ def scan( else: console.print_json(json_str) else: - if len(results) == 1: - _render_single_result(results[0], lang=selected_lang) + if len(results) == 1 and target.is_file(): + render_single_result(results[0], lang=selected_lang) else: - _render_batch_results(results, target, lang=selected_lang) - - -def _render_single_result(result: ScanResult, lang: Language = Language.PL) -> None: - title_meta = "Metadane Pliku" if lang == Language.PL else "File Metadata" - col_prop = "Właściwość" if lang == Language.PL else "Property" - col_val = "Wartość" if lang == Language.PL else "Value" - - info_table = Table(title=title_meta, show_header=True) - info_table.add_column(col_prop, style="cyan") - info_table.add_column(col_val, style="bold white") - - label_path = "Ścieżka" if lang == Language.PL else "Path" - label_size = "Rozmiar" if lang == Language.PL else "Size" - - size_display = ( - f"{result.file_size_bytes / 1024:.2f} KB" if result.file_size_bytes is not None else "N/A" - ) - - info_table.add_row(label_path, str(result.file_path)) - info_table.add_row(label_size, size_display) - info_table.add_row("MIME Type (Magic Bytes)", str(result.mime_type or "unknown")) - if getattr(result, "sha256", None): - info_table.add_row("SHA-256", str(result.sha256)) - - console.print(info_table) - console.print() - - is_safe = getattr(result, "is_safe", getattr(result, "is_clean", len(result.threats) == 0)) - - if is_safe: - status_title = "Status Bezpieczeństwa" if lang == Language.PL else "Security Status" - safe_msg = ( - "✓ Brak wykrytych zagrożeń ani podejrzanych znaków/skryptów." - if lang == Language.PL - else "✓ No threats or suspicious characters/scripts detected." - ) - console.print( - Panel( - f"[bold green]{safe_msg}[/bold green]", - title=status_title, - border_style="green", - ) - ) - else: - threat_details = [] - for threat in result.threats: - color = "red" if threat.severity in (Severity.CRITICAL, Severity.HIGH) else "yellow" - sev_name = ( - threat.severity.name if hasattr(threat.severity, "name") else str(threat.severity) - ) - threat_details.append( - f"• [{color}][{sev_name}][/{color}] [bold]{threat.title}[/bold]\n {threat.description}" - ) - - panel_text = "\n\n".join(threat_details) - max_sev_name = ( - result.max_severity.name - if hasattr(result.max_severity, "name") - else str(result.max_severity) - ) - panel_title = ( - f"DETEKCJA ZAGROŻEŃ (Max Severity: {max_sev_name})" - if lang == Language.PL - else f"THREAT DETECTION (Max Severity: {max_sev_name})" - ) - console.print( - Panel( - panel_text, - title=f"[bold red]{panel_title}[/bold red]", - border_style="red", - ) - ) - - -def _render_batch_results( - results: list[ScanResult], target: Path, lang: Language = Language.PL -) -> None: - table_title = ( - f"Wyniki skanowania katalogu: {target}" - if lang == Language.PL - else f"Directory scan results: {target}" - ) - col_file = "Plik" if lang == Language.PL else "File" - col_status = "Status" - col_count = "Liczba Detekcji" if lang == Language.PL else "Detections Count" - - summary_table = Table(title=table_title, show_header=True) - summary_table.add_column(col_file, style="bold white") - summary_table.add_column("MIME", style="cyan") - summary_table.add_column(col_status, style="bold") - summary_table.add_column("Max Severity", style="bold") - summary_table.add_column(col_count, justify="right") - - unsafe_count = 0 - - for res in results: - is_safe = getattr(res, "is_safe", getattr(res, "is_clean", len(res.threats) == 0)) - file_name = getattr(res, "file_name", res.file_path.name) - mime_type = str(res.mime_type or "unknown") - - if is_safe: - status_str = ( - "[green]BEZPIECZNY[/green]" if lang == Language.PL else "[green]SAFE[/green]" - ) - sev_str = "[dim]NONE[/dim]" - else: - unsafe_count += 1 - status_str = "[red]ZAGROŻENIE[/red]" if lang == Language.PL else "[red]THREAT[/red]" - color = "red" if res.max_severity in (Severity.CRITICAL, Severity.HIGH) else "yellow" - sev_name = ( - res.max_severity.name - if hasattr(res.max_severity, "name") - else str(res.max_severity) - ) - sev_str = f"[{color}]{sev_name}[/{color}]" - - summary_table.add_row( - file_name, - mime_type, - status_str, - sev_str, - str(len(res.threats)), - ) - - console.print(summary_table) - console.print() - - total = len(results) - if unsafe_count > 0: - msg = ( - f"Wykryto podejrzane pliki: {unsafe_count} z {total} przeanalizowanych." - if lang == Language.PL - else f"Suspicious files detected: {unsafe_count} out of {total} analyzed." - ) - console.print(f"[bold red]{msg}[/bold red]") - else: - msg = ( - f"Przeanalizowano {total} plików. Brak wykrytych zagrożeń." - if lang == Language.PL - else f"Analyzed {total} files. No threats detected." - ) - console.print(f"[bold green]✓ {msg}[/bold green]") + render_batch_results(results, target, lang=selected_lang) @app.command( @@ -392,7 +246,7 @@ def config_show() -> None: def config_set( lang: Annotated[ str | None, - typer.Option("--lang", "-l", help="Default language (pl / en)"), + typer.Option("-l", "--lang", help="Default language (pl / en)"), ] = None, rules_dir: Annotated[ str | None, diff --git a/src/guarddoc/views.py b/src/guarddoc/views.py new file mode 100644 index 0000000..1013380 --- /dev/null +++ b/src/guarddoc/views.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +from pathlib import Path + +from rich.console import Console +from rich.panel import Panel +from rich.table import Table + +from guarddoc.core.i18n import Language +from guarddoc.core.models import ScanResult, Severity + +console = Console() + + +def render_single_result(result: ScanResult, lang: Language = Language.PL) -> None: + """Renders detailed metadata and threat detection panels for a single file.""" + title_meta = "Metadane Pliku" if lang == Language.PL else "File Metadata" + col_prop = "Właściwość" if lang == Language.PL else "Property" + col_val = "Wartość" if lang == Language.PL else "Value" + + info_table = Table(title=title_meta, show_header=True) + info_table.add_column(col_prop, style="cyan") + info_table.add_column(col_val, style="bold white") + + label_path = "Ścieżka" if lang == Language.PL else "Path" + label_size = "Rozmiar" if lang == Language.PL else "Size" + + size_display = ( + f"{result.file_size_bytes / 1024:.2f} KB" if result.file_size_bytes is not None else "N/A" + ) + + info_table.add_row(label_path, str(result.file_path)) + info_table.add_row(label_size, size_display) + info_table.add_row("MIME Type (Magic Bytes)", str(result.mime_type or "unknown")) + if getattr(result, "sha256", None): + info_table.add_row("SHA-256", str(result.sha256)) + + console.print(info_table) + console.print() + + is_safe = getattr(result, "is_safe", getattr(result, "is_clean", len(result.threats) == 0)) + + if is_safe: + status_title = "Status Bezpieczeństwa" if lang == Language.PL else "Security Status" + safe_msg = ( + "✓ Brak wykrytych zagrożeń ani podejrzanych znaków/skryptów." + if lang == Language.PL + else "✓ No threats or suspicious characters/scripts detected." + ) + console.print( + Panel( + f"[bold green]{safe_msg}[/bold green]", + title=status_title, + border_style="green", + ) + ) + else: + threat_details = [] + for threat in result.threats: + color = "red" if threat.severity in (Severity.CRITICAL, Severity.HIGH) else "yellow" + sev_name = ( + threat.severity.name if hasattr(threat.severity, "name") else str(threat.severity) + ) + threat_details.append( + f"• [{color}][{sev_name}][/{color}] [bold]{threat.title}[/bold]\n {threat.description}" + ) + + panel_text = "\n\n".join(threat_details) + max_sev_name = ( + result.max_severity.name + if hasattr(result.max_severity, "name") + else str(result.max_severity) + ) + panel_title = ( + f"DETEKCJA ZAGROŻEŃ (Max Severity: {max_sev_name})" + if lang == Language.PL + else f"THREAT DETECTION (Max Severity: {max_sev_name})" + ) + console.print( + Panel( + panel_text, + title=f"[bold red]{panel_title}[/bold red]", + border_style="red", + ) + ) + + +def render_batch_results( + results: list[ScanResult], target: Path, lang: Language = Language.PL +) -> None: + """Renders tabular summary for directory scan results.""" + table_title = ( + f"Wyniki skanowania katalogu: {target}" + if lang == Language.PL + else f"Directory scan results: {target}" + ) + col_file = "Plik" if lang == Language.PL else "File" + col_status = "Status" + col_count = "Liczba Detekcji" if lang == Language.PL else "Detections Count" + + summary_table = Table(title=table_title, show_header=True) + summary_table.add_column(col_file, style="bold white") + summary_table.add_column("MIME", style="cyan") + summary_table.add_column(col_status, style="bold") + summary_table.add_column("Max Severity", style="bold") + summary_table.add_column(col_count, justify="right") + + unsafe_count = 0 + + for res in results: + is_safe = getattr(res, "is_safe", getattr(res, "is_clean", len(res.threats) == 0)) + file_name = getattr(res, "file_name", res.file_path.name) + mime_type = str(res.mime_type or "unknown") + + if is_safe: + status_str = ( + "[green]BEZPIECZNY[/green]" if lang == Language.PL else "[green]SAFE[/green]" + ) + sev_str = "[dim]NONE[/dim]" + else: + unsafe_count += 1 + status_str = "[red]ZAGROŻENIE[/red]" if lang == Language.PL else "[red]THREAT[/red]" + color = "red" if res.max_severity in (Severity.CRITICAL, Severity.HIGH) else "yellow" + sev_name = ( + res.max_severity.name + if hasattr(res.max_severity, "name") + else str(res.max_severity) + ) + sev_str = f"[{color}]{sev_name}[/{color}]" + + summary_table.add_row( + file_name, + mime_type, + status_str, + sev_str, + str(len(res.threats)), + ) + + console.print(summary_table) + console.print() + + total = len(results) + if unsafe_count > 0: + msg = ( + f"Wykryto podejrzane pliki: {unsafe_count} z {total} przeanalizowanych." + if lang == Language.PL + else f"Suspicious files detected: {unsafe_count} out of {total} analyzed." + ) + console.print(f"[bold red]{msg}[/bold red]") + else: + msg = ( + f"Przeanalizowano {total} plików. Brak wykrytych zagrożeń." + if lang == Language.PL + else f"Analyzed {total} files. No threats detected." + ) + console.print(f"[bold green]✓ {msg}[/bold green]")