Lyrebird
Modern internet-services emulation suite for malware analysis labs.
Lyrebird stands up fake-but-believable network services — HTTP/HTTPS, DNS (UDP + TCP), SMTP, POP3/IMAP, FTP, TFTP, SSH, Telnet, IRC, NTP, TLS, and a generic TCP sink — so that a malware sample running in an isolated sandbox behaves as if it were online. Every interaction is recorded as a structured JSON event, and every payload the sample sends is captured to disk. It's a spiritual successor to INetSim (last release 1.3.2, 2020), rebuilt async-first in Python with detection telemetry as a first-class output.
⚠️ Authorized, isolated lab use only. Lyrebird emulates benign services to observe malware in containment. It is a defensive/research tool. It contains no implant, no command-and-control capability, and no evasion tooling. Run it on a segmented, non-routable network. Do not expose it to the internet.
Demo
A lab boots, a stand-in sample talks to the emulated services, and every interaction lands as structured JSONL with detections firing as tags:
The "sample" is a benign
curl/dig— Lyrebird only observes. The recording is generated fromdemo/lyrebird.tapewith VHS; seedemo/to reproduce it.
With the AI model layer
Optionally, Lyrebird can call a model to (1) improvise a believable, inert reply for an endpoint no static rule anticipated — so an unfamiliar sample keeps talking — and (2) triage the captured session into a verdict and candidate Sigma detections:
Off by default. The responder is constrained to generic placeholder content — never payloads, scripts, or tasking (see
SCOPE.mdandsrc/lyrebird/models/responder.py).
Why
INetSim is still the reference tool, but it's Perl-based, synchronous, config is a custom format, and its logging was designed for human reports rather than SIEM ingestion. Lyrebird keeps the proven model and modernizes it:
- Async core (asyncio / FastAPI / aiosmtpd / dnslib)
- YAML config with sane defaults — an empty file still gives a working lab
- Structured JSONL events — one normalized object per interaction, tailable straight into a SIEM (this is the backbone everything keys off)
- Artifact capture — uploads, mail bodies, and raw socket data stored + hashed
- Auto lab CA — TLS handshakes just work; certs are minted on first run
- Container-native —
docker compose upon an internal (no-egress) network - Paired Sigma detections — every service ships with detection content
Quick start
pip install -r requirements.txt
python -m lyrebird --config config/lyrebird.yaml
Or containerized (recommended — the compose network is internal: true, so the
lab has no outbound route by default):
cd docker && docker compose up --build
Point your malware analysis VM's default gateway / DNS at the Lyrebird host, then detonate the sample and watch the events stream in.
What gets emitted
Each interaction is one line of JSON:
{"schema":"1.0","ts":"2026-06-29T12:00:00.000+00:00","session":"...",
"service":"dns","transport":"udp","src_ip":"10.13.37.66","src_port":51000,
"dst_port":53,"event_type":"request","summary":"A evil.example",
"request":{"qname":"evil.example.","qtype":"A"},
"response":{"rcode":0,"answer":"10.13.37.1"},"artifacts":[],"tags":[]}
Events land in labdata/events/<session>.jsonl; captured payloads in
labdata/artifacts/<service>/.
The full field-by-field event schema and the complete detection catalog are in
REFERENCE.md — generated from events.py and the Sigma rules,
so it never drifts. Regenerate with python scripts/gen_reference.py.
Services
| Service | Transport | Status | Notes |
|---|---|---|---|
| HTTP / HTTPS | TCP | ✅ implemented | catch-all any method/path; auto-TLS; body capture |
| DNS | UDP | ✅ implemented | sinkhole responder; logs every lookup; optional realistic NXDOMAIN mode (off by default) |
| SMTP | TCP | ✅ implemented | accepts + captures mail; logs envelope |
| POP3 | TCP | ✅ implemented | fake mailbox; logs credentials/commands |
| FTP | TCP | ✅ implemented | passive + active (PORT) mode; captures STOR uploads |
| TFTP | UDP | ✅ implemented | captures WRQ uploads; per-transfer TID |
| IRC | TCP | ✅ implemented | observes bot C2 — nick, channels, PRIVMSG tasking |
| NTP | UDP | ✅ implemented | answers time; configurable faketime delta |
| TCP sink | TCP | ✅ implemented | logs all data to extra ports (INetSim "Dummy") |
| IMAP | TCP | ✅ implemented | fake mailbox; logs LOGIN credentials; IDLE push (mailbox-C2 long-poll) → imap-idle |
| DNS over TCP | TCP | ✅ implemented | sinkhole over TCP transport |
| TLS (fingerprint + serve) | TCP | ✅ implemented | JA3/JA4 + SNI, terminates & serves, same-connection SNI-vs-Host (off by default) |
| TLS fingerprint tap | TCP | ✅ implemented | JA3/JA4 + SNI capture then close (off by default) |
| SSH | TCP | ✅ implemented | asyncssh honeypot; captures brute-force credentials, then a fake shell logs commands (ssh-bruteforce, ssh-payload-pull) |
| Telnet | TCP | ✅ implemented | plaintext IoT/Mirai honeypot; brute-force creds → fake shell logs commands (telnet-bruteforce, telnet-payload-pull) |
| QUIC / HTTP-3 | UDP | ✅ implemented | aioquic h3 server; captures each HTTP/3 request and answers benignly (http3-transport) |
The SSH honeypot uses the
asyncsshlibrary (a required dependency). Ifasyncsshis unavailable the SSH service is skipped and the rest of the emulator keeps running.
Customizing responses
Tailor what any service returns without touching code, via a responses block
per service (the modern take on INetSim's fakefiles). Resolution order is
operator rule → fakefile → model responder (if enabled) → built-in default,
and the chosen source is recorded on every event (response.source).
services:
http:
responses:
http:
- path: "/gate.php" # glob over the URL path
method: "POST"
status: 200
content_type: "application/json"
body: '{"status":"ok","task":"none"}'
- path: "/*.exe"
body_file: "fakefiles/stub.bin" # relative to data_dir
fakefiles_dir: "fakefiles" # also serve real files by URL path
dns:
responses:
dns:
- qname: "*.evil-c2.com" # point a family at a specific sink host
qtype: "A"
answer: "10.13.37.66"
Fakefile serving is path-traversal protected — paths are confined to the configured directory.
Enabling / disabling services
Per-service enabled: true|false in config, or override at launch:
python -m lyrebird --disable smtp,ntp
python -m lyrebird --enable http,dns --no-banner
Model layer (frontier + local)
Lyrebird talks to models through one vendor-agnostic interface, so you can use a
frontier API or a fully local model interchangeably. Selecting local keeps all
data on-host for air-gapped analysis.
| provider | backend |
|---|---|
anthropic |
Claude (ANTHROPIC_API_KEY) |
openai |
OpenAI (OPENAI_API_KEY) |
gemini |
Google Gemini (GEMINI_API_KEY) |
local |
any OpenAI-compatible local server — Ollama, LM Studio, llama.cpp, vLLM |
mock |
offline deterministic stub (tests / dry-runs) |
Primary use — session triage into detections. Point it at a captured session and get a structured verdict + candidate Sigma ideas:
python -m lyrebird.analyze --session labdata/events/<id>.jsonl --provider local
python -m lyrebird.analyze --session <file> --provider anthropic --model claude-sonnet-4-6
Optional use — response generation. When models.respond.enabled: true, an
HTTP request with no matching rule gets a model-generated benign placeholder
body so unfamiliar samples keep talking. It's off by default (static templates
are preferred), and it's deliberately constrained: captured input is sanitized
first, the model is restricted to generic inert content (never payloads, scripts,
or commands), and output is canary-checked and length-capped — any failure falls
back to the static default.
Untrusted input is treated as untrusted
Captured traffic is adversary-controlled, so anything that reaches a model is a
prompt-injection surface. models/sanitize.py defangs injection markers, frames
captured data as inert observations behind a canary, and validates model output
against a schema. Nothing captured is ever executed — services only serve or log
bytes.
Adding a service
Subclass BaseService, implement start() / stop(), emit events with
self.emit(...), and register the class in orchestrator.REGISTRY. That's the
whole contract — see services/dns.py for a compact example.
Detections
detections/sigma/ holds Sigma rules that key off the JSONL schema
(logsource.product: lyrebird). Shipped so far: DNS long-label/DGA, HTTP
missing-User-Agent beacon, SMTP bulk recipients. The principle is that every
emulated technique ships with its paired detection.
Detection analytics
Beyond single-event Sigma rules, three analytics run over a captured session for the statistical / behavioural cases:
python -m lyrebird.beacons --session <jsonl>— beaconing, jitter (via inter-arrival CV), and channel rotation. The defensive pair to Phase 2.python -m lyrebird.mimicry --session <jsonl> [--data-dir labdata]— traffic-mimicry and encryption tells: protocol-on-unexpected-port, domain- fronting heuristics, browser-UA-but-bot, and high-entropy (encrypted) bodies. The defensive pair to Phase 3.python -m lyrebird.dns_tunnel --session <jsonl>— DNS tunneling / data-exfil channels: high-entropy, near-all-unique subdomains streamed under one parent domain, distinct from the single-query long-label DGA rule.
Layout
src/lyrebird/
events.py # structured event model + JSONL sink (the backbone)
config.py # YAML loading + defaults
base.py # BaseService plugin contract
certs.py # lab CA / leaf certs
tls.py # ClientHello parsing + JA3 / JA4
profiles.py # operator response templates
orchestrator.py # loads config, runs enabled services
cli.py # `python -m lyrebird`
analyze.py # model-assisted session triage
beacons.py # beacon / jitter / channel-rotation analytic
mimicry.py # traffic-mimicry / encryption-tell analytic
dns_tunnel.py # DNS tunneling / exfil analytic
services/ # http, dns, dns_tcp, smtp, pop3, imap, ftp, tftp,
# irc, ntp, tls_capture, tcp_sink
models/ # anthropic, openai, gemini, local, mock + sanitize
config/lyrebird.yaml
detections/sigma/
scripts/lint_sigma.py
docker/
tests/
Status
All sixteen services and all three session analytics are implemented, tested
(113 tests, green on Python 3.10–3.12), and runnable; the suite boots end to end
and every service emits telemetry. Recent additions — SSH and Telnet
credential-capture honeypots, IMAP IDLE emulation, FTP active-mode (FTP-bounce)
hardening, TLS JA3/JA4 fingerprinting, NTP mode-6/7 MONLIST detection, and a DNS
tunneling/exfil analytic — each ship with their paired detection. The plugin
contract is stable; further work is additive. See SCOPE.md for positioning and
the roadmap.
Contributing
PRs welcome — see CONTRIBUTING.md for setup, the bar for changes, and a
step-by-step walkthrough for adding a new service. CI (GitHub Actions) runs the
test suite on Python 3.10–3.12, lints the Sigma rules, and smoke-tests the CLI on
every push and PR.
PYTHONPATH=src python -m pytest tests/ -q
PYTHONPATH=src python scripts/lint_sigma.py
License
GPL-3.0-or-later (see LICENSE). Chosen to keep Lyrebird a copyleft, fork-friendly community tool in the same spirit as INetSim (GPLv2). Every source file carries an SPDX header.
Metadata
Release files for lyrebird-emulator 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| lyrebird_emulator-0.2.0.tar.gz | 95.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| lyrebird_emulator-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 186.1 kB
Release files / lyrebird_emulator-0.2.0.tar.gz
| Download URL | lyrebird_emulator-0.2.0.tar.gz |
|---|---|
| Size | 95.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d070bb3fc9413e0620a0a62178fe0b47b44e61f74f201f311185b75a1d6ace65
|
|
BLAKE2b-256 checksum How to use checksums |
7755f0724875d85f4279a2cdf405eeabeff04838fb46a89d206075e88edfd470
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 1, 2026.
Transparency logRelease files / lyrebird_emulator-0.2.0-py3-none-any.whl
| Download URL | lyrebird_emulator-0.2.0-py3-none-any.whl |
|---|---|
| Size | 90.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3c0ace679023e7d54231be336fc5074128ada8cfb2b38255511ed43a9e876798
|
|
BLAKE2b-256 checksum How to use checksums |
75179d1a5da0f36cd7a629ebc9f63ccb531da68742c988b50f159a5b7f3cb200
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 1, 2026.
Transparency log