<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Furkan Barış — machine learning for network security</title><description>Notes on machine learning for cybersecurity — anomaly detection, phishing detection, and retrieval-augmented generation. Written by Furkan Barış.</description><link>https://ffurkanbaris.pages.dev</link><language>en-us</language><item><title>Hello, world</title><link>https://ffurkanbaris.pages.dev/posts/hello-world</link><guid isPermaLink="true">https://ffurkanbaris.pages.dev/posts/hello-world</guid><description>Why I am starting a blog, and what I plan to put on it.</description><pubDate>Sun, 30 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I have spent the last two years training models on security data — network captures from
medical devices, phishing URLs, documents fed into a retrieval system. Most of what I learned
in that time never made it out of a notebook or a thesis draft. This site is where I fix that.&lt;/p&gt;
&lt;h2 id=&quot;what-goes-here&quot;&gt;What goes here&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#what-goes-here&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Three kinds of posts, roughly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Working notes.&lt;/strong&gt; The thing I tried, the number it produced, and whether I believed the
number. Failed experiments count — they are usually the more useful half.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explanations.&lt;/strong&gt; Methods I had to read three papers to understand, written the way I wish
someone had written them for me.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build logs.&lt;/strong&gt; Shipping a RAG platform to production taught me more about retrieval than any
benchmark did. That kind of thing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What will &lt;em&gt;not&lt;/em&gt; be here: tutorials rewritten from documentation, or benchmark numbers without the
setup that produced them.&lt;/p&gt;
&lt;h2 id=&quot;on-numbers&quot;&gt;On numbers&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#on-numbers&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Every accuracy figure I publish comes with the dataset, the split, and the class balance. This
matters more in security than almost anywhere else, because the interesting class is always the
rare one:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light&quot; style=&quot;background-color:#22272e;--shiki-light-bg:#fff;color:#adbac7;--shiki-light:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#768390;--shiki-light:#6A737D&quot;&gt;# An &quot;accurate&quot; model on a realistic intrusion dataset.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7;--shiki-light:#24292E&quot;&gt;attacks &lt;/span&gt;&lt;span style=&quot;color:#F47067;--shiki-light:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF;--shiki-light:#005CC5&quot;&gt; 1_000&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7;--shiki-light:#24292E&quot;&gt;benign  &lt;/span&gt;&lt;span style=&quot;color:#F47067;--shiki-light:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#6CB6FF;--shiki-light:#005CC5&quot;&gt; 99_000&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ADBAC7;--shiki-light:#24292E&quot;&gt;predict_benign_always &lt;/span&gt;&lt;span style=&quot;color:#F47067;--shiki-light:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7;--shiki-light:#24292E&quot;&gt; benign &lt;/span&gt;&lt;span style=&quot;color:#F47067;--shiki-light:#D73A49&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7;--shiki-light:#24292E&quot;&gt; (benign &lt;/span&gt;&lt;span style=&quot;color:#F47067;--shiki-light:#D73A49&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7;--shiki-light:#24292E&quot;&gt; attacks)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6CB6FF;--shiki-light:#005CC5&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7;--shiki-light:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F47067;--shiki-light:#D73A49&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#96D0FF;--shiki-light:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#F47067;--shiki-light:#005CC5&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7;--shiki-light:#24292E&quot;&gt;predict_benign_always&lt;/span&gt;&lt;span style=&quot;color:#F47067;--shiki-light:#D73A49&quot;&gt;:.3f&lt;/span&gt;&lt;span style=&quot;color:#F47067;--shiki-light:#005CC5&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#96D0FF;--shiki-light:#032F62&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ADBAC7;--shiki-light:#24292E&quot;&gt;)  &lt;/span&gt;&lt;span style=&quot;color:#768390;--shiki-light:#6A737D&quot;&gt;# 0.990 — and it catches nothing&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ninety-nine percent accuracy, zero detections. So when I write down &lt;code&gt;0.97&lt;/code&gt;, I will also write down
what it was measured against, and what it looked like on the minority class.&lt;/p&gt;
&lt;h2 id=&quot;elsewhere&quot;&gt;Elsewhere&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#elsewhere&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The code behind most of what I write about is on
&lt;a href=&quot;https://github.com/ffurkanbaris&quot;&gt;GitHub&lt;/a&gt;. If something here is wrong or unclear, my inbox is
open — the address is on the &lt;a href=&quot;https://ffurkanbaris.pages.dev/about&quot;&gt;about&lt;/a&gt; page.&lt;/p&gt;</content:encoded><category>meta</category></item><item><title>One-class detection for IoMT network traffic</title><link>https://ffurkanbaris.pages.dev/posts/one-class-detection-for-iomt-traffic</link><guid isPermaLink="true">https://ffurkanbaris.pages.dev/posts/one-class-detection-for-iomt-traffic</guid><description>Why intrusion detection on medical device networks is an anomaly detection problem, not a classification one.</description><pubDate>Mon, 24 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Internet of Medical Things networks have a property that makes supervised intrusion detection
awkward: you can collect as much normal traffic as you like, and almost none of the other kind.
An infusion pump talking to a monitoring server produces the same handful of patterns for months.
The attack you actually care about has not happened yet — and when it does, it will probably not
look like anything in your training set.&lt;/p&gt;
&lt;p&gt;This is the setting for my thesis, and it pushed me away from classification and toward one-class
modelling.&lt;/p&gt;
&lt;h2 id=&quot;why-not-just-train-a-classifier&quot;&gt;Why not just train a classifier&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#why-not-just-train-a-classifier&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The obvious approach is to label traffic as benign or malicious and train a binary classifier.
Two things go wrong.&lt;/p&gt;
&lt;p&gt;The first is arithmetic. Attack traffic is rare, so accuracy stops meaning anything — a model
that predicts “benign” for everything scores well and detects nothing. I wrote about that
&lt;a href=&quot;https://ffurkanbaris.pages.dev/posts/hello-world&quot;&gt;in the first post on this site&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The second is worse. A supervised model learns the boundary between &lt;em&gt;the normal traffic you have&lt;/em&gt;
and &lt;em&gt;the specific attacks you have&lt;/em&gt;. Deploy it against a technique that was not in the training
set and it has no reason to flag anything. You have built a detector for yesterday’s attacks.&lt;/p&gt;
&lt;p&gt;One-class models invert the question. Instead of learning what an attack looks like, they learn a
tight boundary around normal behaviour and treat everything outside it as suspicious. You never
have to enumerate the attacks. That fits the data you can actually collect from a hospital
network.&lt;/p&gt;
&lt;h2 id=&quot;turning-flows-into-small-images&quot;&gt;Turning flows into small images&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#turning-flows-into-small-images&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I used DROCC — Deep Robust One-Class Classification — with a convolutional backbone, which means
the model wants something image-shaped as input. Network flow records are not image-shaped; they
are a row of numbers.&lt;/p&gt;
&lt;p&gt;So each flow’s 36 features get reshaped into a 6×6 single-channel grid and treated as a tiny
grayscale image. Convolution then sees small neighbourhoods of features at once rather than one
long flat vector.&lt;/p&gt;
&lt;p&gt;This is a real modelling decision, not just plumbing: a convolution over a 6×6 grid only ever
mixes features that ended up adjacent, so &lt;strong&gt;the ordering of the columns matters&lt;/strong&gt;. Features that
belong together should sit together. It is the part of the pipeline I would poke at first if the
numbers were disappointing.&lt;/p&gt;
&lt;h2 id=&quot;the-collapse-problem&quot;&gt;The collapse problem&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#the-collapse-problem&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;One-class objectives have an obvious degenerate solution. If the only instruction is “map normal
data somewhere consistent”, the network can satisfy it by mapping &lt;em&gt;everything&lt;/em&gt; — normal traffic,
attacks, random noise — to a single point. Loss goes to zero. The model has learned nothing. This
is representation collapse, and it is the reason naive one-class training often produces something
that looks trained and detects nothing.&lt;/p&gt;
&lt;p&gt;DROCC’s answer is to make the trivial solution impossible. It assumes normal points lie on a
locally linear, low-dimensional manifold, then generates adversarial negative points by gradient
ascent in a shell &lt;em&gt;just outside&lt;/em&gt; each training point — close enough to be plausible, far enough to
be wrong. The model is trained to separate real points from those synthetic near-misses.&lt;/p&gt;
&lt;p&gt;A constant function cannot do that. To push the fake points away while keeping the real ones, the
boundary has to actually hug the shape of normal traffic. That is the mechanism, and it is what
makes the approach work at all rather than just appear to.&lt;/p&gt;
&lt;h2 id=&quot;results&quot;&gt;Results&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#results&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Two public datasets, one from IoMT device traffic and one from a healthcare monitoring testbed:&lt;/p&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Dataset&lt;/th&gt;&lt;th&gt;Domain&lt;/th&gt;&lt;th&gt;Accuracy&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;CIC-IoMT-2024&lt;/td&gt;&lt;td&gt;IoMT device traffic&lt;/td&gt;&lt;td&gt;0.92&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;WUSTL-EHMS-2020&lt;/td&gt;&lt;td&gt;Healthcare monitoring system&lt;/td&gt;&lt;td&gt;0.97&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The gap between the two is the interesting part. WUSTL-EHMS-2020 comes from a smaller, more
constrained testbed, so “normal” is a tighter target and the boundary can be drawn closer around
it. CIC-IoMT-2024 covers a wider variety of devices and behaviours, so normal is a bigger, messier
region — and a bigger region is harder to wrap tightly without letting attacks inside it.&lt;/p&gt;
&lt;p&gt;That is the general shape of the tradeoff in one-class detection. The more varied your normal
traffic, the looser your boundary, and the more attacks slip through it.&lt;/p&gt;
&lt;h2 id=&quot;what-i-would-want-next&quot;&gt;What I would want next&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#what-i-would-want-next&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Accuracy is the headline number, but it is not the number that decides whether a detector is
deployable. For an intrusion detection system what matters is the false positive rate at a useful
detection rate — a model that flags 3% of a hospital’s normal traffic is switched off in a week,
whatever its accuracy is. A full ROC curve, and per-attack-family recall, would say much more than
a single figure.&lt;/p&gt;
&lt;p&gt;Code: &lt;a href=&quot;https://github.com/ffurkanbaris/anomaly-detection&quot;&gt;github.com/ffurkanbaris/anomaly-detection&lt;/a&gt;&lt;/p&gt;</content:encoded><category>anomaly-detection</category><category>security</category><category>pytorch</category></item><item><title>Three branches and an attention layer: QR phishing detection</title><link>https://ffurkanbaris.pages.dev/posts/cross-modal-attention-for-qr-phishing</link><guid isPermaLink="true">https://ffurkanbaris.pages.dev/posts/cross-modal-attention-for-qr-phishing</guid><description>Combining a Transformer lexical encoder with URL and QR structural features, and letting attention decide what matters.</description><pubDate>Tue, 18 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A malicious QR code hides its URL behind a picture. That is the whole problem. A phishing link in
an email can be read, hovered over, and doubted before it is clicked; the same link inside a QR
code is a grid of squares until a phone has already decoded it and opened the page. The usual
defence — look at where it goes before you go there — is gone.&lt;/p&gt;
&lt;p&gt;So the detector has to work on what is available before the user commits. The question I wanted to
answer was where that signal actually lives: in the URL string, in the structure of the code
itself, or in the relationship between the two.&lt;/p&gt;
&lt;h2 id=&quot;three-branches&quot;&gt;Three branches&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#three-branches&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The answer I settled on was to not choose. PhishFusion runs three encoders in parallel, one per
view of the same object:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Branch&lt;/th&gt;&lt;th&gt;Input&lt;/th&gt;&lt;th&gt;Encoder&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Lexical&lt;/td&gt;&lt;td&gt;URL character sequence&lt;/td&gt;&lt;td&gt;Transformer encoder&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;URL&lt;/td&gt;&lt;td&gt;Structural URL features&lt;/td&gt;&lt;td&gt;MLP&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;QR&lt;/td&gt;&lt;td&gt;QR structural features&lt;/td&gt;&lt;td&gt;MLP&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The lexical branch reads the URL as a character sequence. This is where the classic phishing tells
live — brand names spliced into subdomains, character substitutions, absurd path depth, hyphen
soup. A Transformer suits it because those patterns are positional and contextual: &lt;code&gt;paypal&lt;/code&gt; early
in a hostname means something different from &lt;code&gt;paypal&lt;/code&gt; buried in a path segment, and self-attention
can represent that distinction directly.&lt;/p&gt;
&lt;p&gt;The two structural branches are deliberately not sequence models. They take engineered features —
counts, lengths, ratios, encoding properties — and an MLP is the right tool for a fixed-width
feature vector. Handing those to a Transformer would be architecture for its own sake.&lt;/p&gt;
&lt;p&gt;Each branch emits a 32-dimensional embedding. Three views, one common width.&lt;/p&gt;
&lt;h2 id=&quot;why-attention-instead-of-concatenation&quot;&gt;Why attention instead of concatenation&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#why-attention-instead-of-concatenation&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The cheap way to combine three embeddings is to glue them into a 96-dimensional vector and put a
classifier on top. That works, and it is the baseline any fusion model has to beat.&lt;/p&gt;
&lt;p&gt;What it cannot do is weight the branches &lt;em&gt;per sample&lt;/em&gt;. Concatenation learns one fixed set of
weights over the three views and applies it to every input. But the views are not equally
informative for every URL. A shortened link is lexically almost empty — there is nothing to read,
which is itself the signal — and the structural branches have to carry the decision. A long,
hand-crafted lookalike hostname is the opposite case: the lexical branch has plenty to work with.&lt;/p&gt;
&lt;p&gt;Cross-modal attention across the three 32-dimensional embeddings lets each branch attend to the
others, so the model can learn &lt;em&gt;conditional&lt;/em&gt; relationships — a suspicious QR structure matters more
when the URL is also unusual, and an unremarkable URL is less reassuring when the code it came
from is malformed. The weighting is computed from the input rather than fixed at training time.&lt;/p&gt;
&lt;p&gt;That interaction is the actual claim of the architecture. Whether it earns its complexity is an
empirical question, and the honest way to answer it is single-branch and concatenation baselines
measured on the same split.&lt;/p&gt;
&lt;h2 id=&quot;results&quot;&gt;Results&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#results&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;0.96 binary classification accuracy on an imbalanced phishing dataset.&lt;/p&gt;
&lt;p&gt;The word doing the work in that sentence is &lt;em&gt;imbalanced&lt;/em&gt;, and it is worth being blunt about what
the number therefore does not tell you. On a skewed set, accuracy is dominated by the majority
class; a model can look strong while missing a large share of the phishing it was built to catch.
Precision and recall on the phishing class are the figures that decide whether the thing is
useful, and they are what I would put in front of the accuracy if I were presenting this as a
deployable system rather than a study.&lt;/p&gt;
&lt;p&gt;I am stating the metric I measured, not the metric I wish I had measured. That distinction seems
worth keeping.&lt;/p&gt;
&lt;h2 id=&quot;what-the-setup-does-not-cover&quot;&gt;What the setup does not cover&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#what-the-setup-does-not-cover&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Two limits worth naming. The model sees a decoded URL plus structural features — it never sees the
page, so a phishing site that hides behind a clean-looking domain and a redirect chain is out of
scope. And QR phishing in the wild is largely a physical-world attack: stickers over legitimate
codes on parking meters, restaurant tables, EV chargers. None of the context that makes those
convincing is in the dataset.&lt;/p&gt;
&lt;p&gt;A detector like this belongs in the layer that inspects a link before the browser follows it, not
as the last line of defence.&lt;/p&gt;
&lt;p&gt;Code: &lt;a href=&quot;https://github.com/ffurkanbaris/phishfusion&quot;&gt;github.com/ffurkanbaris/phishfusion&lt;/a&gt;&lt;/p&gt;</content:encoded><category>phishing</category><category>transformers</category><category>pytorch</category></item></channel></rss>