Skip to content

Event schema

Every sensor sends the same shape of event. The model is Event in the suckerpot-common package (sensors/common). It is a Pydantic model with frozen=True and extra="forbid". Unknown fields are rejected.

FieldTypeConstraintsContent
tsdatetimeMust include a time zone. Stored in UTC.When the attacker did it
sensorenumssh, telnet, adb, web, docker, redis, mysql, mssql, rdp, smb, sip, synologyWhich sensor saw it
session_idstring1 to 64 characters, A-Z a-z 0-9 _ - onlyGroups all events of one connection
src_ipIP addressIPv4 or IPv6Attacker address
src_portinteger0 to 65535Attacker port
dst_portinteger0 to 65535Port the attacker connected to
typeenumSee belowWhat happened
dataobjectJSON-serializable. At most 16 KiB once serialized.Details, as JSON. Keys depend on type.
typeMeaning
connectA connection opened
login_attemptA login was tried, successful or not
commandA command was typed in a fake shell
http_requestAn HTTP request was received
download_attemptA download command was seen (for example wget). The URL is recorded, the file is not fetched.
disconnectThe connection closed
scanA probe seen by a sensor, for example a port scan

The data object is free-form, but sensors should use these keys. The tagging rules (M4) depend on them.

typeKeys
login_attemptusername, password, success (boolean), method (password, publickey or keyboard-interactive for SSH and Telnet; rdp-cookie, rdp-ntlm or rdp-nla-off for RDP)

The Synology sensor sends HTTP requests, so its http_request events carry the usual HTTP keys. On top of them it sets these keys.

KeyContent
api, api_method, api_versionThe DSM Web API call, for example SYNO.API.Auth, login, version 6.
flagsThe request shapes the sensor saw, such as dsm_api, sid_forge or ransom_indicator. See the Synology sensor.
cve["CVE-2026-13639", "CVE-2026-13684"], only with sid_forge or scgi_injection.

The RDP sensor uses three login methods. Each one carries different fields, and the password is only ever set in the last two cases.

methodWhen it happensPasswordExtra fields
rdp-cookieThe client sent a mstshash user name in the connection requestAlways nullclient_tool, requested_protocols, routing_token and others
rdp-ntlmThe client completed an NTLM exchange (NLA)Set only for a configured decoy account whose password matched. Otherwise null.domain, workstation, ntlm_version (v1 or v2), nt_response_len, nt_response_preview (8 bytes)
rdp-nla-offThe client skipped NLA and sent its password in the Client Info PDU. Only with RDP_NLA_MODE=optional.Cleartext, length-limiteddomain, alternate_shell, working_dir, client_tool

The full RDP field list is in RDP sensor. | command | input (string), output_preview (string, optional) | | download_attempt | url, tool (wget, curl, tftp and so on), command | | http_request | method, path, query, headers (object), user_agent, body_preview, status | | connect | client_version (optional) | | disconnect | duration_s, reason | | scan | payload_preview (hex string) |

LimitValueWhat happens when it is exceeded
Serialized data16 KiBThe event is rejected by validation
Raw event field in a stream entry64 KiBRejected before the JSON is parsed

Sensors must cut attacker-controlled text before they build an event. The helper truncate_data() in suckerpot-common does this. It caps each string and the total size of data, and it cuts keys to a fixed length. Use it on every field that comes from the attacker.

An SSH login attempt, with documentation addresses only:

{
"ts": "2026-10-24T18:02:11Z",
"sensor": "ssh",
"session_id": "k3J9x2_a7Qe",
"src_ip": "198.51.100.23",
"src_port": 51422,
"dst_port": 22,
"type": "login_attempt",
"data": {
"username": "root",
"password": "123456",
"success": false,
"method": "password"
}
}

Each sensor has its own Redis stream: events:ssh, events:telnet, events:adb, events:web, events:docker, events:redis, events:mysql, events:mssql, events:rdp, events:smb, events:sip and events:synology. Each event is one entry in the stream of the sensor that produced it. The sensor field of the event must match the stream suffix, or ingest drops the event. The entry has two fields:

FieldValue
vSchema version. Currently "1".
eventThe event as a JSON string

The publisher writes the entry with XADD, with an approximate length cap set by EVENTS_MAXLEN (default 10,000):

XADD events:ssh MAXLEN ~ 10000 * v 1 event '{"ts":"2026-10-24T18:02:11Z", ...}'

A reader checks v first. It rejects any version it does not know. Then it validates the JSON with the same model.

The publisher never raises an error into the sensor, because an attacker-facing handler must not crash or hang when the queue is unhealthy. If Redis refuses the write (for example when memory is full), the event is dropped and counted. The error is logged at most once every 30 seconds per error kind. There is no internal buffer.

ingest does not stop on a bad entry. An entry that fails validation is logged, with the content truncated, and then acknowledged. This keeps the stream moving. A bad entry is never written to the database.

If the database rejects a batch, ingest retries it with a backoff. A single message that still fails after 5 attempts is logged with its stream ID and acknowledged, so it is dead-lettered.

Ingest groups events by session_id into the sessions table. A session keeps its first and last times, the number of events, and counters such as login_attempts, login_success, commands, downloads and http_requests. Enrichment and tags are added to both events and sessions (M4).

A session also has client_tool, a best-effort guess at the software that connected (for example FreeRDP, nmap or mstsc). Only the RDP sensor sets it, from the client_tool value in its events. unknown and other sensors’ values are stored as null. The guess is never authoritative. See Client tool identification.