Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 29 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.
Expand All @@ -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

---

Expand Down Expand Up @@ -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.

---

Expand All @@ -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
Expand Down
68 changes: 57 additions & 11 deletions core/arp_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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")
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
94 changes: 93 additions & 1 deletion core/ollama_analyst.py → core/llm_analyst.py
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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()
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading