diff --git a/README.md b/README.md index 8993df8..a6928d3 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## Overview -Phantom is a **network reconnaissance and security auditing tool** designed for directly connected networks. It discovers devices via ARP scanning, tracks their history, detects ARP spoofing attacks, and can perform MITM interception with live packet analysis powered by a local LLM. +Phantom is a **network reconnaissance and security auditing tool** designed for directly connected networks. It discovers devices via ARP scanning, tracks their history, detects ARP spoofing attacks, and can perform MITM interception with live packet analysis powered by a local or cloud LLM. The GUI is built with **PySide6** (Qt framework) and uses **Scapy** for all packet-level operations. @@ -21,7 +21,7 @@ The GUI is built with **PySide6** (Qt framework) and uses **Scapy** for all pack - **New Device & MAC Change Detection**: Highlights new devices (green) and IP-to-MAC binding changes (red) — a classic ARP spoofing indicator. - **ARP Spoof Detection**: Passive background sniffer that alerts on conflicting ARP bindings and gateway MAC changes. - **MITM Interception**: ARP-spoof a target to intercept its traffic; captured packets are displayed in real time with a full layer-by-layer breakdown. -- **LLM Packet Analysis**: Send any captured packet to a local [Ollama](https://ollama.com) instance for AI-assisted analysis (protocol identification, risk assessment, credential spotting). +- **LLM Packet Analysis**: Send any captured packet to a local [Ollama](https://ollama.com) instance or the [Anthropic API](https://www.anthropic.com) for AI-assisted analysis (protocol identification, risk assessment, credential spotting). - **PCAP Export**: Save captured packets from a MITM session as a `.pcap` file for offline analysis in Wireshark. - **Scan Export**: Export scan results to JSON or CSV. - **Progress Bar**: Live progress feedback during scanning. @@ -38,7 +38,9 @@ The GUI is built with **PySide6** (Qt framework) and uses **Scapy** for all pack - **PySide6** — graphical user interface - **netifaces** — network interface introspection - **requests** — Ollama API streaming +- **anthropic** — Anthropic API client (installed via `requirements.txt`) - **Ollama** (optional) — local LLM for packet analysis (`ollama serve`) +- **Anthropic API key** (optional) — set via `ANTHROPIC_API_KEY` env var or entered in the UI --- @@ -132,18 +134,35 @@ Click **Save PCAP** to write the captured session to a `.pcap` file. > **Note:** MITM requires root/sudo. IP forwarding is restored automatically when MITM is stopped. -### 4. LLM packet analysis (Ollama) +### 4. LLM packet analysis -With [Ollama](https://ollama.com) running locally (`ollama serve`) and at least one model pulled: +Select a captured packet in the MITM window, then choose a **Provider**: -1. Select a captured packet in the MITM window. -2. Choose a model from the **Model** drop-down (populated automatically from the running Ollama instance). Click **↻** to refresh the list after pulling a new model. +#### Ollama (local) + +Requires [Ollama](https://ollama.com) running locally (`ollama serve`) with at least one model pulled. + +1. Set **Provider** to **Ollama (local)**. +2. Choose a model from the **Model** drop-down (populated automatically). Click **↻** to refresh after pulling a new model. 3. Optionally add context in the **Context** field (e.g. `"this is a smart TV"`). -4. Click **Analyse with LLM** — the analysis opens in a dedicated window and streams in token by token. Use **Copy analysis** to copy the result to the clipboard. +4. Click **Analyse with LLM**. + +> **Tip:** Any model available via `ollama list` can be used. Smaller models respond faster; larger ones give more detailed analysis. + +#### Anthropic API (cloud) + +Requires an [Anthropic API key](https://console.anthropic.com). + +1. Set **Provider** to **Anthropic**. +2. Choose a model (`claude-opus-4-6`, `claude-sonnet-4-6`, or `claude-haiku-4-5`). +3. Enter your API key in the **API key** field (or set `ANTHROPIC_API_KEY` in the environment and it will pre-fill automatically). +4. Optionally add context, then click **Analyse with LLM**. + +> **Tip:** `claude-haiku-4-5` is fastest and cheapest for quick checks; `claude-opus-4-6` gives the most thorough analysis. -The LLM identifies protocol/service, describes what the endpoints are doing, flags security-relevant observations, and provides a risk rating. +The analysis opens in a dedicated window and streams token by token. Use **Copy analysis** to copy the result to the clipboard. -> **Tip:** Any model available via `ollama list` can be used. Smaller models (e.g. `llama3.2:1b`) respond faster; larger ones (e.g. `llama3.1:8b`) give more detailed analysis. +The LLM identifies protocol/service, flags security-relevant observations (plaintext credentials, CVE patterns, suspicious beaconing), and provides a risk rating. --- @@ -156,7 +175,7 @@ core/ arp_spoofer.py — low-level ARP spoof / restore primitives mitm.py — MitmThread (spoof loop + sniffer), IP forwarding management spoof_detector.py — passive ARP sniff-based spoof detection - ollama_analyst.py — OllamaThread for streaming LLM packet analysis + llm_analyst.py — OllamaThread and AnthropicThread for streaming LLM packet analysis db.py — SQLite persistence (device history, MAC audit trail) networking.py — CIDR calculation, hostname resolution helpers vendor.py — OUI/MAC vendor lookup diff --git a/core/arp_scanner.py b/core/arp_scanner.py index f7385a8..3cb087c 100644 --- a/core/arp_scanner.py +++ b/core/arp_scanner.py @@ -22,7 +22,9 @@ import core.networking as net from core import vendor from core.mitm import MitmThread -from core.ollama_analyst import OllamaThread, fetch_ollama_models +from core.llm_analyst import ( # pylint: disable=E0611 + ANTHROPIC_MODELS, AnthropicThread, OllamaThread, fetch_ollama_models +) from core.platform import get_os from ui.ui_arpscan import Ui_DeviceDiscovery @@ -191,7 +193,7 @@ def __init__( # pylint: disable=too-many-arguments,too-many-positional-argument self.setCentralWidget(central_widget) self.resize(680, 850) - self._load_ollama_models() + self._on_provider_changed() @Slot(bool) def _toggle_mitm(self, checked): @@ -247,8 +249,17 @@ def _build_packet_panel(self, layout: QVBoxLayout): ) self._user_context.returnPressed.connect(self._analyse_packet) + # Provider selector + self._provider_combo = QComboBox() + self._provider_combo.addItems(["Ollama (local)", "Anthropic"]) + self._provider_combo.currentIndexChanged.connect(self._on_provider_changed) + provider_row = QHBoxLayout() + provider_row.addWidget(QLabel("Provider:")) + provider_row.addWidget(self._provider_combo, stretch=1) + + # Model selector — contents change with provider self._model_combo = QComboBox() - self._model_combo.setPlaceholderText("Select Ollama model…") + self._model_combo.setPlaceholderText("Select model…") self._refresh_models_button = QPushButton("↻") self._refresh_models_button.setFixedWidth(28) self._refresh_models_button.setToolTip("Refresh available Ollama models") @@ -258,13 +269,37 @@ def _build_packet_panel(self, layout: QVBoxLayout): model_row.addWidget(self._model_combo, stretch=1) model_row.addWidget(self._refresh_models_button) + # Anthropic API key field (hidden when Ollama is selected) + import os as _os # pylint: disable=import-outside-toplevel + self._api_key_edit = QLineEdit() + self._api_key_edit.setPlaceholderText( + "Anthropic API key (or set ANTHROPIC_API_KEY env var)" + ) + self._api_key_edit.setEchoMode(QLineEdit.EchoMode.Password) + self._api_key_edit.setText(_os.environ.get("ANTHROPIC_API_KEY", "")) + self._api_key_edit.setVisible(False) + layout.addWidget(QLabel("Captured packets:")) layout.addWidget(pkt_splitter) layout.addWidget(QLabel("Context:")) layout.addWidget(self._user_context) + layout.addLayout(provider_row) layout.addLayout(model_row) + layout.addWidget(self._api_key_edit) layout.addWidget(self._analyse_button) + def _on_provider_changed(self): + """Switch model list and API key visibility when the provider changes.""" + is_anthropic = self._provider_combo.currentText() == "Anthropic" + self._api_key_edit.setVisible(is_anthropic) + self._refresh_models_button.setVisible(not is_anthropic) + self._model_combo.clear() + if is_anthropic: + self._model_combo.addItems(ANTHROPIC_MODELS) + self._model_combo.setEnabled(True) + else: + self._load_ollama_models() + def _load_ollama_models(self): """Populate the model combo box with models available on the local Ollama server.""" models = fetch_ollama_models() @@ -314,24 +349,35 @@ def _analyse_packet(self): model = self._model_combo.currentText() if not model: - QMessageBox.warning(self, "No model", "No Ollama model selected — click ↻ to refresh.") + QMessageBox.warning(self, "No model", "No model selected.") return pkt = self._captured_packets[row] pkt_text = _format_packet(pkt) user_context = self._user_context.text().strip() + is_anthropic = self._provider_combo.currentText() == "Anthropic" self._llm_window = LlmAnalysisWindow(pkt.summary(), pkt_text, parent=self) self._llm_window.set_analysing() self._llm_window.show() - self._ollama_thread = OllamaThread( - pkt_text, - model, - user_context=user_context, - device_vendor=self._device_vendor, - hostname=self._hostname, - ) + if is_anthropic: + self._ollama_thread = AnthropicThread( + pkt_text, + model, + api_key=self._api_key_edit.text().strip(), + user_context=user_context, + device_vendor=self._device_vendor, + hostname=self._hostname, + ) + else: + self._ollama_thread = OllamaThread( + pkt_text, + model, + user_context=user_context, + device_vendor=self._device_vendor, + hostname=self._hostname, + ) self._ollama_thread.token.connect(self._on_llm_token) self._ollama_thread.error.connect(self._on_llm_error) self._ollama_thread.finished.connect(self._on_llm_finished) diff --git a/core/ollama_analyst.py b/core/llm_analyst.py similarity index 56% rename from core/ollama_analyst.py rename to core/llm_analyst.py index fc7f8fd..50f25b3 100644 --- a/core/ollama_analyst.py +++ b/core/llm_analyst.py @@ -1,12 +1,19 @@ """ -Ollama integration — streams LLM analysis of a captured packet. +LLM integration — streams packet analysis via Ollama or the Anthropic API. """ import json +import os import requests from PySide6.QtCore import QThread, Signal # pylint: disable=E0611 +ANTHROPIC_MODELS = [ + "claude-opus-4-6", + "claude-sonnet-4-6", + "claude-haiku-4-5", +] + OLLAMA_BASE = "http://localhost:11434" OLLAMA_URL = f"{OLLAMA_BASE}/api/generate" @@ -105,3 +112,88 @@ def run(self): self.error.emit(str(e)) finally: self.finished.emit() + + +class AnthropicThread(QThread): + """Streams packet analysis via the Anthropic API. Emits token by token.""" + + token = Signal(str) + finished = Signal() + error = Signal(str) + + def __init__( + self, + packet_text: str, + model: str, + api_key: str = "", + user_context: str = "", + device_vendor: str = "", + hostname: str = "", + parent=None, + ): + super().__init__(parent) + self.packet_text = packet_text + self.model = model + self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY", "") + self.user_context = user_context + self.device_vendor = device_vendor + self.hostname = hostname + + def run(self): + """Stream analysis from the Anthropic API to the token signal.""" + try: + import anthropic # pylint: disable=import-outside-toplevel + except ImportError: + self.error.emit( + "anthropic package not installed — run: pip install anthropic" + ) + self.finished.emit() + return + + if not self.api_key: + self.error.emit( + "No Anthropic API key — set ANTHROPIC_API_KEY or enter it in the UI." + ) + self.finished.emit() + return + + device_section = "" + if self.device_vendor or self.hostname: + device_section = "\nDevice under analysis:" + if self.hostname: + device_section += f"\n Hostname : {self.hostname}" + if self.device_vendor: + device_section += f"\n Vendor : {self.device_vendor}" + device_section += "\n" + + context_section = ( + f"\nAdditional context from analyst:\n{self.user_context}\n" + if self.user_context + else "" + ) + user_message = ( + f"{device_section}{context_section}\nPacket:\n{self.packet_text}" + ) + + try: + client = anthropic.Anthropic(api_key=self.api_key) + with client.messages.stream( + model=self.model, + max_tokens=4096, + system=SYSTEM_PROMPT, + messages=[{"role": "user", "content": user_message}], + ) as stream: + for text in stream.text_stream: + self.token.emit(text) + except anthropic.AuthenticationError: + self.error.emit("Invalid Anthropic API key.") + except anthropic.RateLimitError: + self.error.emit("Anthropic rate limit reached — try again shortly.") + except anthropic.APIConnectionError: + self.error.emit("Cannot reach Anthropic API — check your network.") + except anthropic.APIStatusError as e: + self.error.emit(f"Anthropic API error {e.status_code}: {e.message}") + except Exception as e: # pylint: disable=broad-exception-caught + self.error.emit(str(e)) + finally: + self.finished.emit() diff --git a/requirements.txt b/requirements.txt index 9310eb6..1363ed0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ pylint==3.3.5 PySide6==6.10.1 PySide6_Addons==6.10.1 PySide6_Essentials==6.10.1 +anthropic>=0.40.0 requests==2.33.0 scapy==2.6.1 setuptools==78.1.1