top of page

The Most Anomalous Flows Weren't The Attacks

  • Jul 17
  • 6 min read

I built an unsupervised model to flag suspicious traffic in 2.8 million network flows — no labels, no cloud account, just SQL and Python. The surprise wasn't whether it worked. It was where the attacks were hiding.

The result, up front

One tuning decision moved both numbers the right way — here's the before and after.


Precision · flagged that were real Recall · attacks that got caught

34.8%48.4% 14.1%36.9%


Trained blind on 2.8M flows from CIC-IDS-2017. Labels touched nothing during training — they were used only afterward to grade what the model caught. Tuned for precision, it still surfaced flows from all 14 attack types in the dataset.


Here's the throughline I keep coming back to: I spent years finding the one cell in a weather grid that didn't belong. Now I find the one flow on a network that doesn't belong.


This project is the bridge piece in my move from data analytics into security. No Azure account, no infrastructure to stand up — just the two tools a data analyst already uses every day. What does that skill set actually buy in a detection role? The answer turned out to be more interesting than a clean accuracy number.


The tool changes, the question doesn't: which one of these doesn't belong, and why?


01 / THE SETUP

Finding attacks without being told what they are


On a live network, labeled attack data is a luxury you seldom have. So the honest question isn't "can I classify known attacks" — it's can I find suspicious behavior without knowing in advance what suspicious looks like. That's an unsupervised problem, and it's exactly the shape of work a data analyst is trained for.


The dataset is CIC-IDS-2017 — about 2.8 million network flows captured over a work week. Monday is entirely benign. The rest of the week layers in brute force, denial-of-service, web attacks, infiltration, a botnet, port scanning, and DDoS. Each flow carries 78 numeric features describing its shape: duration, packet rates, byte counts, flag counts, and so on.

The pipeline is four stages, and the whole thing runs on a laptop:


SQL  load + clean + feature table.  IsolationForest, blind to labels.  score + flag every flow.  validate against the label


The discipline that makes it honest

The label column is dropped before the model ever sees the data. It comes back only at the very end, to grade the model's guesses. If labels leaked into training, the whole result would be theater. They didn't.

02 / THE SQL

Where a decade of SQL earns its keep


Before any machine learning, the data has to become trustworthy and shaped for the model. This is the part I didn't have to learn for this project — it's the part I already knew. I learned this from my AI-Aided Cybersecurity course from ELVTR.


The raw CSVs ship with quirks: column names padded with stray spaces, a duplicated header, and rate columns that contain the literal strings Infinity and NaN on zero-duration flows.


The cleaning step drops the non-finite rows and hands the model a table of interpretable, per-flow features — the ones that actually hint at why a flow looks wrong:


CREATE TABLE flow_features AS
SELECT
    flow_id,
    flow_duration,
    flow_packets_s,
    fwd_packets_s,
    packet_length_mean,
    syn_flag_count,
    ack_flag_count,
    down_up_ratio,
    init_win_bytes_forward,
    -- ...compact, interpretable subset
    label            -- carried for VALIDATION ONLY
FROM flows_clean;

I chose a compact set of features on purpose instead of throwing all 78 at the model. When the goal is an explainable flag — one a human analyst can act on — features you can reason about beat a black box every time.

03 / THE MODEL

Isolation Forest, in one plain paragraph


An Isolation Forest works by trying to isolate each data point with random splits. Normal points sit in dense neighborhoods and take many splits to separate. Anomalies sit alone and get isolated in just a few. Fewer splits to isolate means more anomalies. That's the whole intuition — no labels required, which is exactly why it fits the problem.


I trained it on a 200,000-flow sample (plenty to learn the shape of "normal"), then scored all 2.8 million. Then I asked the labels a simple question: of the flows you flagged, how many were real attacks — and of all the attacks, how many did you catch?


The first run, on the default setting, was fine but noisy: 34.8% precision, 14.1% recall. Good enough to prove the concept. Not good enough to stop there. So I went looking for a better threshold — and that's where the interesting part showed up.

04 / THE SURPRISE

The most anomalous flows were the innocent ones


The standard move to raise precision is to flag fewer flows — tighten the net to only the most extreme outliers. I swept the contamination setting (the fraction of flows the model is allowed to flag) across a wide range and watched precision. It did the opposite of what the textbook predicts:


Contamination

Precision

Recall

0.02

2.9%

0.3%

0.05

20.5%

5.2%

0.08

34.4%

14.0%

0.10

40.5%

20.6%

0.15  ◆

49.9%

38.1%

0.20

44.4%

45.1%

0.30

32.8%

50.1%


Read the top of that table again. When the model flagged only the most anomalous 2% of flows, precision cratered to 2.9%. Almost everything in the extreme tail was a false positive. Precision climbed as I let the model flag more, peaking at 15%, then fell off again.


Here's what that shape is telling us: the flows the model scored as most anomalous were disproportionately benign oddballs — weird but harmless traffic. The real attacks weren't the strangest thing on the network. They lived in the moderate-anomaly zone, a notch in from the extreme.



Anomaly scores, benign vs. attack. Lower = more anomalous. Note how benign traffic (teal) reaches furthest into the anomalous tail — the attacks (purple) sit a notch in from the extreme.



Precision (teal) rises to a peak near contamination = 0.15, then declines — the opposite of the usual "flag less to gain precision" tradeoff.


An unsupervised model learns "unusual," not "malicious." On this network, the most unusual traffic wasn't the attack traffic.


That gap — between unusual and malicious — is the entire job of a detection analyst. The model narrows the haystack. The human decides what's actually a needle. Tuning taught me where the needles clustered, and that's a more useful thing to know than any single accuracy score.

05 / WHAT IT CAUGHT

Tuned for precision, still caught everything


Setting contamination to 0.15 nearly doubled precision (34.8% → 48.4%) and more than doubled recall (14.1% → 36.9%) versus the default. Both numbers moved the right way at once — which only happened because the tuning pulled the net back from the false-positive-heavy extreme tail toward where the attacks actually lived.


And breadth held. Even tuned tight for precision, the model surfaced flows from every one of the 14 attack categories — from the massive classes down to the rare ones:



The honest caveat

About half the flagged flows were still false positives. That's normal for unsupervised detection with zero labels — and it's exactly why a real SOC pairs automated flagging with human triage. The model hands you a short list worth looking at. It doesn't hand you a verdict.

06 / THE SKEPTIC'S PASS

What I'd question before trusting this


Here's a thing worth saying plainly: a result is only as good as the data under it, and CIC-IDS-2017 has known, peer-reviewed problems. A responsible analyst names them before an employer has to ask.


The most important one is the labels I'm grading against. When researchers reconstructed and relabeled the dataset, they found attacks that were carried out but never labeled, along with duplicated and misordered packets in the capture. Follow-up work put the label-corruption rate around 6.7% overall, with a few attack classes far higher. That matters directly for my numbers: precision and recall are measured against these labels — so some portion of what I'm counting as a "false positive" may be a mislabeled attack my model actually caught. I can't quantify how much without the corrected dataset, but it means my true precision is probably a little better than 48.4%, not worse.


The feature values have caveats too — analysts have documented flow-construction and feature-calculation errors upstream of any model. And more simply: a single week of 2017 traffic is a snapshot, and networks don't hold still. None of this invalidates the exercise. It just means I'd treat this as a proving ground for the method, and revalidate on corrected or live data before trusting any specific number.


Why this belongs in the writeup

Knowing a dataset's limits is part of the analysis, not an apology for it. The skill I'm showing here isn't "I got a number" — it's "I know exactly how far to trust the number, and why."

07 / THE POINT

What a data analyst brings to a SOC


No cloud environment. No labeled attack corpus. No specialized tooling. The whole investigation ran on SQL and Python — and it produced a working, explainable, tuned detector plus a genuine finding about where attacks hide in the anomaly distribution.

That's the case I wanted to make.


The pipeline instinct, the feature reasoning, the discipline of not letting labels leak, the willingness to chase a weird curve until it explains itself — those transfer directly. Detection work is data work with the stakes turned up.


Storm-to-SOC: same instinct, different domain.



Read the code

Every stage — the SQL, the model, the tuning sweep that produced those figures — is on GitHub, committed step by step. Clone it, point it at CIC-IDS-2017, and reproduce the whole thing.







Comments


Let's learn this together. Have a question, a better query, or just want to say hi? Drop a line below.

© 2026 by DataSec Chronicles. Data-Inspired, Instinct-Driven.    Privacy Policy    Terms & Conditions

bottom of page