460 lines
12 KiB
Python
460 lines
12 KiB
Python
"""
|
|
ⒸAngelaMos | 2026
|
|
main.py
|
|
|
|
Typer CLI commands for network traffic analyzer
|
|
|
|
Defines the netanal CLI with six commands: capture, analyze, stats, export,
|
|
chart, and interfaces. Capture performs live sniffing via CaptureEngine with
|
|
optional verbose packet streaming and JSON output. The analyze, stats, export,
|
|
and chart commands load from a pcap file through _analyze_pcap_to_stats,
|
|
which replays packets through StatisticsCollector.
|
|
|
|
Key exports:
|
|
app - The Typer application instance, used as the entry point in __main__.py
|
|
|
|
Connects to:
|
|
analyzer.py - calls analyze_pcap_file() for all pcap-based commands
|
|
capture.py - imports CaptureConfig, CaptureEngine, GracefulCapture, check_capture_permissions, get_available_interfaces
|
|
export.py - calls export_to_json(), export_to_csv(), statistics_to_dict()
|
|
filters.py - calls validate_bpf_filter() before starting a live capture
|
|
output.py - imports console and all print_* functions
|
|
statistics.py - creates StatisticsCollector to process replayed pcap packets
|
|
visualization.py - calls chart creation functions for the chart command
|
|
__init__.py - imports __version__ for the --version flag
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Annotated, Literal
|
|
|
|
import typer
|
|
|
|
from netanal import __version__
|
|
from netanal.analyzer import analyze_pcap_file
|
|
from netanal.capture import (
|
|
CaptureConfig,
|
|
CaptureEngine,
|
|
GracefulCapture,
|
|
check_capture_permissions,
|
|
get_available_interfaces,
|
|
)
|
|
from netanal.export import (
|
|
export_to_csv,
|
|
export_to_json,
|
|
statistics_to_dict,
|
|
)
|
|
from netanal.filters import validate_bpf_filter
|
|
from netanal.models import CaptureStatistics, PacketInfo
|
|
from netanal.output import (
|
|
console,
|
|
print_bandwidth_stats,
|
|
print_capture_summary,
|
|
print_error,
|
|
print_interfaces,
|
|
print_packet,
|
|
print_protocol_table,
|
|
print_success,
|
|
print_top_talkers,
|
|
print_warning,
|
|
)
|
|
from netanal.statistics import StatisticsCollector
|
|
from netanal.visualization import (
|
|
create_bandwidth_chart,
|
|
create_protocol_bar_chart,
|
|
create_top_talkers_chart,
|
|
generate_all_charts,
|
|
save_chart,
|
|
)
|
|
|
|
|
|
ExportFormat = Literal["json", "csv"]
|
|
ChartType = Literal["protocols", "top-talkers", "bandwidth", "all"]
|
|
|
|
app = typer.Typer(
|
|
name="netanal",
|
|
help="[bold cyan]Network Traffic Analyzer[/bold cyan] - Capture and analyze packets",
|
|
rich_markup_mode="rich",
|
|
no_args_is_help=True,
|
|
)
|
|
|
|
|
|
def _analyze_pcap_to_stats(pcap_file: Path) -> tuple[CaptureStatistics,
|
|
list[PacketInfo]]:
|
|
"""
|
|
Analyze a PCAP file and return statistics with packet list
|
|
"""
|
|
packets = analyze_pcap_file(str(pcap_file))
|
|
collector = StatisticsCollector()
|
|
collector.start()
|
|
|
|
for packet in packets:
|
|
collector.record_packet(packet)
|
|
|
|
return collector.get_statistics(), packets
|
|
|
|
|
|
def version_callback(value: bool) -> None:
|
|
"""
|
|
Display version and exit
|
|
"""
|
|
if value:
|
|
console.print(
|
|
f"[bold cyan]netanal[/bold cyan] version [green]{__version__}[/green]"
|
|
)
|
|
raise typer.Exit()
|
|
|
|
|
|
@app.callback()
|
|
def main(
|
|
version: Annotated[
|
|
bool | None,
|
|
typer.Option(
|
|
"--version",
|
|
"-v",
|
|
help="Show version and exit",
|
|
callback=version_callback,
|
|
is_eager=True,
|
|
),
|
|
] = None,
|
|
) -> None:
|
|
"""
|
|
[bold cyan]Network Traffic Analyzer[/bold cyan]
|
|
|
|
Capture and analyze network packets with protocol distribution,
|
|
top talkers identification, and bandwidth visualization.
|
|
"""
|
|
|
|
|
|
@app.command()
|
|
def capture(
|
|
interface: Annotated[
|
|
str | None,
|
|
typer.Option(
|
|
"--interface",
|
|
"-i",
|
|
help="Network interface to capture on",
|
|
),
|
|
] = None,
|
|
filter_expr: Annotated[
|
|
str | None,
|
|
typer.Option(
|
|
"--filter",
|
|
"-f",
|
|
help="BPF filter expression",
|
|
),
|
|
] = None,
|
|
count: Annotated[
|
|
int | None,
|
|
typer.Option(
|
|
"--count",
|
|
"-c",
|
|
help="Number of packets to capture",
|
|
),
|
|
] = None,
|
|
timeout: Annotated[
|
|
float | None,
|
|
typer.Option(
|
|
"--timeout",
|
|
"-t",
|
|
help="Capture timeout in seconds",
|
|
),
|
|
] = None,
|
|
output: Annotated[
|
|
Path | None,
|
|
typer.Option(
|
|
"--output",
|
|
"-o",
|
|
help="Output file for results (JSON)",
|
|
),
|
|
] = None,
|
|
verbose: Annotated[
|
|
bool,
|
|
typer.Option(
|
|
"--verbose",
|
|
help="Show individual packets",
|
|
),
|
|
] = False,
|
|
) -> None:
|
|
"""
|
|
[bold green]Capture[/bold green] live network packets
|
|
|
|
Examples:
|
|
netanal capture -i eth0 --count 100
|
|
netanal capture --filter "tcp port 80" --timeout 30
|
|
netanal capture -i lo -c 50 --verbose
|
|
"""
|
|
can_capture, msg = check_capture_permissions()
|
|
if not can_capture:
|
|
print_error(f"Cannot capture packets: {msg}")
|
|
raise typer.Exit(1)
|
|
|
|
if filter_expr and not validate_bpf_filter(filter_expr):
|
|
print_error(f"Invalid BPF filter: {filter_expr}")
|
|
raise typer.Exit(1)
|
|
|
|
config = CaptureConfig(
|
|
interface=interface,
|
|
bpf_filter=filter_expr,
|
|
packet_count=count,
|
|
timeout_seconds=timeout,
|
|
)
|
|
|
|
packets_captured: list[PacketInfo] = []
|
|
|
|
def on_packet(packet: PacketInfo) -> None:
|
|
if verbose:
|
|
print_packet(packet)
|
|
if output:
|
|
packets_captured.append(packet)
|
|
|
|
console.print(
|
|
f"[cyan]Starting capture on {interface or 'all interfaces'}...[/cyan]"
|
|
)
|
|
console.print("[dim]Press Ctrl+C to stop[/dim]\n")
|
|
|
|
engine = CaptureEngine(
|
|
config=config,
|
|
on_packet=on_packet if verbose or output else None
|
|
)
|
|
|
|
with GracefulCapture(engine) as cap:
|
|
stats = cap.wait()
|
|
|
|
console.print()
|
|
print_capture_summary(stats)
|
|
print_protocol_table(stats)
|
|
print_top_talkers(stats)
|
|
|
|
if output:
|
|
export_to_json(stats, output, packets_captured)
|
|
print_success(f"Results saved to {output}")
|
|
|
|
|
|
@app.command()
|
|
def analyze(
|
|
pcap_file: Annotated[
|
|
Path,
|
|
typer.Argument(help="PCAP file to analyze"),
|
|
],
|
|
top_talkers: Annotated[
|
|
int,
|
|
typer.Option(
|
|
"--top-talkers",
|
|
"-t",
|
|
help="Show top N talkers",
|
|
),
|
|
] = 10,
|
|
json_output: Annotated[
|
|
bool,
|
|
typer.Option(
|
|
"--json",
|
|
"-j",
|
|
help="Output results as JSON",
|
|
),
|
|
] = False,
|
|
) -> None:
|
|
"""
|
|
[bold green]Analyze[/bold green] packets from a PCAP file
|
|
|
|
Examples:
|
|
netanal analyze traffic.pcap
|
|
netanal analyze traffic.pcap --top-talkers 20
|
|
netanal analyze traffic.pcap --json
|
|
"""
|
|
if not pcap_file.exists():
|
|
print_error(f"File not found: {pcap_file}")
|
|
raise typer.Exit(1)
|
|
|
|
console.print(f"[cyan]Analyzing {pcap_file}...[/cyan]")
|
|
|
|
stats, _ = _analyze_pcap_to_stats(pcap_file)
|
|
|
|
if json_output:
|
|
console.print(json.dumps(statistics_to_dict(stats), indent=2))
|
|
else:
|
|
print_capture_summary(stats)
|
|
print_protocol_table(stats)
|
|
print_top_talkers(stats, limit=top_talkers)
|
|
|
|
|
|
@app.command()
|
|
def stats(
|
|
pcap_file: Annotated[
|
|
Path,
|
|
typer.Argument(help="PCAP file to analyze"),
|
|
],
|
|
bandwidth: Annotated[
|
|
bool,
|
|
typer.Option(
|
|
"--bandwidth",
|
|
"-b",
|
|
help="Show bandwidth statistics",
|
|
),
|
|
] = False,
|
|
protocols: Annotated[
|
|
bool,
|
|
typer.Option(
|
|
"--protocols",
|
|
"-p",
|
|
help="Show protocol distribution",
|
|
),
|
|
] = False,
|
|
endpoints: Annotated[
|
|
bool,
|
|
typer.Option(
|
|
"--endpoints",
|
|
"-e",
|
|
help="Show endpoint statistics",
|
|
),
|
|
] = False,
|
|
) -> None:
|
|
"""
|
|
[bold green]Display statistics[/bold green] from a PCAP file
|
|
|
|
Examples:
|
|
netanal stats traffic.pcap --bandwidth
|
|
netanal stats traffic.pcap --protocols --endpoints
|
|
"""
|
|
if not pcap_file.exists():
|
|
print_error(f"File not found: {pcap_file}")
|
|
raise typer.Exit(1)
|
|
|
|
stats_result, _ = _analyze_pcap_to_stats(pcap_file)
|
|
|
|
print_capture_summary(stats_result)
|
|
|
|
if protocols or (not bandwidth and not endpoints):
|
|
print_protocol_table(stats_result)
|
|
|
|
if endpoints:
|
|
print_top_talkers(stats_result, limit=20)
|
|
|
|
if bandwidth:
|
|
print_bandwidth_stats(stats_result)
|
|
|
|
|
|
@app.command("export")
|
|
def export_cmd(
|
|
pcap_file: Annotated[
|
|
Path,
|
|
typer.Argument(help="PCAP file to export"),
|
|
],
|
|
output: Annotated[
|
|
Path,
|
|
typer.Option(
|
|
"--output",
|
|
"-o",
|
|
help="Output file path",
|
|
),
|
|
],
|
|
format_type: Annotated[
|
|
ExportFormat,
|
|
typer.Option(
|
|
"--format",
|
|
"-f",
|
|
help="Output format",
|
|
),
|
|
] = "json",
|
|
) -> None:
|
|
"""
|
|
[bold green]Export[/bold green] capture data to CSV or JSON
|
|
|
|
Examples:
|
|
netanal export traffic.pcap -o results.json -f json
|
|
netanal export traffic.pcap -o packets.csv -f csv
|
|
"""
|
|
if not pcap_file.exists():
|
|
print_error(f"File not found: {pcap_file}")
|
|
raise typer.Exit(1)
|
|
|
|
stats, packets = _analyze_pcap_to_stats(pcap_file)
|
|
|
|
if format_type == "json":
|
|
export_to_json(stats, output, packets)
|
|
elif format_type == "csv":
|
|
export_to_csv(stats, output, packets)
|
|
|
|
print_success(f"Exported to {output}")
|
|
|
|
|
|
@app.command()
|
|
def chart(
|
|
pcap_file: Annotated[
|
|
Path,
|
|
typer.Argument(help="PCAP file to visualize"),
|
|
],
|
|
chart_type: Annotated[
|
|
ChartType,
|
|
typer.Option(
|
|
"--type",
|
|
"-t",
|
|
help="Chart type",
|
|
),
|
|
] = "all",
|
|
output: Annotated[
|
|
Path | None,
|
|
typer.Option(
|
|
"--output",
|
|
"-o",
|
|
help="Output file path (for single chart)",
|
|
),
|
|
] = None,
|
|
output_dir: Annotated[
|
|
Path | None,
|
|
typer.Option(
|
|
"--output-dir",
|
|
"-d",
|
|
help="Output directory (for all charts)",
|
|
),
|
|
] = None,
|
|
) -> None:
|
|
"""
|
|
[bold green]Generate charts[/bold green] from capture data
|
|
|
|
Examples:
|
|
netanal chart traffic.pcap --type protocols -o protocols.png
|
|
netanal chart traffic.pcap --type all -d ./charts/
|
|
"""
|
|
if not pcap_file.exists():
|
|
print_error(f"File not found: {pcap_file}")
|
|
raise typer.Exit(1)
|
|
|
|
stats, _ = _analyze_pcap_to_stats(pcap_file)
|
|
|
|
if chart_type == "all":
|
|
out_dir = output_dir or Path()
|
|
generated = generate_all_charts(stats, out_dir)
|
|
for path in generated:
|
|
print_success(f"Generated {path}")
|
|
else:
|
|
if not output:
|
|
output = Path(f"{chart_type}.png")
|
|
|
|
if chart_type == "protocols":
|
|
fig = create_protocol_bar_chart(stats)
|
|
elif chart_type == "top-talkers":
|
|
fig = create_top_talkers_chart(stats)
|
|
else:
|
|
fig = create_bandwidth_chart(stats)
|
|
|
|
save_chart(fig, output)
|
|
print_success(f"Generated {output}")
|
|
|
|
|
|
@app.command()
|
|
def interfaces() -> None:
|
|
"""
|
|
[bold green]List[/bold green] available network interfaces
|
|
"""
|
|
ifaces = get_available_interfaces()
|
|
if not ifaces:
|
|
print_warning("No interfaces found")
|
|
raise typer.Exit(0)
|
|
|
|
print_interfaces(ifaces)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app()
|