· ~4 min read
A network log analysis agent that keeps the evidence in-house
Detecting beaconing, DNS tunnelling, lateral movement and exfiltration in network logs with plain statistics, then letting a small local LLM write the summary.
Goal
My second project for the Digital Forensics course of my MSc was a tool for the first hour of an incident: take a batch of network and host logs, find what does not look like normal traffic, and produce a summary an analyst can act on. The constraint I set myself was that nothing leaves the machine. Logs from a real incident are confidential, so the language model runs locally.
The input was a set of synthetic course datasets (network flows, DNS queries, email and host records): 12 files and 99 events around one simulated compromised workstation.
Architecture
| Part | Role |
|---|---|
| Streamlit UI | Batch upload, a Sankey diagram of the flows, a jitter gauge, an entropy scatter plot and report export |
| FastAPI backend | Detects CSV or JSON, computes the statistics, and packs the findings into a compact summary for the model |
| Ollama with qwen2:0.5b | Runs on the CPU, writes the incident narrative; no external API and no token cost |
The important split is that detection is deterministic code. The model never decides what is malicious; it only turns already-computed findings into prose.
Detection rules
| Behaviour | Rule |
|---|---|
| DNS tunnelling | Subdomain longer than 35 characters and Shannon entropy above 3.5 bits per character |
| Exfiltration to cloud storage | More than 1 MB outbound in one connection to an external storage endpoint |
| C2 beaconing | Repeated connections to the same external address at a steady interval |
| Lateral movement | Internal-to-internal connections on SMB (445), SSH (22) or RDP (3389) |
Entropy is what makes encoded data stand out in DNS names. Ordinary names such as update.microsoft.com sit below about 2.8; the suspicious queries in the dataset were between 3.9 and 4.7. The calculation is small:
from collections import Counter
from math import log2
def shannon_entropy(label: str) -> float:
counts = Counter(label)
return -sum(n / len(label) * log2(n / len(label)) for n in counts.values())
Making a small model useful
| Version | Model | Input | Tokens | Latency | Result |
|---|---|---|---|---|---|
| v1 | Gemini Flash (cloud) | Raw JSON of every flow | 1,250 | 1.85 s | Works, but expensive and the data leaves the network |
| v2 | Gemini Flash (cloud) | Pre-aggregated categories | 115 | 0.92 s | Cheap, still cloud |
| v3 | qwen2:0.5b (local) | Open-ended prompt | 95 | 32.53 s | Repeated words |
| v4 | qwen2:0.5b (local) | Instruct format, repeat_penalty 1.25 | 65 | 31.58 s | Usable, fully on-premises |
Two changes did most of the work: sending the model counts per category instead of raw flows, and a strict instruct-style prompt asking for a two-sentence executive summary.
What I learned
- The model is the typist, not the detector. Even v4 added a fragment and a time window that were not in its input. With a 0.5B model that is expected; it is also why its text must never be the only record of a finding.
- Check the statistics before anything sits on top of them. Re-reading my report, a few numbers do not reconcile: the category percentages do not match the event counts, and the mean beacon intervals are orders of magnitude longer than the capture window, which points to a units bug. The deterministic layer deserves the same scrutiny as the model.
- Beaconing needs a better rule. Two connections at a steady rate is far too low a bar. A minimum number of connections plus a low coefficient of variation of the intervals (σ/μ) is the more robust test.
- Entropy alone is noisy. CDNs and cloud services use long random-looking names too. Combining entropy with per-domain query volume and the share of TXT records cuts false positives.
- Local inference is slow but private. About 30 seconds on a CPU is acceptable for a summary written once per incident, and the logs never leave the building.
A course project for the MSc in Cybersecurity & Data Science at the University of Piraeus, on synthetic datasets.
Want the configs or code from this lab, or have a question? Email me at kostaspapas1023@gmail.com