Author: Aimen Bensadia · Email: aimenbensadia118@gmail.com · Date: 2026-05-25
A mini Security Operations Center pipeline — from raw network data to a trained anomaly detector and an LLM-powered alert analyst — built on the CICIDS2017 benchmark dataset.
- What We Built
- Dataset — CICIDS2017
- Step 1 — Exploration
- Step 2 — Cleaning
- Step 3 — Rule-Based Detector
- Step 4 — Isolation Forest
- Step 5 — Random Forest
- Step 6 — LLM Analyst
- Step 7 — Interactive Dashboard
- Final Comparison
- Quick Start
- Project Structure
A full detection pipeline starting from a raw network traffic CSV, ending with three detectors compared head-to-head, an LLM that explains each alert in plain English, and an interactive dashboard to explore them.
Raw CSV → Clean → Rule-Based → Isolation Forest → Random Forest
↓ ↓ ↓
79.8% F1 28.8% F1 99.9% F1
↓
LLM Analyst (llama3.2)
↓
Interactive Dashboard (Streamlit)
The Canadian Institute for Cybersecurity IDS 2017 dataset is a standard benchmark for network intrusion detection. Each row is a network flow — a full conversation between two hosts, summarised as 78 statistical features.
We used the Friday afternoon DDoS file: Friday-WorkingHours-Afternoon-DDos.pcap_ISCX.csv
| Property | Value |
|---|---|
| Total rows | 225,745 |
| Features | 78 numeric + 1 label |
| BENIGN flows | 97,718 (43.3%) |
| DDoS flows | 128,027 (56.7%) |
| Bucket | Columns | What they measure |
|---|---|---|
| Directional | Fwd *, Bwd * |
Traffic from client→server (Fwd) and server→client (Bwd) |
| Volume & Size | Total * Packets, * Length Mean/Std/Max/Min |
How much data and how large the pieces |
| Time & Speed | Flow Duration, * IAT *, Flow Bytes/s, Flow Packets/s |
How fast, how long, gaps between packets |
| TCP Flags | SYN/ACK/FIN/RST Flag Count |
Connection handshake signals |
| Active / Idle | Active *, Idle * |
Periods of activity vs silence within a flow |
Script: explore_cicids2017.py
We compared mean feature values between BENIGN and DDoS flows and found a clear fingerprint:
| Feature | BENIGN | DDoS | Signal |
|---|---|---|---|
Fwd Packet Length Mean |
371 bytes | 7 bytes | Attackers spam tiny packets |
Bwd Packet Length Mean |
117 bytes | 1,481 bytes | Server sends large error responses |
Total Backward Packets |
6.3 | 3.3 | Server starts dropping connections |
The chart below shows why Fwd Packet Length Mean is the smoking gun — the two distributions barely overlap:
DDoS flows cluster hard at 0–10 bytes (tiny flooding packets). Normal traffic spreads across a wide range of meaningful payload sizes. The yellow dashed line is the 20-byte threshold used in the rule-based detector.
Script: clean.py → produces data/friday_clean.csv
| Issue | Cause | Action | Rows affected |
|---|---|---|---|
Negative Flow Duration |
Corrupt packet captures | Dropped | 2 |
inf in Flow Bytes/s / Flow Packets/s |
Division by zero (0-duration flows) | Replaced with NaN → dropped | 34 |
| NaN in key columns | Upstream data gaps | Dropped | 0 |
| Total removed | 36 rows | ||
| Rows kept | 225,709 (99.98%) |
Script: detect.py
A purely statistical baseline — no ML, pure pandas. Learns what "normal" looks like from BENIGN flows, then flags anything outside those bounds. Analogous to: "flag any login from a country this user has never logged in from before."
| Rule | Threshold | How it's set | Signal |
|---|---|---|---|
R1 Fwd Packet Length Mean < 20 bytes |
20 b (domain knowledge) | Minimum meaningful TCP payload | Tiny-packet flood |
R2 Flow Packets/s > 666,667 |
99th pct of BENIGN | Learned from normal traffic | Volumetric rate attack |
R3 Bwd Packet Length Mean > 1,507 bytes |
99th pct of BENIGN | Learned from normal traffic | Server under stress |
An alert fires when any rule triggers.
| Metric | Value |
|---|---|
| Total alerts | 160,293 (71% of flows) |
| False positives | 32,268 |
| Precision | 79.9% |
| Recall | 100.0% |
R1 does 99% of the work. The 32k false positives are legitimate ACK-only and DNS flows that also have tiny forward packet sizes — the rules can't distinguish them from flood traffic.
Script: iforest.py
Trains an IsolationForest on BENIGN flows only — the model has never seen attack labels. It learns to score how "abnormal" each flow is by randomly isolating points in feature space. Anomalies (points that are easy to isolate) get flagged.
iforest = IsolationForest(n_estimators=100, contamination=0.05, random_state=42)
iforest.fit(X_benign) # learns "normal" — never sees any labels
alerts = iforest.predict(X_all) == -1DDoS in CICIDS2017 is a mass, homogeneous attack — 128k flows all look nearly identical. Isolation Forest relies on anomalies being rare and isolated. When an attack forms its own dense cluster, the forest treats it as a second normal region and fails to flag it.
| Metric | Value |
|---|---|
| Total alerts | 28,912 |
| False positives | 4,885 |
| Precision | 83.1% |
| Recall | 18.8% |
Fewer false alarms than the rules, but misses 81% of the attacks. Isolation Forest is better suited for subtle, low-volume anomalies.
Script: rforest.py
A supervised classifier trained on labeled data (80/20 stratified split). It builds 100 decision trees, each voting on whether a flow is an attack.
rf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
rf.fit(X_train, y_train) # sees both BENIGN and DDoS labelsThe model independently discovered the same signals we found in EDA:
| Rank | Feature | Importance | Why |
|---|---|---|---|
| 1 | Total Fwd Packets |
28.1% | DDoS sends many rapid bursts |
| 2 | Fwd Packet Length Mean |
26.4% | The 7-byte smoking gun |
| 3 | Bwd Packet Length Mean |
19.4% | Server stress indicator |
| 4 | Total Backward Packets |
11.1% | Server drop rate |
The top 4 features account for 85% of the model's decisions — consistent with what we found manually in Step 1.
Scripts: analyze.py, make_report.py
For each flagged alert, the raw flow features are sent to a local LLM (llama3.2 via Ollama) with a SOC analyst prompt:
"You are a SOC analyst. Given these network flow logs, give a 3-sentence summary: What happened? Why is it suspicious? What should I check next?"
Results are saved to data/alert_summaries.jsonl. A readable markdown report can be generated with make_report.py.
python analyze.py # analyses 20 sampled alerts (default)
python analyze.py --n 50 # analyse more
python analyze.py --provider groq # use Groq free-tier API instead
python make_report.py # writes data/alert_report.mdAlert — DDoS — R1 + R3 triggered
A network flow was detected with extremely small forward packets (8.7 bytes mean) and unusually large backward packets (2,321 bytes mean), consistent with a DDoS tiny-packet flood. This is suspicious because the asymmetry — tiny requests triggering massive responses — is a classic amplification signature that saturates upstream bandwidth. I would check the source IP against threat intelligence feeds and inspect firewall logs for correlated connection attempts.
Script: dashboard.py
A Streamlit app that brings everything together: alerts ranked by anomaly score, ground-truth labels, triggered rules, and LLM summaries — all in one place. Alerts without summaries can be analysed on demand with a single click.
streamlit run dashboard.py
# Opens at http://localhost:8501Features:
- Alerts ranked by Isolation Forest anomaly score (most suspicious first)
- Filter by rule and ground-truth label
- Click any row to see the full feature table and LLM analysis
- "Generate" button calls llama3.2 on demand and saves the result
Requires Ollama running locally (
ollama serve) with llama3.2 pulled.
All three models evaluated on the same held-out 20% test set (45,142 flows).
| Metric | Rule-Based | Isolation Forest | Random Forest |
|---|---|---|---|
| Total alerts | 32,101 | 5,446 | 25,602 |
| True positives | 25,605 | 4,476 | 25,590 |
| False positives | 6,496 | 970 | 12 |
| Missed attacks | 0 | 21,129 | 15 |
| Precision | 79.8% | 82.2% | 100.0% |
| Recall | 100.0% | 17.5% | 99.9% |
| F1 | 88.7% | 28.8% | 99.9% |
Rule-Based — perfect recall, but 6,496 false alarms. Great as a first-pass filter or when you have no labels. Fragile: a new attack type it wasn't tuned for would be completely missed.
Isolation Forest — fewest false alarms, but only catches 1 in 5 attacks. Works best for subtle, low-volume anomalies. Fails against dense, uniform floods.
Random Forest — wins on every metric simultaneously. Near-perfect precision and recall. The cost: it requires labeled training data. In a real SOC, you'd seed it with known attack signatures and retrain as new threats emerge.
The right architecture is all three in a pipeline:
- Isolation Forest for unknown/novel anomalies
- Rules for high-confidence known patterns
- Random Forest as the final classifier when you have labels
- LLM analyst for human-readable triage on every alert
pip install pandas numpy scikit-learn matplotlib streamlitFor the LLM analyst, install Ollama and pull a model:
ollama pull llama3.2python explore_cicids2017.py # optional EDA
python clean.py # data/friday_clean.csv
python detect.py # rule-based baseline
python iforest.py # isolation forest
python rforest.py # random forest + comparison
python visualize.py # plots/
python analyze.py # LLM summaries
python make_report.py # data/alert_report.md
streamlit run dashboard.py # http://localhost:8501The raw CSV (Friday-WorkingHours-Afternoon-DDos.pcap_ISCX.csv) is not committed. Download it via Kaggle:
# place ~/.kaggle/kaggle.json first (from Kaggle account settings)
python explore_cicids2017.py # auto-downloads on first runMiniSOC/
├── explore_cicids2017.py # download + load + explore the dataset
├── clean.py # remove infinities, nulls, corrupt rows
├── detect.py # rule-based anomaly detector (pure pandas)
├── iforest.py # isolation forest vs rule-based comparison
├── rforest.py # random forest + full three-way comparison
├── visualize.py # generate all plots
├── analyze.py # LLM analyst — per-alert SOC summaries
├── make_report.py # convert JSONL summaries to markdown report
├── dashboard.py # Streamlit alert dashboard
├── data/
│ ├── friday_clean.csv (generated by clean.py)
│ ├── alert_summaries.jsonl (generated by analyze.py)
│ └── alert_report.md (generated by make_report.py)
└── plots/
├── 1_label_distribution.png
├── 2_fwd_packet_distribution.png
├── 3_model_comparison.png
├── 4_feature_importance.png
└── 5_confusion_matrices.png




