Zeek Workshop · Berkeley September 10–11, 2026 — Register →

How Attackers Learn Your Network Before They Attack It

by Aashish Sharma

Published:

Almost no attack begins with an exploit. It begins with a question: What’s out there?

Before anyone tries a default password on your VPN gateway or throws an exploit at an unpatched service, they have to find the gateway and the service. That discovery phase, reconnaissance, is the most predictable stage of an intrusion, which makes it the cheapest place to catch one. An attacker can hide their exploit in encrypted traffic, but they cannot hide the fact that they had to go looking first.

Think of it the way a burglar works a neighborhood. Before breaking into a house, they walk the street: which houses have lights on, which doors are locked, which windows are open a crack. Any single action is innocent on its own; anyone can walk past a house or try a doorknob once. What gives the burglar away is the pattern: one person trying fifty doorknobs on one street, or checking every window on a single house. Network scanning is exactly this, and scan detection is the neighbor who notices.

Zeek ships an answer to this problem in its package ecosystem: Simple Scan (bro-simple-scan), a deliberately small scan detector. The whole thing is one script of about 300 lines, written to be readable. It’s a great default, and understanding what it does (and doesn’t do) is the best way to understand scan detection in general.

What Simple Scan Actually Catches

The first thing to understand is what Simple Scan counts. It doesn’t inspect payloads or look for “scanner-like” packets. It watches for one thing: failed TCP connections.

event connection_attempt(c: connection)
    {
    if ( c$history == "S" || c$history == "SW")
        add_scan(c$id);
    }

event connection_rejected(c: connection)
    {
    if ( c$history == "Sr" || c$history == "SWr")
        add_scan(c$id);
    }

 

That history-string check is doing real work. S means someone sent a SYN and got silence. Sr means they sent a SYN and got rejected with a RST. Both mean the same thing: somebody tried to talk to a host-and-port combination and nothing was there to answer. Legitimate clients mostly connect to things that exist—they got the address from DNS, a link, or a config file. Scanners, by definition, don’t know what exists yet. Failure is the signature of ignorance, and ignorance is the signature of reconnaissance.

Each failure is recorded as a unique (victim, port) pair per source. When one source accumulates 25 unique host+port failures (the default scan_threshold; 250 for hosts inside your own network, which are noisier), Simple Scan fires a notice. What kind of notice depends on the shape of the failure set:

Address scanning (Scan::Address_Scan): One source, many hosts, one port (or a handful of ports). This is the burglar trying the same doorknob on every house: someone sweeping your address space looking for anything listening on, say, 22 or 445.

Port scanning (Scan::Port_Scan): One source, one host, many ports. The burglar circling a single house checking every window: someone has picked a target and is inventorying its services.

Random scanning (Scan::Random_Scan): Many hosts and many ports, no clean pattern. Some scanners deliberately randomize their probe order to look less like the first two shapes. If the failure set doesn’t collapse to a few ports or a few victims, it’s called random.

The resulting notice is plain-spoken. From this package’s own test suite:

notice.log Scan::Port_Scan 192.168.2.154 scanned at least 5 unique ports on host 192.168.2.22 in 0m0s

 

There are two refinements worth knowing about, because they hint at where scan detection is headed (and where this series is going):

Darknets make detection faster. Simple Scan depends on the bro-is-darknet package, which lets you declare which parts of your address space are unused. Nothing legitimate ever connects to an unused address, since there’s nothing there to have business with. So once a source touches 3 darknet hosts, Simple Scan stops giving it the benefit of the doubt and drops its threshold from 25 failures to 10. The scanner convicts itself faster by touching addresses that shouldn’t receive traffic at all.

“Knock-knock” detection catches focused sweeps. Separately from the main threshold, if a remote source fails against 20 unique hosts on the same single port (only 3, if it’s also hitting darknet), that alone fires a notice. This catches an extremely common real-world case: a botnet sweeping the internet for one specific service, even when the total failure count is still modest.

Once a source trips a notice, it goes on a known-scanners list and is ignored for the suppression interval (an hour by default), so one loud scanner doesn’t bury you in ten thousand duplicate alerts.

A Different Way to Think About Detection

Everything discussed so far is threshold thinking: count a source’s failures and alert when the count is high enough. It works, and it’s cheap. But notice what the darknet feature is really doing, because it’s a different idea wearing a threshold costume.

The darknet check works because you told Zeek something about your network: these addresses are empty. With that one fact, a single connection becomes meaningful on its own. You don’t need 25 failures to be suspicious of traffic to an address where nothing has ever lived—you need one. The evidence isn’t the volume, it’s the implausibility.

Now extend that idea. You know far more about your network than which addresses are empty:

  • Which hosts actually exist and are active.
  • Which ports each host legitimately serves. Your mail server speaks 25 and 993; it has never spoken 3389.
  • Who normally talks to whom. Your database tier gets connections from the app tier, not from residential ISP space in another country.

Build that profile (from Zeek’s own logs, which already record every connection) and detection inverts. Instead of asking “Has this source failed enough times to look like a scanner?”, you ask “Does this connection make sense for a client that already knows my network?”

A SYN to a port that no host in your network serves is a strong signal at count one. A connection to a real service from a peer that has talked to it every day for a year is unremarkable at count one million.

This is profile-based (or knowledge-based) detection, and it’s the conceptual leap the rest of this series builds on. Simple Scan contains the seed of it: the darknet integration and the knock-knock heuristic are both “this shouldn’t happen, regardless of count” rules, but it only encodes one fact about your network. There is a lot more you could tell your detector.

Signals Beyond the Connection Itself

So far every signal has lived inside connection records: SYNs, RSTs, host+port pairs. But some of the strongest reconnaissance indicators come from context around the connection—facts about the world that make traffic implausible even when each packet looks fine.

Geographic implausibility. If credentials for one of your users authenticate from your campus at 9:00 and from another continent at 9:20, no threshold math is needed. Physics already ruled it out. The same logic applies to infrastructure: a “returning client” whose network location jumps in ways real clients’ don’t is telling you something.

Timing implausibility. Humans generate traffic in bursts with pauses; they sleep. A source that touches one new host of yours every 47 minutes, around the clock, for a week, is a machine on a schedule (and a patient one), deliberately pacing itself below anyone’s failure-counting window. The individual connections may even succeed. The regularity is the tell.

Traffic that answers questions nobody asked. A fun example from this package’s own test suite: backscatter. When someone out on the internet runs a spoofed-source scan or suffers a DoS attack, and the spoofed addresses happen to be yours, you receive SYN-ACKs and RSTs for connections you never initiated. A naive detector counts these as inbound scans. Simple Scan’s careful history-string filtering (S, Sr, genuine outbound-SYN failures only) exists precisely to exclude this noise. But flip it around: correctly identified backscatter is itself intelligence. It tells you your address space is being used as camouflage in someone else’s attack.

None of these signals live in a single connection record. They come from correlating connections against time, geography, and knowledge of what your network actually looks like, which is exactly the kind of stateful, cross-connection analysis Zeek’s scripting layer was built for.

Why This Is Harder Than It Sounds

If scan detection were just “count failures, alert at 25,” this series would be one post long. The catch is that scanning behavior spans an enormous range, and every detection parameter is a bet about where in that range your adversary sits.

Consider speed. On one end, tools like masscan and internet-wide scanning services can sweep your entire address space in seconds: hundreds of probes per second, trivially detectable, and largely not worth losing sleep over. That firehose is mostly automated, indiscriminate, and already bouncing off your firewall. On the other end is the scanner that should worry you: one probe an hour, sequenced across weeks, possibly targeting only the twelve hosts they learned about from a phished email. Same technique, same intent, separated by six orders of magnitude in rate.

Simple Scan’s window is honest about this tradeoff, right in the source:

## Failed connection attempts are tracked until not seen for this interval.
## A higher interval will detect slower scanners, but may also yield more
## false positives.

const scan_timeout = 15min &redef;

 

State about a source is kept until that source goes quiet for 15 minutes. A scanner that paces below roughly two probes per half hour simply never accumulates 25 tracked failures. It ages out of memory between probes. You can raise the timeout, but then you hold state for more sources for longer, and you start rolling unrelated, innocent failures from chatty-but-legitimate hosts into scan-shaped piles. Every threshold has this dual nature: lower it and you drown in false positives, raise it and the patient adversary walks under it.

And false positives are not hypothetical. This package’s own README calls out the classic one: heavy BitTorrent users, whose clients try to reach hundreds of long-dead peers and look exactly like outbound scanners. P2P software, aggressive monitoring systems, mobile clients roaming between networks, misconfigured load balancers all fail connections in bulk for boring reasons. Simple Scan gives you a Scan::scan_policy hook to carve out exceptions, but each exception is a judgment call only you can make, because it depends on what’s normal for your network.

Then there’s the adversary who read the same detection literature you did: distributed scanning. Split the sweep across 50 sources, each probing a handful of your hosts, and no single source ever approaches any per-source threshold. The scan is plainly visible in aggregate, 50 strangers asking one coordinated question, but invisible to any detector that keys its state on the source address alone. Catching it requires correlating across sources, which is another place profile-based thinking earns its keep: fifty different sources probing a port your network doesn’t serve is one anomaly, not fifty small ones.

When You Need More Than the Default

For a lot of networks, Simple Scan tuned to your environment is genuinely enough: declare your darknet ranges, set thresholds that match your size, write a scan policy for your known noisy hosts, and wire notices to whatever you use for blocking. It’s fast, understandable, and its author’s stated goal—the simplest thing that could possibly work—is a feature, not an apology.

But some environments outgrow it. Research networks and universities with huge, open address spaces face constant scanning at volumes where per-source failure counting alone is too blunt. High-security environments care precisely about the scans Simple Scan structurally can’t see: the slow ones, the distributed ones, the ones that mostly succeed because the attacker already has a target list. If your threat model includes a patient adversary, the default’s 15-minute memory and per-source state are the first things you’ll bump into.

This is where Zeek’s package ecosystem matters. Simple Scan isn’t the ceiling, it’s the floor. The same zkg package manager that installs it offers detectors that push further, most notably scan-NG (developed at Lawrence Berkeley National Laboratory, a network that has been a scanning target and a scan-detection research site for decades), which layers multiple complementary heuristics: landmine-style detection on unused addresses, knock-knock-style checks against knowledge of which hosts serve which ports, backscatter identification, and detection tuned for low-and-slow sources. Beyond dedicated scan detectors, Zeek’s Intelligence Framework lets you match traffic against feeds of known scanner infrastructure, and the NetControl framework closes the loop from detected to blocked. Because every one of these is Zeek script, you can read them, tune them, and—this is the real point—extend them with facts about your own network.

We’ll dig into those approaches, and what it takes to run them in production, in another blog post.

You Know Your Network Better Than They Do

Here is the asymmetry that makes scan detection worth doing: the attacker is scanning because they don’t know your network. You already do.

Every probe they send is an admission of ignorance, a doorknob tried on a house they’ve never seen. Simple Scan catches the clumsy version of this by counting the failures that ignorance produces, and for many networks that’s a solid, low-effort win. But the deeper opportunity is to encode what you know: which addresses are empty, which ports are real, who talks to whom, what your normal actually looks like. Every fact you teach your detector is a fact the attacker has to discover the hard way, and the act of discovering it is exactly what gives them away.

Good scan detection isn’t about outsmarting the attacker. It’s about refusing to forfeit the home-field advantage.

Coming up in this series: a closer look at profile-based detection, teaching Zeek what “normal” means on your network, and catching the scanner who only knocks once.

About Aashish Sharma

Aashish Sharma is a member of the Cyber Security Team at Lawrence Berkeley National Laboratory and serves on the Zeek Leadership Team. With nearly 23 years of experience in security and incident response, his work focuses on intrusion detection and incident response.

View all posts by Aashish Sharma