gomon monitors sandboxed malware or bot traffic and emits structured alerts whenever observed network behavior crosses configurable thresholds. It ingests either a live interface or a pre-recorded PCAP file, groups packets into fixed time windows, and writes Suricata-compatible Eve JSON for each window that shows suspicious activity.
The tool is designed for use inside automated malware-analysis pipelines. It integrates directly with bottle and bottle-warden, but runs standalone on any host with libpcap.
- gomon
gomon runs a sliding-window analysis loop over captured packets.
Packets (live interface or PCAP)
│
▼
FlowCollector — groups packets into bidirectional flows per window
│
│ WindowStats (packet counts per flow, start time, duration)
▼
BehaviorClassifier — applies thresholds, assigns local and global behaviors
│
│ LocalBehaviors (per-flow), GlobalBehavior (window-level)
▼
Eve JSON output — Suricata-compatible alert or stats record per event
Each window produces:
- One global behavior record reflecting the overall character of the window (scanning or idle).
- Zero or more local behavior records, one per distinct bidirectional flow that crossed a threshold.
When the window is empty (no packets at all), cross-window state (flow IDs, previously seen hosts) is reset so that a resumed session is not contaminated by a previous one.
Every bidirectional flow observed in a window is classified as one of:
| Class | Condition | Eve event_type |
|---|---|---|
attack |
Packet rate exceeds --packet-threshold and a C2 IP (--c2-ip) is set |
alert |
outbound_connection |
Packet rate below threshold, or no C2 IP is configured | alert |
The attack classification deliberately requires a known C2 IP because a high packet rate alone is ambiguous (e.g. bulk file transfer). If no --c2-ip is provided, every flow is logged as outbound_connection regardless of rate.
Packet rate is computed as total packets in both directions divided by the window duration in seconds.
Direction stats are included in every local behavior record: src_to_dst_packets, dst_to_src_packets, src_to_dst_rate, dst_to_src_rate, and an amplification factor (dst_to_src_packets / src_to_dst_packets). An amplification factor greater than 1 indicates the destination responded with more packets than it received, which is a useful signal for reflection attacks.
The "source" of a flow is determined by a priority chain:
- Whichever side matches
--bot-ip(i.e.<src_ip>). - Whichever side is RFC 1918 (private address space).
- Whichever side sent fewer packets in the window (heuristic for scanner/initiator vs. responder).
- Canonical tie-break (lower IP address / lower port).
The global behavior summarizes the entire window:
| Class | Condition | Eve event_type |
|---|---|---|
scanning |
Unique destination host count exceeds --destination-threshold |
alert |
idle |
No threshold exceeded (emitted only with --show-idle) |
stats |
The destination count and the mode used to compute it are both recorded in the output, making the classification fully reproducible from the log alone.
Three modes are available via --scan-detection-mode. All operate on unique destination IP addresses; port diversity (vertical scans) is not yet considered.
| Mode | Description |
|---|---|
filtered-host-rate (default) |
Unique destination IPs per window, excluding hosts already flagged as attack targets in the same window. Prevents a single victim from inflating the scan rate when the bot attacks the same host many times. |
host-rate |
Raw unique destination IPs per window, no filtering. Use when you want a conservative, easy-to-replicate metric. |
new-host-rate |
Unique destination IPs that were not seen in the previous window. Useful for detecting scanners that enumerate hosts steadily across windows; suppresses the initial burst from already-known hosts. |
gomon writes one JSON object per line (NDJSON), compatible with Suricata's Eve schema. Standard Eve fields are present (timestamp, event_type, src_ip, dest_ip, src_port, dest_port, proto, flow_id, alert), with gomon-specific detail under metadata.gomon.
{
"timestamp": "2024-11-01T12:00:15.000000Z",
"event_type": "alert",
"src_ip": "10.0.0.5",
"dest_ip": "1.2.3.4",
"src_port": 54321,
"dest_port": 80,
"proto": "tcp",
"flow_id": 12345678901234567,
"host": "my-sample-42",
"alert": {
"action": "allowed",
"gid": 5,
"signature_id": 2100001,
"rev": 1,
"signature": "gomon high packet-rate to single host",
"category": "attack",
"severity": 2
},
"metadata": {
"gomon": {
"scope": "local",
"context": {
"sample_id": "my-sample-42",
"bot_ip": "10.0.0.5",
"c2_ip": "203.0.113.4"
},
"packet_rate": 47.3,
"packet_threshold": 20.0,
"src_to_dst_packets": 120,
"dst_to_src_packets": 60,
"src_to_dst_rate": 4.0,
"dst_to_src_rate": 2.0,
"amplification_factor": 0.5
}
}
}{
"timestamp": "2024-11-01T12:00:15.000000Z",
"event_type": "alert",
"src_ip": "10.0.0.5",
"dest_ip": "0.0.0.0",
"flow_id": 98765432109876543,
"alert": {
"signature_id": 2100002,
"signature": "gomon horizontal scan host-rate exceeded",
"category": "scan",
"severity": 3
},
"metadata": {
"gomon": {
"scope": "global",
"context": { "sample_id": "my-sample-42", "bot_ip": "10.0.0.5" },
"packet_rate": 210.0,
"packet_threshold": 20.0,
"destination_rate": 35.0,
"destination_rate_threshold": 10.0
}
}
}The list of scanned destination IPs is available through the corresponding local behavior records emitted in the same window. Each local record's dest_ip is one flow target.
Emitted only with --show-idle. Uses event_type: stats (not alert) and carries the measured rates even though no threshold was crossed. Useful for correlating quiet periods against sandbox execution state.
| Behavior | signature_id |
|---|---|
attack |
2100001 |
scanning |
2100002 |
outbound_connection |
2100003 |
These IDs are stable across versions and can be used as filters in downstream processing (e.g. jq 'select(.alert.signature_id == 2100001)').
Each behavior is assigned a stable flow_id that persists across consecutive windows as long as the same flow remains active. A gap (empty window) resets continuity. This lets you reconstruct the timeline of a single flow across multiple windows by grouping on flow_id.
Requirements:
- Linux with libpcap headers (
libpcap-devon Debian/Ubuntu) for live capture. - Root or appropriate group membership to open a network interface. PCAP files work without elevated privileges.
- Go 1.24+ for building from source.
# Install from the module registry
go install github.com/cochaviz/gomon@latest
# Or build from source
git clone https://github.com/cochaviz/gomon.git
cd gomon
go build -o gomon ./cmd/gomonConfirm installation:
gomon --version
# gomon version v0.3.0gomon <input> <src_ip> [flags]
| Argument | Description |
|---|---|
<input> |
Network interface name (e.g. eth0, vnet0) or path to a .pcap / .pcapng file. |
<src_ip> |
IPv4 address of the monitored host (the bot). Used to orient flows and populate bot_ip in output. |
gomon sample.pcap 10.0.0.5 \
--window 15 \
--packet-threshold 20 \
--destination-threshold 25 \
--scan-detection-mode filtered-host-rate \
--c2-ip 203.0.113.4 \
--sample-id my-sample-42 \
--eve-log-path /tmp/my-sample-42.eve.jsonsudo gomon vnet0 10.10.0.20 \
--c2-ip 203.0.113.4 \
--sample-id beacon-42 \
--save-packets 100 \
--capture-dir /var/log/gomon/captures \
--show-idleUse --ignore-dst to drop known-benign endpoints (DNS resolver, gateway, sandbox controller) from flow counts so they do not inflate rates or appear in scan lists. The flag is repeatable:
gomon sample.pcap 10.0.0.5 \
--ignore-dst 10.0.0.1 \
--ignore-dst 8.8.8.8 \
--ignore-dst 192.168.1.254The C2 IP supplied via --c2-ip is automatically added to this exclusion list.
| Flag | Default | Description |
|---|---|---|
--window |
30 |
Analysis window size in seconds. |
--packet-threshold |
5 |
Packets per second per flow before the flow is classified as attack (requires --c2-ip). |
--destination-threshold |
10 |
Unique destination hosts per second before the window is classified as scanning. |
--scan-detection-mode |
filtered-host-rate |
One of host-rate, new-host-rate, filtered-host-rate. See Scan detection modes. |
--c2-ip |
(unset) | Known C2 server IP. Required for attack classification; automatically excluded from metrics. |
--sample-id |
(unset) | Free-form identifier written into every output record's context.sample_id. |
--ignore-dst |
(none) | Destination IPs to exclude from all metrics. Repeatable. |
--show-idle |
false |
Emit a stats record for every window that produces no alerts. |
--eve-log-path |
(stdout) | File path for Eve JSON output. Appends if the file already exists. |
--save-packets |
0 (off) |
Number of most recent packets per attack destination to write as a PCAP artifact when an attack alert fires. |
--capture-dir |
./captures |
Directory for packet capture artifacts. Created if it does not exist. |
--log-level |
info |
Verbosity of the operational log written to stderr: debug, info, warn, error. |
--version |
Print the binary version and exit. |
Memory note: Flow tracking is capped at 1024 unique bidirectional flows per window. Windows that hit the cap log a warning; flows beyond the cap are not counted. In practice this limit is not reached in single-sandbox scenarios.
gomon is designed as a sidecar to the bottle sandbox instrumentation framework. Add it to a bottle profile so every sandbox run inherits consistent thresholds:
cli:
- command: >
gomon {{ .VmInterface }} {{ .VmIp }}
{{- if .C2Ip }} --c2-ip {{ .C2Ip }}{{ end }}
--sample-id {{ .SampleName }}
--window 30
--packet-threshold 20
--destination-threshold 25
--save-packets 100
--capture-dir {{ .LogDir }}/captures
--eve-log-path {{ .LogDir }}/{{ .SampleName }}.eve.json
output: filebottle-warden can tail the Eve file to detect when beaconing stops or spikes, using standard Suricata tooling or plain jq queries.
At startup, gomon prints every active flag value to stderr before processing begins:
Configuration:
version: v0.3.0
input: sample.pcap
source-ip: 10.0.0.5
window: 30s
packet-threshold: 20.00
destination-threshold: 25.00
scan-detection-mode: filtered-host-rate
...
Capturing this output alongside Eve logs gives a complete record of the parameters used for a run.
The default thresholds (--packet-threshold 5, --destination-threshold 10) are intentionally conservative. For publication, report the thresholds used, the window size, and the scan detection mode explicitly, since all three affect what is and is not classified as suspicious.
PCAP file analysis is fully deterministic: the same file with the same flags always produces the same output. Live interface capture is not deterministic (packet ordering depends on the OS scheduler). For reproducible experiments, record traffic first with tcpdump or Wireshark and replay offline.
When --save-packets N is set, gomon writes a .pcap file to --capture-dir for each attack alert, named after the sample ID, destination IP, and timestamp. These artifacts let you inspect the exact packets that triggered an alert without re-running the sandbox.
# Run all tests
go test ./...
# Run with verbose window accounting
gomon sample.pcap 10.0.0.5 --log-level debug
# Build and install locally
go build -o gomon ./cmd/gomonModule path: github.com/cochaviz/gomon
Source layout:
| Path | Contents |
|---|---|
cmd/cli.go |
CLI entry point, flag definitions, startup banner |
internal/collector.go |
FlowCollector — packet accumulation and windowing |
internal/classifier.go |
BehaviorClassifier — threshold application, flow ID continuity |
internal/analysis.go |
AnalysisConfiguration — orchestration, Eve output, capture |
internal/behavior.go |
Data types: LocalBehavior, GlobalBehavior, BehaviorFlow |
internal/flows.go |
Flow key types, canonical orientation, normalizedFlowCounts |
internal/eve_logger.go |
Suricata Eve JSON serialization |
internal/utils.go |
Packet counting, RFC 1918 checks, source endpoint heuristics |