CYJANKALI is the offensive test playground for the
Cyjan IDS. It collects
Kali Linux scripts that put your IDS deployment under realistic load —
along with what the frontend should show in response. If a
detection fails to trigger, you know exactly where to tune.
The wordplay is the point: cyanide (potassium cyanide,
German Cyankali) is the poison the system protects against.
Kali Linux is the tool we use to simulate the poison.
Lab setup
Topology
At least two VMs in an isolated subnet:
Cyjan IDS – installed from the current ISO, with a mirror interface (e.g. ens19) on the SPAN/mirror port of the lab switch.
Kali attacker – Kali Linux, regular routing interface.
Target VM (optional) – so the sniffer sees real traffic: a web server, a domain controller, a DVWA container.
No physical switch with a SPAN port? In Proxmox/VMware put the vSwitch
into promiscuous mode and attach the IDS mirror interface to
the same vSwitch as the target VM's routing interface. The sniffer
will then capture all bridge traffic.
Provision Kali
Grab a fresh Kali image and import it into your VM platform of choice:
Recommendation: the pre-built virtual machines
(.ova / .vmx) save you a full installer run. Change the default login
kali / kali immediately.
Proxmox: VM mirror via hookscript (no physical SPAN port required)
On a Proxmox host you can mirror all traffic of a target VM to a
sniffer VM — without switch configuration and without putting the
vSwitch into promiscuous mode. The script below registers as a
Proxmox VM hook and applies a bidirectional tc mirred
rule that copies every packet to the tap device of the sniffer VM.
The whole lab runs as 3 VMs on a single Proxmox host:
Master IDS VM — Cyjan IDS ISO on your management network. No mirror interface needed.
Tap VM — Cyjan Tap ISO with two NICs: management + mirror NIC on a dedicated bridge (e.g. vmbr99). Paired to the master via cyjan-tap pair.
Kali attacker VM — generates the offensive traffic. Sits on the regular lab network and is auto-mirrored to the tap VM by the hookscript.
The tap forwards alerts to the master via mTLS-WSS. The master frontend
shows them just like with a hardware SPAN port — except the entire
topology lives in one host.
Prerequisite: dedicated mirror bridge
Before the hookscript can do anything, you need a Linux bridge in
Proxmox dedicated to mirror traffic. The sniffer NIC of the
tap VM hangs off this bridge in isolation and only sees the packets
that tc mirred copies onto the bridge — no lab traffic,
no ARP storms, no DHCP noise.
The bridge has no physical port and no
IP. That's intentional: packets stay purely virtual on the
host and only enter via the hookscript mirroring.
Via the GUI (Proxmox web UI):
Datacenter → <node> → System → Network → Create → Linux Bridge
Name: vmbr99 (or any free vmbrN — must match SNIFFER_BRIDGE in the hookscript)
IPv4/IPv6: leave empty
Bridge ports: empty (no eth/eno/enp attached)
Comment: e.g. „Cyjan mirror"
Create, then click Apply Configuration at the top (live, no reboot)
Or as a file edit (equivalent) — directly in
/etc/network/interfaces on the Proxmox host:
cat >> /etc/network/interfaces <<'EOF'
auto vmbr99
iface vmbr99 inet manual
bridge-ports none
bridge-stp off
bridge-fd 0
#Cyjan mirror bridge (no physical port, no IP)
EOF
ifreload -a # or: ifup vmbr99
Verify:
ip -br link show vmbr99 # → vmbr99 UP ...
bridge link show master vmbr99 # shows attached taps (empty initially)
brctl show vmbr99 # alternative view
Wire the tap VM to the mirror bridge
The tap VM needs two NICs: one for management
(pairing with the master, SSH, updates) and one on vmbr99
for pure listen-only. Both are selected in the first-boot wizard
(ids-setup inside the tap VM) as management vs. mirror
interface.
# Example: VM 108 = tap. net0 is management (vmbr0), net1 becomes mirror.
qm set 108 --net1 virtio,bridge=vmbr99
# Then reboot the tap VM (or replug net1) so the new tap (tap108i1)
# shows up on the bridge.
qm reboot 108
# Inside the tap VM: the new interface appears as ens19 / eth1.
# In the first-boot wizard (ids-setup) pick it as 'mirror interface',
# NOT as management.
Important: the mirror interface inside the tap VM gets no
IP (the wizard sets iface eth1 inet manual +
promisc mode). The sniffer container reads it directly via AF_PACKET
— routing isn't needed.
Adjust three variables at the top:
TARGET_VM (the source VM you mirror),
SNIFFER_VM (your tap VM ID) and
SNIFFER_BRIDGE (Proxmox bridge of the tap VM's mirror NIC).
Install on the Proxmox host
Via SSH as root on the Proxmox host:
# 1. Drop the script — Proxmox looks for hookscripts under
# /var/lib/vz/snippets/ on the local 'local' storage.
mkdir -p /var/lib/vz/snippets
cp proxmox-mirror.sh /var/lib/vz/snippets/mirror.sh
chmod +x /var/lib/vz/snippets/mirror.sh
# 2. Edit the CONFIG block — TARGET_VM/SNIFFER_VM/SNIFFER_BRIDGE
nano /var/lib/vz/snippets/mirror.sh
# 3. Bind the hookscript to the TARGET VM (VMID = the Kali / target VM)
qm set 109 --hookscript local:snippets/mirror.sh
# 4. Pre-check: tap VM must have a NIC on SNIFFER_BRIDGE
qm config 108 | grep ^net # → expects 'bridge=vmbr99' on one NIC
# 5. Trigger: reboot target VM. The post-start hook sets up the mirror
# automatically (and starts the tap VM if AUTO_START_SNIFFER=1).
qm reboot 109
# 6. Verify: check tc rules on the source tap
tc qdisc show dev tap109i0
tc filter show dev tap109i0 ingress
journalctl -t mirror-hook -n 50
How the mirroring actually works
Proxmox creates a tap<VMID>i<index> device for
every VM NIC and attaches it to the configured bridge. The script:
Adds an ingress qdisc to the source tap so packets from the VM toward the bridge get mirrored.
Adds a root prio qdisc to mirror packets from the bridge toward the VM as well.
Both qdiscs get a matchall filter with mirred egress mirror dev <sniffer-tap> — the kernel copies every packet onto the tap of the sniffer VM.
The sniffer NIC of the tap VM sees the traffic exactly like a real SPAN port. The sniffer container (cyjan-tap-sniffer) reads via AF_PACKET and feeds the pipeline.
Works with and without the Proxmox firewall enabled — the script
detects the extra fwbr/fwpr chain and finds
the sniffer VM's tap regardless.
Cleanup
A regular qm shutdown/stop of the target VM
removes the tc rules via the pre-stop hook
automatically. To unbind the hookscript:
qm set 109 --delete hookscript
rm /var/lib/vz/snippets/mirror.sh
Test catalogue
Each category provides a script you run on the Kali box, plus the
expected reaction in the Cyjan frontend. If the alert does
not fire → that's an anti-pattern for a rule or ML model improvement.
Concrete lab results from a real pentest run against a
Cyjan-IDS lab — with IDS response per test and every code fix that came
out of it: → Lab report (phases 1–6)
1. Reconnaissance · nmap scans
Various stealth variants against the target VM. Good to verify
sniffer drop_pct stays clean and the connection graph marks the
Kali IP as a new node.
#!/usr/bin/env bash
TARGET=${1:-192.168.56.10}
echo "[+] SYN scan (top 1000 ports)"
sudo nmap -sS -T4 "$TARGET"
echo "[+] FIN scan (stealth)"
sudo nmap -sF -T3 "$TARGET"
echo "[+] NULL scan"
sudo nmap -sN -T3 "$TARGET"
echo "[+] Xmas scan"
sudo nmap -sX -T3 "$TARGET"
echo "[+] Service and OS detection"
sudo nmap -sV -O -A "$TARGET"
Alert manager: the dedup window suppresses the nikto spam wave after 300 s.
3. Brute force · hydra against SSH
Classic SSH login hammering against a lab-owned account.
Tests both the signature engine (connection burst pattern) and the
ML anomaly detection.
The wordlists are deliberately variables — do not blindly point
rockyou.txt at a production host. Create a separate lab
user with a controlled, intentionally guessable password so you can
verify the hit without burning third-party accounts.
Classic Windows lab attack. We use
analysis mode (-A): responder observes
LLMNR/NBT broadcasts and logs them but does not write
anything back onto the network. That is enough for the IDS detection
— the sniffer sees broadcast traffic anyway — and avoids spoofing
foreign hostnames, which would be a different legal corridor.
#!/usr/bin/env bash
# Passive analysis only: responder logs LLMNR/NBT queries without
# answering them. Active spoofing (-wF) deliberately not listed.
IFACE=${1:-eth0}
sudo responder -I "$IFACE" -A
Connection graph: a cluster of broadcast-active hosts becomes visible; the Kali box is a passive observer.
5. DNS attack despite resolver allowlist
A Cyjan IDS operator maintains the DNS resolver allowlist
(Settings → System → DNS resolvers) so legitimate response flows do
not scream as pseudo port scans. Only medium and
low severity alerts are suppressed there. Real DNS
attacks (high) must still fire — this test verifies
exactly that.
Three steps: first a harmless lookup wave against the configured
resolver (control, should stay silent), then a DGA
simulation and a tunneling volume test (both should scream).
#!/usr/bin/env bash
# Cyjankali DNS test: shows that the DNS resolver allowlist in Cyjan IDS
# only suppresses low/medium FPs — high-severity DNS attacks (tunneling,
# DGA) still fire.
#
# Prerequisite: $RESOLVER is configured in Cyjan IDS under Settings →
# System → DNS resolvers.
RESOLVER=${1:-192.168.56.1}
echo "[1/3] Control: 200 normal lookups against the resolver"
echo " Expectation: NO alert (medium/low dropped via allowlist)"
for i in $(seq 1 200); do
dig +short +time=1 +tries=1 example.com @"$RESOLVER" > /dev/null
done
echo
echo "[2/3] DGA simulation: 50 random subdomains with variable IAT, single socket"
echo " Expectation: DNS_DGA_001 (high) fires despite allowlist"
RESOLVER="$RESOLVER" python3 - <<'PY'
import os, random, socket, struct, time
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('', 0)) # one source port → one flow
target = (os.environ['RESOLVER'], 53)
for _ in range(50):
qname = ''.join(random.choices('abcdefghijklmnop', k=24)) + '.test.invalid'
txid = random.randint(0, 0xffff)
header = struct.pack('!HHHHHH', txid, 0x0100, 1, 0, 0, 0)
parts = qname.split('.')
body = b''.join(bytes([len(p)]) + p.encode() for p in parts) + b'\x00'
body += struct.pack('!HH', 1, 1) # type A, class IN
sock.sendto(header + body, target)
time.sleep(random.uniform(0.05, 0.6)) # → entropy_iat > 2.5
PY
echo
echo "[3/3] Tunneling volume: 1500 queries with large QNAMES, single socket"
echo " Expectation: DNS_TUNNEL_001 (high) fires despite allowlist"
RESOLVER="$RESOLVER" python3 - <<'PY'
import os, random, socket, struct
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('', 0))
target = (os.environ['RESOLVER'], 53)
for _ in range(1500):
# 60-hex-char subdomain → ~95 byte query, 1500× > 50 KB byte_count
sub = ''.join(random.choices('0123456789abcdef', k=60))
qname = sub + '.exfil.test.invalid'
txid = random.randint(0, 0xffff)
header = struct.pack('!HHHHHH', txid, 0x0100, 1, 0, 0, 0)
parts = qname.split('.')
body = b''.join(bytes([len(p)]) + p.encode() for p in parts) + b'\x00'
body += struct.pack('!HH', 1, 1)
sock.sendto(header + body, target)
PY
echo
echo "In the Cyjan frontend (Settings → DNS resolvers with $RESOLVER configured):"
echo " Step 1 → no new alerts (allowlist takes effect)"
echo " Step 2 → DNS_DGA_001 (high) – fires even with allowlist"
echo " Step 3 → DNS_TUNNEL_001 (high) – fires even with allowlist"
Expected IDS reaction
Step 1 (control): no alerts – the allowlist
suppresses SCAN_002 / DNS_AMP_001 / DNS_NONSTANDARD_001 (all
medium/low) for the configured resolver.
Step 2 (DGA):DNS_DGA_001 severity
high – the allowlist does not apply here because
policy lets high/critical through.
Step 3 (tunneling):DNS_TUNNEL_001
severity high – byte_count > 50 KB in a single
flow, same path as step 2 (no suppress).
alert-manager stats log:dns_allowlisted=N counts the step-1 drops — that's
how you verify server-side that suppression actually works.
If steps 2 or 3 fail to fire, check whether the DNS allowlist is
accidentally blocking high severity too (bug) or whether the test
queries reach the IDS sniffer at all (dual-NIC: mirror port active?
single-NIC: resolver must sit in the same L2 segment as the mgmt
interface).
More categories (to follow)
Slow DoS (slowloris, hping3 --rand-source)
Metasploit modules against typical lab targets
BloodHound + impacket suite (lateral movement)
BurpSuite Active Scan against DVWA
Pull requests welcome – schema per test is script / expected
detection / severity.
Verification in the IDS frontend
While running the Kali scripts, watch the following spots in the
Cyjan frontend (http://<mgmt-ip>/):
UI area
What you should see
Alert feed (live)
WebSocket push of new alerts in real time, severity column correlates with the test category.
Connection graph
Kali IP appears as a new hub node; nmap scans give it a high out-degree.
Threat level (header)
The 15-minute score indicator moves from green → yellow → orange → red, depending on volume and severity.
PCAP download
Per-alert ±60 s PCAP downloadable (once the MinIO upload completes).
ML status
Anomaly score histogram grows a right tail during the test sessions.
The scripts collected here are intended for authorised
security testing in your own lab. Using them against systems
you do not own without explicit written permission from the owner is
a criminal offence under § 202c StGB (preparation of
spying out and intercepting data), § 303a StGB (data
alteration) and § 303b StGB (computer sabotage) in
Germany. In the United States 18 U.S.C. § 1030 (CFAA)
applies. Comparable laws exist in most other jurisdictions.
No warranty. The scripts are provided "as-is" for
educational and detection-validation purposes — without any guarantee
of function, safety, or lawful use. Anyone aiming the CYJANKALI
material at production systems does so at their own risk; the
maintainers expressly accept no liability for the resulting
consequences.
Trademark notice: Kali Linux is a registered
trademark of OffSec Services Limited. This project is not affiliated
with Kali Linux, OffSec, or the Kali Linux team. The name
"CYJANKALI" references the chemical poison Cyankali
(potassium cyanide) — the wordplay with "Kali" is a pun, not an
endorsement claim.