Suricata

The Suricata API gives you the alerts the Suricata intrusion detection engine raises on a capture file, as a ranked triage summary or as the individual alerts behind it. It is the same data as the Threat Assessment view in CloudShark, in a form scripts, dashboards, and security tools can use.

Authentication

All endpoints require an API token with the PCAP analysis permission enabled, and the token’s user must be able to view the capture. Pass the token in the Authorization header:

Authorization: <token>

The examples below use these values:

  • ${CLOUDSHARK_URL}: CloudShark instance URL.
    • Example: https://cloudshark.local
  • ${CAPTURE_ID}: CloudShark capture ID.
    • Example: ea238bd44aaa
  • ${API_TOKEN}: API token with PCAP analysis enabled.
    • Example: 82f2230b777174f098623e4a5203e4ce

Scanning

You never have to start a scan yourself. If Suricata has not scanned a capture yet, or the ruleset has been updated since it last did, the first request for that capture runs the scan and answers when it is done. Later requests read the stored results.

If a scan of the capture is already running, for example because another user just opened it in Threat Assessment, the request waits for that scan to finish rather than returning partial results.

A scan can take a while on a large capture, so allow for a slow first response.


Why a summary

A busy capture can raise hundreds of alerts, and most rulesets include many rules that describe ordinary traffic: an operating system checking its network connection, a browser using DNS over HTTPS, a software update. Sorted by count or time, the one alert that matters ends up buried under the ones that fire constantly.

The summary groups alerts by signature, so each rule appears once with its alert count, and ranks the groups so the most serious come first:

  1. Priority. How serious the ruleset rates the kind of activity, taken from Suricata’s own severity for the rule: High, Medium, Low, or Other.
  2. Severity. The rule author’s own rating, where the rule declares one: Critical, Major, Minor, or Informational. A rule its author calls Informational is a benign observation, so it ranks one priority band lower than its priority alone would put it.
  3. Behavior. How the traffic behaved in this capture. Alerts that recur on a regular interval, like a beacon checking in, rank above ones that don’t, and alerts spread across at least half the capture rank above a single burst.
  4. Count, then the signature ID, so the order is the same every time.

Each step only breaks ties in the one before it. A serious alert that fired once always ranks above a minor one that fired a thousand times, because volume never outranks seriousness.

The summary also keeps two kinds of noise out of the ranked list:

  • Engine events such as SURICATA STREAM excessive retransmissions describe how Suricata saw the traffic, not a threat. They are listed separately as anomalies.
  • Long tails are capped. Each finding shows up to five flows and five HTTP, DNS, TLS, and file entries, and the host list shows up to 20 hosts. When anything is cut, truncation says what.

If you want every alert to filter or count your own way, the alert list has them all.

The absence of alerts is weak evidence. Suricata can only see what its rules describe, and encrypted traffic to an unknown server usually matches nothing. An empty summary means no rule matched, not that the capture is clean.


Endpoints

Get the summary

GET /api/v2/suricata/:id/alerts/summary

Returns the ranked triage summary for the capture.

Response: 200 OK (shortened to the first finding)

{
  "capture": {
    "alerts": 9,
    "window": {
      "start": "2026-10-01T19:27:27.845Z",
      "end": "2026-10-01T19:30:55.252Z",
      "duration_seconds": 207.407442
    }
  },
  "findings": [
    {
      "sid": 2018959,
      "signature": "ET INFO PE EXE or DLL Windows file download HTTP",
      "category": "Potential Corporate Privacy Violation",
      "priority": "High",
      "severity": "Informational",
      "rule": { "family": "INFO", "confidence": "High", "tags": null },
      "behavior": {},
      "count": 1,
      "n_flows": 1,
      "n_src": 1,
      "n_dst": 1,
      "first": "2026-10-01T19:30:37.031Z",
      "last": "2026-10-01T19:30:37.031Z",
      "flows": [
        {
          "community_id": "1:c8V1cQkZy/GD6rcc/qORahY2s6Y=",
          "src_ip": "10.10.1.128",
          "src_port": 63723,
          "dest_ip": "107.175.82.242",
          "dest_port": 9000,
          "proto": "tcp",
          "app_proto": "http",
          "alerts": 1,
          "wire_bytes": { "up": 1389, "down": 48148 }
        }
      ],
      "http": [
        {
          "n": 1,
          "method": "GET",
          "host": "107.175.82.242",
          "url": "/wilow/runner_ilove.exe",
          "ua": "curl/8.21.0",
          "status": 200
        }
      ],
      "dns": null,
      "tls": null,
      "files": [
        { "n": 1, "filename": "/wilow/runner_ilove.exe", "max_size": 44200 }
      ]
    }
  ],
  "anomalies": [
    {
      "sid": 2221036,
      "signature": "SURICATA HTTP Response excessive header repetition",
      "reason": "suricata-engine-event",
      "count": 1,
      "n_flows": 1,
      "sample_community_ids": ["1:c8V1cQkZy/GD6rcc/qORahY2s6Y="]
    }
  ],
  "hosts": [
    {
      "ip": "10.10.1.128",
      "scope": "internal",
      "high": 1,
      "medium": 3,
      "low": 3,
      "anomaly": 2,
      "n_peers": 4
    }
  ],
  "truncation": null
}

Summary fields

Field Description
capture Total alerts and the time span they cover.
findings One entry per signature, most serious first. See below.
anomalies Suricata engine events, with their counts and up to three sample Community IDs.
hosts Each IP involved in an alert, with its alert counts by priority, whether it is an internal (private) or external address, and how many peers it had alerts with. Most involved first.
truncation What was capped, if anything; null when the summary is complete.

Finding fields

Field Description
sid, signature, category The rule that fired. Use sid to list its alerts.
priority High, Medium, Low, or Other, from Suricata’s severity for the rule.
severity The rule author’s signature_severity, or empty when the rule doesn’t declare one.
rule What the rule’s metadata says: its ruleset family (such as MALWARE or INFO), the author’s confidence, and the malware_family, target, and tags where present.
behavior periodic_seconds when the alerts recur on a regular interval (at least ten alerts), and span_pct, how much of the capture lies between the first and last alert.
count, n_flows, n_src, n_dst Alerts, flows, source IPs, and destination IPs for this signature.
first, last Time of the first and last alert.
flows The flows the rule fired on, each with its Community ID and the bytes sent each way.
http, dns, tls, files What the alerts saw at the application layer: requests, lookups, TLS server names and certificates, and file names and sizes.

List alerts

GET /api/v2/suricata/:id/alerts

Returns individual alerts in time order, with the total that matched.

Parameter Type Description
sid integer Only alerts from this signature.
community_id string Only alerts on this flow.
limit integer Maximum alerts to return. Defaults to 100.
offset integer Alerts to skip before the returned ones.

sid and community_id can be combined. Each alert includes an id, which you can use to get the alert.

The id is not assigned by Suricata. It is the alert’s position among all the alerts in the capture’s current scan, counting from 0, so it stays the same whatever filter you list with. Because it is only a position, it identifies an alert within one scan: when the capture is scanned again, for example after a ruleset update, the same id can refer to a different alert. Don’t store it as a permanent reference.

Response: 200 OK

{
  "alerts": [
    {
      "id": 5,
      "timestamp": "2026-10-01T19:30:37.031Z",
      "signature_id": 2018959,
      "rev": 6,
      "signature": "ET INFO PE EXE or DLL Windows file download HTTP",
      "severity": 1,
      "src_ip": "107.175.82.242",
      "src_port": 9000,
      "dest_ip": "10.10.1.128",
      "dest_port": 63723,
      "app_proto": "http",
      "community_id": "1:c8V1cQkZy/GD6rcc/qORahY2s6Y=",
      "indicator": {
        "http.hostname": "107.175.82.242",
        "http.http_user_agent": "curl/8.21.0",
        "http.url": "/wilow/runner_ilove.exe"
      }
    }
  ],
  "total": 1
}

severity here is Suricata’s number for the rule, 1 being the most serious; the summary’s priority is the same value as a word. indicator holds the most useful application-layer fields for the alert.


Get an alert

GET /api/v2/suricata/:id/alerts/:alert_id

Returns one alert in full, including the text of the rule that raised it and everything Suricata recorded about the flow.

Response: 200 OK

{
  "id": 5,
  "timestamp": "2026-10-01T19:30:37.031Z",
  "src_ip": "107.175.82.242",
  "src_port": 9000,
  "dest_ip": "10.10.1.128",
  "dest_port": 63723,
  "proto": "TCP",
  "app_proto": "http",
  "community_id": "1:c8V1cQkZy/GD6rcc/qORahY2s6Y=",
  "flow_id": 1174607991823444,
  "alert": {
    "signature_id": 2018959,
    "rev": 6,
    "signature": "ET INFO PE EXE or DLL Windows file download HTTP",
    "category": "Potential Corporate Privacy Violation",
    "severity": 1,
    "rule": "alert http $EXTERNAL_NET any -> $HOME_NET any (msg:\"ET INFO PE EXE or DLL Windows file download HTTP\"; flow:established,to_client; flowbits:isnotset,ET.http.binary; flowbits:isnotset,ET.INFO.WindowsUpdate; flowbits:set,ET.http.binary; file.data; content:\"MZ\"; within:2; byte_jump:4,58,relative,little; content:\"PE|00 00|\"; distance:-64; within:4; classtype:policy-violation; sid:2018959; rev:6; metadata:created_at 2014_08_19, confidence High, signature_severity Informational, updated_at 2024_03_14;)"
  },
  "http": {
    "hostname": "107.175.82.242",
    "method": "GET",
    "url": "/wilow/runner_ilove.exe",
    "user_agent": "curl/8.21.0",
    "status": 200
  },
  "files": [
    { "filename": "/wilow/runner_ilove.exe", "size": 44200, "state": "UNKNOWN" }
  ]
}

Examples

The examples use curl and the jq JSON parser.

Triage a capture

The ranked findings, one line each:

curl --request GET \
  --header "Authorization: ${API_TOKEN}" \
  --silent \
  --url "${CLOUDSHARK_URL}/api/v2/suricata/${CAPTURE_ID}/alerts/summary" \
  | jq -r '.findings[] | [.priority, .severity, .count, .sid, .signature] | @tsv'

Output

High	Informational	1	2018959	ET INFO PE EXE or DLL Windows file download HTTP
Medium	Informational	1	2016141	ET INFO Executable Download from dotted-quad Host
Medium	Informational	1	2021076	ET HUNTING SUSPICIOUS Dotted Quad Host MZ Response
Medium	Informational	1	2034567	ET HUNTING curl User-Agent to Dotted Quad
Low	Informational	1	2014819	ET INFO Packed Executable Download
Low	Informational	1	2031071	ET INFO Microsoft Connection Test
Low	Informational	1	2047866	ET INFO Observed Google DNS over HTTPS Domain (dns .google in TLS SNI)

The top five findings describe the same event from different angles: a Windows executable fetched with curl from a bare IP address on port 9000. The Microsoft connection test and Google DNS over HTTPS rank last, where routine traffic belongs.

Hosts most involved

Which addresses appear in the most serious alerts, and whether each is on your network:

curl --request GET \
  --header "Authorization: ${API_TOKEN}" \
  --silent \
  --url "${CLOUDSHARK_URL}/api/v2/suricata/${CAPTURE_ID}/alerts/summary" \
  | jq -c '.hosts[] | {ip, scope, high, medium, low, n_peers}'

Output

{"ip":"10.10.1.128","scope":"internal","high":1,"medium":3,"low":3,"n_peers":4}
{"ip":"107.175.82.242","scope":"external","high":1,"medium":3,"low":1,"n_peers":1}
{"ip":"23.219.87.72","scope":"external","high":null,"medium":null,"low":1,"n_peers":1}
{"ip":"8.8.4.4","scope":"external","high":null,"medium":null,"low":1,"n_peers":1}
{"ip":"195.63.128.106","scope":"external","high":null,"medium":null,"low":null,"n_peers":1}

Counts that are zero are left out of the response, which jq shows as null.

Every alert on one flow

A finding’s flows give the Community ID of each flow it fired on. List every alert on that flow, from any rule, in time order:

curl --request GET \
  --header "Authorization: ${API_TOKEN}" \
  --silent \
  --get \
  --data-urlencode "community_id=1:c8V1cQkZy/GD6rcc/qORahY2s6Y=" \
  --url "${CLOUDSHARK_URL}/api/v2/suricata/${CAPTURE_ID}/alerts" \
  | jq -r '.alerts[] | [.id, .timestamp, .severity, .signature] | @tsv'

Output

2	2026-10-01T19:30:36.887Z	2	ET INFO Executable Download from dotted-quad Host
3	2026-10-01T19:30:36.887Z	2	ET HUNTING curl User-Agent to Dotted Quad
4	2026-10-01T19:30:36.887Z	3	SURICATA HTTP Response excessive header repetition
5	2026-10-01T19:30:37.031Z	1	ET INFO PE EXE or DLL Windows file download HTTP
6	2026-10-01T19:30:37.031Z	2	ET HUNTING SUSPICIOUS Dotted Quad Host MZ Response
7	2026-10-01T19:30:37.031Z	3	ET INFO Packed Executable Download

Use --data-urlencode for Community IDs: they are base64 and can contain + and /.

Read the rule behind an alert

Get one alert and print the rule that raised it, to see exactly what Suricata matched:

curl --request GET \
  --header "Authorization: ${API_TOKEN}" \
  --silent \
  --url "${CLOUDSHARK_URL}/api/v2/suricata/${CAPTURE_ID}/alerts/5" \
  | jq -r '.alert.rule'

Output

alert http $EXTERNAL_NET any -> $HOME_NET any (msg:"ET INFO PE EXE or DLL Windows file download HTTP"; flow:established,to_client; flowbits:isnotset,ET.http.binary; flowbits:isnotset,ET.INFO.WindowsUpdate; flowbits:set,ET.http.binary; file.data; content:"MZ"; within:2; byte_jump:4,58,relative,little; content:"PE|00 00|"; distance:-64; within:4; classtype:policy-violation; sid:2018959; rev:6; metadata:created_at 2014_08_19, confidence High, signature_severity Informational, updated_at 2024_03_14;)

The rule looks for the MZ and PE headers of a Windows executable in an HTTP response body.

Your own filtering

The alert list returns every alert, so you can filter on anything in it. For example, only the most serious alerts, Suricata severity 1:

curl --request GET \
  --header "Authorization: ${API_TOKEN}" \
  --silent \
  --url "${CLOUDSHARK_URL}/api/v2/suricata/${CAPTURE_ID}/alerts?limit=1000" \
  | jq -c '.alerts[] | select(.severity == 1) | {timestamp, signature, src_ip, dest_ip}'

Output

{"timestamp":"2026-10-01T19:30:37.031Z","signature":"ET INFO PE EXE or DLL Windows file download HTTP","src_ip":"107.175.82.242","dest_ip":"10.10.1.128"}

Compare total with the number of alerts returned to check you have them all, and use offset to fetch the rest.

From an alert to Zeek and the packets

The Community ID connects a Suricata alert to the same flow in Zeek’s conn.log, and from there to the packets themselves. Find the Zeek connection for the flow above:

curl --request POST \
  --header "Authorization: ${API_TOKEN}" \
  --silent \
  --data '{
    "fields": ["uid", "id.orig_h", "id.resp_h", "id.resp_p", "service", "duration", "resp_bytes"],
    "filters": [{ "field": "community_id", "op": "eq", "value": "1:c8V1cQkZy/GD6rcc/qORahY2s6Y=" }]
  }' \
  --url "${CLOUDSHARK_URL}/api/v2/zeek/${CAPTURE_ID}/logs/conn.log/query"

Output

# matched: 1  groups: 1  returned: 1  omitted: 0
uid	id.orig_h	id.resp_h	id.resp_p	service	duration	resp_bytes
CDFUSj3EEz8oxoKuFb	10.10.1.128	107.175.82.242	9000	http	2.141484	1647168

Then turn the uid into a display filter with Zeek’s connection filters endpoint, and read the packets with the analysis API.


Errors

Errors are returned as JSON with the message in exceptions:

{ "exceptions": ["API Token is not allowed to execute analysis method."], "status": 403 }
Status Cause
403 The token does not have the PCAP analysis permission.
400 The capture does not exist or the token’s user cannot view it, Suricata cannot scan the file (for example a compressed capture), the scan did not finish in time, or the alert ID does not exist. The message explains which.
404 The path is not one of the endpoints above.