· ~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

PartRole
Streamlit UIBatch upload, a Sankey diagram of the flows, a jitter gauge, an entropy scatter plot and report export
FastAPI backendDetects CSV or JSON, computes the statistics, and packs the findings into a compact summary for the model
Ollama with qwen2:0.5bRuns 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

BehaviourRule
DNS tunnellingSubdomain longer than 35 characters and Shannon entropy above 3.5 bits per character
Exfiltration to cloud storageMore than 1 MB outbound in one connection to an external storage endpoint
C2 beaconingRepeated connections to the same external address at a steady interval
Lateral movementInternal-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

VersionModelInputTokensLatencyResult
v1Gemini Flash (cloud)Raw JSON of every flow1,2501.85 sWorks, but expensive and the data leaves the network
v2Gemini Flash (cloud)Pre-aggregated categories1150.92 sCheap, still cloud
v3qwen2:0.5b (local)Open-ended prompt9532.53 sRepeated words
v4qwen2:0.5b (local)Instruct format, repeat_penalty 1.256531.58 sUsable, 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

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

← all posts