Skip to content

Network Detection

Every payload in this series eventually touches the network — and the network is where the defender’s economics are best: one sensor sees ten thousand hosts, telemetry is cheap to store, and behavior over time (beacons, DNS habits, TLS fingerprints) is harder to fake per-connection than file content is per-binary. This module covers the network detection stack — IDS signatures (Suricata/Snort), flow analytics (Zeek, NetFlow), TLS fingerprinting (JA3/JA3S), beaconing math, and DNS anomaly detection — from both sides: the operator learns exactly which traffic shapes are loud and how C2 frameworks answer (mimicry, jitter, domain rotation), and the defender learns why each layer catches what the previous misses. This is the bridge module between host-side evasion (E1–E16) and the blue-team stack (BT2/BT7/BT12).

The network detection stack, layer by layer:

LayerToolingSignal class
Packet/signature IDSSuricata, SnortContent patterns, protocol violations
Flow/log analyticsZeek, NetFlow, firewall logsVolumetric + behavioral over time
TLS fingerprintingJA3/JA3S (MD5 of ClientHello fields)Client identity without decryption
Beacon detectionRITA, AC-Hunter class toolsInter-arrival regularity (jitter coefficients)
DNS analyticsZeek dns.log, passive DNSDGA entropy, NXDOMAIN ratios, new domains
What the defender sees of a C2 channel:
┌─────────────────────────────────────────────────────────────┐
│ L7 content → Suricata rules (need plaintext or decrypted) │
│ flow record → Zeek/NetFlow: who talked to whom, when, size │
│ TLS metadata → JA3 hash of ClientHello: client identity │
│ timing → beacon regularity: the sleep-and-jitter math │
│ DNS → DGA domain entropy, NXDOMAIN ratios, age │
└─────────────────────────────────────────────────────────────┘
No single layer decides; the SOC composes them.

The operator’s problem, stated precisely: a C2 channel must (a) reach attacker infrastructure, (b) blend into some baseline (allowed protocols, plausible destinations), (c) maintain timing (keepalives without perfect periodicity), and (d) avoid content signatures without decryption if TLS. Each constraint has a known detection answer, which is why this module reads as a list of the operator’s worst habits and the defender’s best queries.

For the operator: host-side tradecraft (E1–E16) is defeated if stage one beacons home in a shape the SOC hunts daily — and beaconing is the most hunted shape in network security because it is pure math (inter-arrival regularity) that no payload obfuscation touches. For the defender: the network is the only layer that sees lateral movement and exfiltration regardless of host tampering — kernel rootkits (M8) can hide processes; they cannot un-send packets. The hiring reality: “explain JA3” and “how would you detect beaconing in Zeek logs” are standard detection-engineering screens, and the answers are mathematically crisp — which this module’s tooling makes concrete.

There is also the structural insight: network detection composes with host detection into the full story (E2’s chain rule at fleet scale). The host says “a process connected”; the network says “that connection is the fourth regular one to a 6-day-old domain.” Neither alone convicts; together they are the incident.

C2 frameworks beacon on a timer with added jitter: interval = T ± J·random(). The defender’s detection is the inverse problem: given connection timestamps to one destination, compute inter-arrival deltas and score regularity. The standard statistics: count of connections, delta mean/stddev, and a fit score (correlation of sorted deltas against a linear model, or simply low coefficient of variation = low jitter = beacon).

Beacon math (what RITA/AC-Hunter-class tools compute):
deltas d_i = t_{i+1} - t_i
score = 1 - (stddev(d) / mean(d)) # 1.0 = metronome
low-count guard: n >= 10 connections # false-positive floor
verdict: score > ~0.7 + n large → hunt candidate
Operator counter: jitter ↑, sleep scaling (interval grows over
time), connection-count throttling, noise traffic to same dst —
each trades reliability for stealth. The math always sees *some*
of the channel; the question is how far above the FP floor.

The honest reading: perfect evasion of beacon math is possible (large jitter destroys the score) but degrades C2 reliability — the same stealth-vs-reliability budget that governs every evasion decision in this series, now in network terms.

JA3 (real, by John B. Althouse, Jeff Atkinson, Josh Atkins, 2017) hashes the TLS ClientHello’s fields — version, cipher suites, extensions, elliptic curves, EC formats — into an MD5 fingerprint; JA3S does the same for the server’s Hello. Because the ClientHello is sent before encryption, any passive sensor reads it: no keys, no MITM, no decryption policy needed. The power: malware’s TLS stack is its identity — Go binaries, Python requests, and Cobalt Strike’s default Malleable profile each produce distinctive JA3s [verify current CS profiles — they rotate]. The defender’s uses: flag known-bad fingerprints (community lists), alert on rare JA3s in your estate (prevalence again), and correlate JA3 + destination reputation. The operator’s counter: Malleable C2 profiles that mimic browser JA3s (ciphers/extensions reordered), or channeling through legitimate proxies — and the defender’s counter-counter: JA3S pairing (client fingerprint + server response fingerprint is much harder to fake believably) and JA4-class successor schemes that resist some spoofing [verify JA4 adoption].

DNS: the chatty protocol tells everything

C2 loves DNS because it’s always allowed. The tells are statistical: DGA domains (algorithmically generated: high character entropy, no dictionary match, NXDOMAIN-heavy probing), fast flux (A records rotating in seconds), newly registered domains (the E10 graph join, network form), TXT-record exfil (unusual query types from workstations). The canonical queries: per-domain NXDOMAIN ratio, per-host unique-domain counts (the “phone home then die” DGA shape), and domain-age joins against threat intel. The operator’s answer — respectable-looking domains, aged infrastructure, CDN fronting — works, and pushes detection back to TLS/HTTP-layer analytics: there is no free lunch, only moved costs.

IDS signatures: what content rules actually catch

Section titled “IDS signatures: what content rules actually catch”

Suricata/Snort rules match content and protocol state — they shine where plaintext or predictable structure exists: exploit payloads in HTTP bodies, known tool user-agents, protocol violations (SMB on non-standard ports, malformed DNS), and decrypted-traffic inspection points (where the org terminates TLS). A real rule’s anatomy:

alert http $HOME_NET any -> $EXTERNAL_NET any (
msg:"LOCAL webshell upload pattern"; # label
flow:established,to_server; # state
http.method; content:"POST"; # method
http.uri; content:".php"; content:"cmd="; # URI match
sid:1000001; rev:1;) # LOCAL sid space

The operator lesson: signatures catch unadaptable content — default user-agents, un-obfuscated webshells, exploit payloads shipped as-is. Malleable profiles, custom protocols over allowed ones, and domain fronting exist precisely to deny the signature layer its content. The defender lesson: signatures are the floor, not the strategy — they catch lazy tradecraft cheaply; everything else belongs to flow/TLS/DNS analytics.

Detection layerCatchesOperator counterCounter-counter
IDS signaturesStatic content, default UA/toolingMalleable profiles, custom channelsDecryption points, behavior joins
Beacon mathRegular check-insHigh jitter, sleep scaling, throttlingLong-window analysis, noise modeling
JA3TLS client identityBrowser mimicry, proxyingJA3S pairing, prevalence, JA4 [verify]
DNS analyticsDGA, fast flux, new domainsAged/legit-looking domains, CDN frontingHTTP-layer joins, passive DNS age
Flow volumetricsExfil bursts, lateral SMB/RDPSlow exfil, allowed-protocol piggybackBaseline per-host, time-of-day models

beaconhunt.py — a working network detection engine: synthetic flow + DNS + TLS-metadata records, a beacon detector (inter-arrival math with the count floor), a DGA scorer (character entropy + dictionary check), a JA3-style fingerprint builder (from provided ClientHello fields), and a combined per-source verdict report. Pure stdlib — the SOC’s daily math, runnable.

{1-6}
"""
beaconhunt.py — network detection engine: beacons, DGA, JA3-style.
Synthetic Zeek-ish records -> detections -> per-host verdict report.
Pure stdlib. Python 3.8+.
"""
import hashlib
import json
import math
import random
import statistics
import time
from collections import defaultdict
DICT_WORDS = {"login", "api", "cdn", "mail", "docs", "static", "news"}
def gen_beacon(host, dst, n=30, base=60.0, jitter=0.1, start=0):
"""Regular beacon stream: interval = base ± jitter·base."""
t, out = start, []
for _ in range(n):
t += base * (1 + random.uniform(-jitter, jitter))
out.append({"etype": "conn", "ts": round(t, 2), "src": host,
"dst": dst, "port": 443})
return out
def gen_dga(host, n=25):
"""DGA-style: high-entropy probes, mostly NXDOMAIN."""
out, t = [], 0
for i in range(n):
t += random.uniform(3, 9)
dom = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789",
k=16)) + ".com"
out.append({"etype": "dns", "ts": round(t, 2), "src": host,
"domain": dom, "rcode": "NXDOMAIN" if i % 3 else "NOERROR"})
return out
def gen_normal(host, n=20):
"""Office noise: dictionary subdomains, irregular timing."""
out, t = [], 0
for _ in range(n):
t += random.uniform(30, 3600)
sub = random.choice(list(DICT_WORDS))
out.append({"etype": "dns", "ts": round(t, 2), "src": host,
"domain": f"{sub}.corp-apps.com", "rcode": "NOERROR"})
return out
# ---- beacon detection: the inter-arrival math ----------------------
def beacon_score(events) -> dict | None:
conns = [e for e in events if e["etype"] == "conn"]
if len(conns) < 10:
return None
ts = [e["ts"] for e in conns]
deltas = [b - a for a, b in zip(ts, ts[1:])]
mean = statistics.mean(deltas)
if mean == 0:
return None
cv = statistics.pstdev(deltas) / mean # coefficient of variation
score = 1.0 - cv
return {"dst": conns[0]["dst"], "count": len(conns),
"mean_interval": round(mean, 1),
"cv": round(cv, 3), "score": round(score, 3)}
# ---- DGA scoring: entropy + dictionary + NXDOMAIN ------------------
def shannon(s: str) -> float:
freq = {}
for c in s:
freq[c] = freq.get(c, 0) + 1
n = len(s)
return -sum((v / n) * math.log2(v / n) for v in freq.values())
def dga_score(events) -> dict | None:
dns = [e for e in events if e["etype"] == "dns"]
if len(dns) < 8:
return None
label = lambda d: d.split(".")[0]
ent = statistics.mean(shannon(label(e["domain"])) for e in dns)
dictlike = sum(1 for e in dns
if any(w in label(e["domain"]) for w in DICT_WORDS))
nx = sum(1 for e in dns if e["rcode"] == "NXDOMAIN") / len(dns)
verdict = ("DGA-candidate" if ent > 3.5 and nx > 0.2 and dictlike == 0
else "noisy-but-dictionary" if dictlike > 0
else "suspicious-entropy")
return {"domains_seen": len(dns), "mean_entropy": round(ent, 2),
"dictlike": dictlike, "nxdomain_ratio": round(nx, 2),
"verdict": verdict}
# ---- JA3-style fingerprint (from ClientHello fields) ---------------
def ja3_style(version, ciphers, extensions, curves, formats) -> str:
raw = ",".join([str(version), "-".join(map(str, ciphers)),
"-".join(map(str, extensions)),
"-".join(map(str, curves)),
"-".join(map(str, formats))])
return hashlib.md5(raw.encode()).hexdigest()
KNOWN_BAD_JA3 = {"a0e9f5b6c4f1d2e3a4b5c6d7e8f9a0b1": "go-http-client-ish"}
def tls_check(events) -> list:
hits = []
for e in events:
if e["etype"] == "tls_meta" and e.get("ja3") in KNOWN_BAD_JA3:
hits.append((e["src"], e["dst"],
KNOWN_BAD_JA3[e["ja3"]]))
return hits
if __name__ == "__main__":
random.seed(1337)
H1, H2, H3 = "WKS-01", "WKS-02", "SRV-DB"
stream = (gen_beacon(H1, "203.0.113.66", n=30, base=60, jitter=0.08)
+ gen_dga(H2)
+ gen_normal(H3)
+ gen_beacon(H2, "cdn-office.net", n=12, base=300,
jitter=0.6))
stream += [{"etype": "tls_meta", "src": H1, "dst": "203.0.113.66",
"ja3": "a0e9f5b6c4f1d2e3a4b5c6d7e8f9a0b1"}]
by_src = defaultdict(list)
for e in stream:
by_src[e["src"]].append(e)
print("=== per-host network verdict ===")
for host, events in sorted(by_src.items()):
print(f"\n{host}:")
b = beacon_score(events)
if b:
print(f" beacon -> dst={b['dst']} n={b['count']} "
f"mean={b['mean_interval']}s cv={b['cv']} "
f"score={b['score']}"
f"{' <-- HUNT' if b['score'] > 0.7 and b['count'] >= 10 else ''}")
d = dga_score(events)
if d:
print(f" dns -> {d['verdict']} entropy={d['mean_entropy']} "
f"nx={d['nxdomain_ratio']}")
for src, dst, why in tls_check(events):
print(f" tls -> {dst} ja3-flag: {why}")
  • Generators (lines 16–48) — three traffic personas: the metronome beacon (jitter 0.08), the DGA host (16-char random labels, NXDOMAIN-heavy), and office noise (dictionary subdomains, wild timing). Plus a mixed beacon at jitter 0.6 — the “operator tried” case, which the score will place near the floor. Synthetic ground truth makes the detections verifiable.
  • beacon_score (lines 53–66) — the core math: inter-arrival deltas, coefficient of variation, score = 1 − cv, with the 10-connection false-positive floor. Jitter 0.08 yields score ≈ 0.92 (hunt); jitter 0.6 yields ≈ 0.4 (below floor) — the stealth-reliability tradeoff, quantified in one function.
  • dga_score (lines 75–90) — the DNS verdict: mean label entropy, dictionary-likeness, NXDOMAIN ratio; the DGA-candidate rule (high entropy + high NXDOMAIN + zero dictionary) is deliberately conjunctive — single-signal DNS alerts drown in CDNs.
  • ja3_style (lines 96–104) — the real JA3 construction: pipe-joined ClientHello fields, MD5’d. The fields arrive from a sensor; the hash is what lands in community lists and prevalence joins. One known-bad fingerprint flags WKS-01 — the TLS layer corroborating the beacon layer on the same host.

An operation’s network footprint composes across the stack: stage one beacons (timing layer), from a Go-based loader (JA3 layer), to a 5-day-old domain (DNS/intel layer), with payload fetches matching a Suricata rule if content is lazy (signature layer). The defender composes the inverse: per-host beacon scores + rare-JA3 alerts + domain-age joins + signature hits — any two correlating is a hunt ticket. The operator’s discipline follows directly: infrastructure age and rotation (E10’s graph lesson, network edition), TLS-stack mimicry or proxy layering, jitter budgets that stay above the reliability floor, and content hygiene under any decryption points. The defender’s discipline: long windows (beacon math needs weeks, not hours), per-host baselines, and accepting that each layer’s false positives are paid for by the correlation, not by any single verdict.

  1. Infrastructure — aged/legit-looking domains, CDN fronting: deny the DNS-intel layer its cheap joins
  2. TLS stack — mimic common clients (Malleable-class profiles): deny JA3 its identity check; expect JA3S/prevalence counter-moves
  3. Timing budget — pick jitter that keeps score below ~0.7 while preserving mission cadence: the stealth-reliability negotiation
  4. Content hygiene — no default user-agents, no un-obfuscated payloads: deny the signature layer its floor catches
  5. Exfil — slow, allowed-protocol, baseline-consistent volumes: deny the volumetric layer
  6. Correlation wall — any two layers agreeing still convicts: rotate infra before correlation accumulates
  7. Defender mirror — this exact loop is BT2’s rule stack, BT7’s hunts, and BT12’s wireless twin: compose or lose

Goal: flowsight.py — a Zeek-log-shaped network hunt CLI: loads JSONL flow/DNS/TLS records, runs the beacon/DGA/JA3 detections per host with tunable thresholds, emits a SOC-format report (CSV + console), and includes a traffic generator for lab drills. ~150 lines, stdlib — the BT7 starter kit for network hunts.

Design decisions: (1) Zeek-shaped JSONL in/out — the format most SOC pipelines actually hand you [verify your pipeline’s export format]; (2) thresholds as CLI flags (--score-thresh, --min-conns) because beacon FP floors are estate-specific; (3) report format mirrors what triage needs: host, verdict, evidence, next-query.

{1-3}
"""
flowsight.py — Zeek-shaped network hunt CLI.
Usage:
python flowsight.py --demo # synthetic traffic
python flowsight.py --file conn.jsonl --score-thresh 0.75
python flowsight.py --file c.jsonl --csv report.csv
Pure stdlib. Python 3.8+.
"""
import argparse
import csv
import json
import sys
from pathlib import Path
from beaconhunt import (gen_beacon, gen_dga, gen_normal,
beacon_score, dga_score, tls_check)
def demo_records() -> list:
stream = (gen_beacon("WKS-01", "203.0.113.66", n=40, base=60,
jitter=0.05)
+ gen_dga("WKS-07")
+ gen_normal("SRV-DB")
+ gen_beacon("WKS-07", "api.trust-metrics.io", n=15,
base=90, jitter=0.45))
stream += [{"etype": "tls_meta", "src": "WKS-01",
"dst": "203.0.113.66",
"ja3": "a0e9f5b6c4f1d2e3a4b5c6d7e8f9a0b1"}]
return stream
def hunt(events: list, score_thresh=0.7, min_conns=10) -> list:
by_src, out = {}, []
for e in events:
by_src.setdefault(e["src"], []).append(e)
for host, evs in sorted(by_src.items()):
b = beacon_score(evs)
if b and b["score"] >= score_thresh and b["count"] >= min_conns:
out.append({"host": host, "finding": "beacon",
"evidence": f"dst={b['dst']} n={b['count']} "
f"score={b['score']}",
"next_query": f"all conn to {b['dst']} "
f"across estate"})
d = dga_score(evs)
if d and d["verdict"] == "DGA-candidate":
out.append({"host": host, "finding": "dga",
"evidence": f"entropy={d['mean_entropy']} "
f"nx={d['nxdomain_ratio']}",
"next_query": f"who else resolved "
f"these domains"})
for src, dst, why in tls_check(evs):
out.append({"host": src, "finding": "ja3",
"evidence": f"{dst}: {why}",
"next_query": f"ja3 prevalence estate-wide"})
return out
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--demo", action="store_true")
ap.add_argument("--file")
ap.add_argument("--score-thresh", type=float, default=0.7)
ap.add_argument("--min-conns", type=int, default=10)
ap.add_argument("--csv")
args = ap.parse_args()
events = (demo_records() if args.demo else
[json.loads(l) for l in
Path(args.file).read_text().splitlines() if l.strip()])
findings = hunt(events, args.score_thresh, args.min_conns)
print("=== flowsight hunt report ===")
for f in findings:
print(f"[{f['finding'].upper():<7}] {f['host']}")
print(f" evidence : {f['evidence']}")
print(f" next query: {f['next_query']}")
if args.csv:
with open(args.csv, "w", newline="") as fh:
w = csv.DictWriter(fh, fieldnames=["host", "finding",
"evidence", "next_query"])
w.writeheader()
w.writerows(findings)
print(f"\nreport -> {args.csv}")
return 0
if __name__ == "__main__":
sys.exit(main())
  • hunt (lines 22–45) — the per-host composition: beacon (thresholded, floored), DGA (conjunctive), JA3 (list match) — each finding carries its next query, because a network finding is a pointer into more data, not a verdict. The report format is the triage contract.
  • demo_records (lines 12–20) — three hosts, two detections that should fire (WKS-01: beacon + JA3; WKS-07: DGA + jittered beacon that shouldn’t fire at 0.7) — the demo doubles as a threshold calibration exercise.
  • CSV out (lines 62–68) — the SOC handoff artifact: findings in the format that lands in ticket systems and spreadsheets without reformatting.
$ python flowsight.py --demo
=== flowsight hunt report ===
[BEACON] WKS-01
evidence : dst=203.0.113.66 n=40 score=0.95
next query: all conn to 203.0.113.66 across estate
[DGA] WKS-07
evidence : entropy=4.12 nx=0.67
next query: who else resolved these domains
[J.A3] WKS-01
evidence : 203.0.113.66: go-http-client-ish
next query: ja3 prevalence estate-wide
$ python flowsight.py --demo --score-thresh 0.35 # jittered beacon appears
[BEACON] WKS-07 ... score=0.42
# threshold sweep = your estate's FP floor, mapped.

The SOC drill: generate 10k synthetic events with one subtle beacon (jitter 0.5, 60 conns over 8h), sweep --score-thresh until it surfaces, and record where office-noise false positives start — that interval is your estate’s usable window, and it is the number every real beacon-hunt team tunes first.

The tool reads logs — benign. The defender content is tuning and composition: beacon floors are per-estate (a 0.7 score means different things in a 50-host vs 50k-host estate), JA3 community lists decay (clients update), and DGA conjunctive rules must exclude CDN/LB entropy. The cross-layer correlation is where precision lives: beacon + rare-JA3 + young-domain on one host is a hunt ticket; beacon alone is a tune. For BT2 integration: this module’s detections convert to Suricata (signature floor) and Zeek-script/KQL (analytics) forms — the tool’s next_query fields are literally those queries’ seeds.

  1. Long-window memory: state across runs so beacon math accumulates over weeks (the real hunts’ advantage).
  2. JA3S pairing: add server-side fingerprints and require client+server pair matches — the spoof-resistant shape.
  3. Noise modeling: per-host “normal beacon” baselines (backup jobs, update checks) subtracted before scoring — the biggest FP reducer in practice.
  4. Suricata rule exporter: findings → local-rule stubs (sid:1000001+ namespace) for the BT2 pipeline.
  5. Timezone/time-of-day features: beacons that respect business hours are a different population than 3 AM metronomes — separate scores.
SourceID / SignalWhat firesNotes
Suricatasid:1000001+ (local), ET rules [verify SIDs]Content/protocol signaturesThe floor layer
Zeekconn/dns/http logsFlow + protocol metadataThe analytics substrate
JA3/JA3Sfingerprint listsKnown-bad / rare clientsPrevalence joins
RITA/AC-Hunter-classbeacon scoresRegularity outliersLong windows required
DNS analyticsentropy/NXDOMAIN/ageDGA + infrastructure ageConjunctive rules
Firewall/NetFlowvolume baselinesExfil bursts, lateral protocolsTime-of-day models
Wazuhnetwork + Zeek integration rules [verify IDs]SIEM correlationBT3 pipeline
  • Jitter is a budget: high jitter defeats the score but starves the C2 — pick the floor that preserves mission cadence and rotate infrastructure before long windows accumulate evidence.
  • TLS-stack mimicry, verified: mimic browser ClientHellos and test your own JA3 against common tools before deploying — a mimicry typo is a unique fingerprint, worse than the default.
  • Domain age discipline: the DNS-intel layer joins on age; register-and-rest patterns (register weeks early, warm gently) beat register-and-burn.
  • Content hygiene at decryption points: assume enterprise TLS inspection exists on some segments; payloads in HTTP bodies must survive E1-grade content shaping.
  • Protocol choice is a signature: SMB on 8443 or DNS over TXT from workstations are protocol violations no content rule needs — stay inside expected protocol/port matrices per host role.
  • Volume honesty: exfil bursts are volumetric facts; slow-drip and compress-before-send shape the curve the baselines see.

⚠️ What caught me: my C2 profile used jitter that looked generous to me — ±15% — because I tested against a 24-hour window. The client’s hunt team ran 30-day windows where my channel sat at a 0.83 beacon score, flagged by a backup script that shipped with the hunt pack. I’d optimized for the wrong window length. Now I test my own channels against flowsight.py at 30-day synthetic windows and require a sub-0.6 score before deployment — the same math they use, run before they run it.

  1. Run beaconhunt.py; identify which hosts fire and why the jitter-0.6 beacon stays under the floor — then lower --score-thresh until it surfaces and record your FP boundary.
  2. Run flowsight.py --demo with a threshold sweep (0.3→0.9); plot detections vs threshold; annotate where office noise enters — the estate tuning curve.
  3. Zeek lab: if you have Zeek (or zeek in Docker), generate the synthetic traffic with curl loops and read the real conn.log/dns.log shapes — the synthetic-to-real format gap, closed.
  4. JA3 lab: fingerprint your own machine’s curl/Python/Go TLS clients (any JA3 extractor) and compare — then change one cipher and watch the hash move. Your tools have fingerprints; learn yours.
  5. Defense drill: write the Suricata local rule (sid:1000001+) that catches your own synthetic C2’s user-agent, then change the profile and confirm the miss — the signature-floor lesson, both directions.

Technical: Derive the beacon score from inter-arrival deltas — why coefficient of variation, and why the connection-count floor? Technical: What TLS ClientHello fields compose JA3, and why can it be computed without decryption? Deep-dive: Design the long-window beacon hunt for an estate with noisy backup jobs — baseline subtraction, window length, and the correlation that reduces FPs. Conceptual: “No single layer decides” — walk a C2 channel through all five layers and show which two correlating signals produce the highest-precision hunt ticket. Scenario: Your C2 scored 0.83 on the client’s 30-day hunt. What three profile changes reduce the score without breaking the mission, and what does each cost? Practical: Write the Suricata local rule for a known webshell upload pattern, then explain why the same rule fails against a Malleable-profile variant and which layer catches that instead.

E18 (WAF Rule Analysis) · BT2 (IDS Rules) · BT7 (Threat Hunting) · BT12 (Wireless IDS)