> ## Content Index
> Fetch the complete content index at: https://www.pinglabz.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Suricata eve.json Alerts: Trace an Nmap Scan to the Rule
- URL: https://www.pinglabz.com/suricata-eve-json-controlled-scan/
- Published: 2026-10-13T06:25:32.000Z
- Updated: 2026-10-13T06:25:32.000Z
- Description: Replay a bounded scan with one explicit Suricata rule. Follow EVE alerts to packets, compare an empty-rule run, and explain a benign alert.
- Author: Jaime
- Tags: Network Security, Packet Analysis, Nmap, Linux Labs

A scan happened in the lab. Whether Suricata reports an alert depends on the rules and configuration it actually loaded.

I processed a known capture containing 25 SYN probes with one explicit lab rule. Suricata produced 17 alerts. Processing the same file with an empty rule file produced none, while its DNS, HTTP, and flow metadata remained.

That difference is the point of this walkthrough: connect an EVE record to a reproducible rule match and the packet that caused it. The existing [Nmap hub](https://www.pinglabz.com/nmap/) covers the scanner's side, and [Cisco-side scan detection](https://www.pinglabz.com/detecting-nmap-scans-cisco-blue-team/) covers ACL and device evidence. Here the input is a saved PCAP and the sensor runs offline.

## Reuse the controlled capture

Generate `evidence/baseline.pcap` and `evidence/mixed.pcap` using [the Zeek lab's capture setup](https://www.pinglabz.com/pcap-to-zeek-connection-dns-logs/). The client is `192.0.2.10`; the server is `192.0.2.20`, with synthetic DNS on UDP 53 and HTTP on TCP 8080.

The baseline contains two DNS exchanges and one HTTP request. The mixed capture adds a SYN scan of ports 8000–8023 and 8080, followed by a successful `/recovery` HTTP request. Both endpoints belong to an isolated namespace pair with no uplink.

Tested with: Suricata `8.0.3` in the maintainer's Docker image, pinned by digest. This test loads one local signature and no vendor or community ruleset. The `docker` commands, including the one inside the helper below, assume your user can run Docker. If it can't, prefix them with `sudo`.

## Prepare the config without starting a live sensor

From the same lab directory, copy the image's stock configuration and its public supporting files:

```bash
SURI_IMAGE='jasonish/suricata@sha256:3f7b2f06e4667e4149deb34c52f278213a4a9b74bf926939aa5cbfc6c595dca0'
mkdir config

for file in suricata.yaml classification.config reference.config; do
  target="$file"
  if [ "$file" = suricata.yaml ]; then target=suricata-stock.yaml; fi
  docker run --rm --network none --entrypoint cat "$SURI_IMAGE" \
    "/etc/suricata/$file" > "config/$target"
done
```

Copying the supporting files makes them readable to the unprivileged container user. In my first validation attempts, the image's default log directory and supporting config paths were not writable/readable to that user. Explicit output paths and these copies resolved the setup errors; those failed attempts never analyzed the PCAP.

Save this as `scripts/prepare-suricata.py`, then run `python3 scripts/prepare-suricata.py`. It retains the stock engine configuration while selecting the EVE types used here and rewriting the supporting file paths.

```python
#!/usr/bin/env python3
from pathlib import Path
import re
p=Path('config/suricata-stock.yaml');s=p.read_text()
outputs='''outputs:
  - eve-log:
      enabled: yes
      filetype: regular
      filename: eve.json
      pcap-file: true
      community-id: true
      community-id-seed: 0
      types:
        - alert
        - flow
        - stats
        - dns
        - http:
            extended: yes

'''
s,n=re.subn(r'(?ms)^outputs:\n.*?(?=^[a-zA-Z][\w-]*:)',outputs,s,count=1)
assert n==1,'Expected one outputs block in this image config'
s=s.replace('classification-file: /etc/suricata/classification.config','classification-file: /work/config/classification.config')
s=s.replace('reference-config-file: /etc/suricata/reference.config','reference-config-file: /work/config/reference.config')
Path('config/suricata.yaml').write_text(s)
```

This enables alert, flow, DNS, HTTP, and stats events. Community ID is enabled with seed 0 for the sibling comparison.

## Load a rule whose meaning is visible

Save this one-line signature as `config/lab.rules`:

```text
alert tcp 192.0.2.10 any -> 192.0.2.20 8000:8080 (msg:"PINGLABZ LAB SYN burst to test ports"; flags:S; flow:stateless; detection_filter:track by_src,count 10,seconds 60; sid:9900001; rev:1;)
```

The match is narrowly scoped to the two lab addresses and TCP destination ports 8000–8080\. `flags:S` selects SYN-only packets, and `detection_filter` tracks matching packets by source, with a count of 10 over 60 seconds. Once the threshold is exceeded, subsequent qualifying packets can alert.

It does not count distinct destination ports, identify Nmap's executable, or inspect intent. A source making repeated ordinary connection attempts can also satisfy it. The [Suricata thresholding reference](https://docs.suricata.io/en/suricata-8.0.3/rules/thresholding.html?ref=pinglabz.com) documents the rate condition.

Define a helper so validation and processing use the same image and mounted files:

```bash
suri() {
  docker run --rm --network none --user "$(id -u):$(id -g)" \
    -v "$PWD:/work" -w /work --entrypoint suricata "$SURI_IMAGE" "$@"
}

suri -T -l evidence -c config/suricata.yaml -S config/lab.rules
```

The final validation succeeded. `-S` loads this rule file exclusively; using another rule-loading mode or an existing system ruleset would change the experiment. The [Suricata command-line reference](https://docs.suricata.io/en/suricata-8.0.3/command-line-options.html?ref=pinglabz.com) describes that option and offline input.

## Compare the benign baseline and scan capture

Use separate output directories:

```bash
mkdir evidence/suricata-baseline evidence/suricata-mixed

for name in baseline mixed; do
  suri --runmode single -c config/suricata.yaml -k none \
    -S config/lab.rules -r "evidence/$name.pcap" \
    -l "evidence/suricata-$name"
done
```

The container has no network connection, and `-r` reads a file. Nothing here attaches Suricata to a live host interface or puts it inline with the lab traffic.

`-k none` ignores checksum validation for these host-side veth captures, matching the analysis choice in the Zeek article. That choice belongs in the run manifest.

Count event types:

```bash
python3 - <<'EOF'
import collections, json
from pathlib import Path
for name in ['baseline', 'mixed']:
    rows = [json.loads(line) for line in Path(f'evidence/suricata-{name}/eve.json').read_text().splitlines()]
    print(name, collections.Counter(row['event_type'] for row in rows))
EOF
```

Observed results:

- Baseline: three flow records, four DNS events, one HTTP event, one stats event, and no alerts.
- Mixed capture: 29 flow records, four DNS events, two HTTP events, one stats event, and 17 alerts.

The decoder packet totals were 16 and 79, matching the corresponding capture files. Four DNS events represent requests and responses for two exchanges; they do not indicate four separate lookups.

## Follow one Suricata eve.json alert to the packet

The first saved alert pointed to frame 36:

```json
{
  "timestamp": "2026-10-07T00:36:26.834674+0000",
  "pcap_cnt": 36,
  "src_ip": "192.0.2.10",
  "src_port": 42000,
  "dest_ip": "192.0.2.20",
  "dest_port": 8017,
  "community_id": "1:0e7qFR7v4d/qcP8Lv50c3ckFiGs=",
  "alert": {
    "action": "allowed",
    "gid": 1,
    "signature_id": 9900001,
    "rev": 1,
    "signature": "PINGLABZ LAB SYN burst to test ports",
    "category": "",
    "severity": 3
  }
}
```

Its SID is 9900001, revision 1—the rule just loaded. The source port is 42000, which the capture runner assigned to Nmap's probes. Check the frame directly:

```bash
tshark -r - -Y 'frame.number == 36' -T fields \
  -e frame.number -e ip.src -e tcp.srcport -e ip.dst \
  -e tcp.dstport -e tcp.flags < evidence/mixed.pcap
```

That frame was a client SYN to port 8017\. Follow the tuple's nearby packets to see the server response.

The EVE `action` was `allowed`. In this test the rule is an alert rule and processing is offline. It supplies detection evidence; it did not prevent the original packets from reaching the server.

## Remove the rule, keep the packets

Create an intentionally empty signature file and process the identical mixed capture into a third directory:

```bash
printf '# No signatures: metadata-only comparison\n' > config/empty.rules
mkdir evidence/suricata-no-rules

suri -T -l evidence -c config/suricata.yaml -S config/empty.rules
suri --runmode single -c config/suricata.yaml -k none \
  -S config/empty.rules -r evidence/mixed.pcap \
  -l evidence/suricata-no-rules
```

The empty-rule validation reports that no rules were loaded; that is intentional here. The resulting EVE file still had 29 flows, four DNS events, two HTTP events, and one stats event, but zero alerts.

The scan did not disappear from the PCAP. The selected detection condition disappeared from the analysis. “No alert” needs the ruleset, log types, input visibility, and engine result attached before it becomes a useful observation.

## An alert also matched the recovery request

The last alert was on the SYN from source port 41002 to TCP 8080, frame 68\. That was the client's ordinary `/recovery` connection. The same source had just exceeded the rule's SYN-rate threshold, so this new SYN also matched.

The connection subsequently returned HTTP 200\. The server and client logs agree that the request succeeded.

Continue with [the same-capture Community ID comparison](https://www.pinglabz.com/zeek-suricata-community-id-same-pcap/). It joins that alert to the HTTP result and Zeek connection, and explains why the tools report different record and byte counts. For the underlying capture checks, use [the packet analysis hub](https://www.pinglabz.com/packet-analysis/).