LogoThreatmatic

Agent Event Reporting Spec

What the ThreatMatic Agent must capture from Windows WFP audit events and how to send it to the server.

Agent Event Reporting Spec

Version: 1.0
Date: June 11, 2026
Author: ThreatMatic Engineering


Background

The ThreatMatic Agent intercepts network traffic using the Windows Filtering Platform (WFP). Every connection decision — permit, block, or callout — generates a WFP audit event with a rich set of fields. Currently the agent sends only a subset of those fields. This spec defines the complete payload the agent must send so that the server can provide meaningful analytics, alerts, and forensic tooling.


Current Gaps

The server's device_access_events table currently receives:

FieldIssue
remote_addressIP and port packed into one string; port missing on egress; IPv6 truncated
applicationFull kernel path only (e.g. \device\harddiskvolume3\...)
direction✓ correct
action✓ correct
hostname✓ correct
metadataOnly {"count": "1"} — the rest of WFP's data is discarded

Missing entirely: local address, local port, remote port, protocol, process ID.


Required Fields

Every event sent to the server must include the following fields. Fields marked required must always be present; fields marked optional should be sent when WFP provides them.

Core 5-tuple

FieldTypeRequiredDescription
local_addressstring (IP)requiredLocal IP address of the connection
local_portintegerrequiredLocal port number (0 for ICMP)
remote_addressstring (IP)requiredRemote IP address — no port packed in, clean IP only
remote_portintegerrequiredRemote port number (0 for ICMP)
protocolstringrequiredTransport protocol: tcp, udp, icmp, icmpv6, or other

Process context

FieldTypeRequiredDescription
applicationstringrequiredFull kernel path as provided by WFP (existing behaviour)
application_namestringoptionalBasename only, e.g. chrome.exe — derived by the agent from application
process_idintegeroptionalPID at the time of the event

Decision context

FieldTypeRequiredDescription
directionenumrequiredingress or egress
actionenumrequiredpermit, block, callout_inspection, callout_terminating, or callout_unknown
policy_iduuidoptionalID of the policy that matched, if tm_enabled is true
tm_enabledbooleanrequiredWhether ThreatMatic policy was active on this device at the time
filter_idintegeroptionalWFP filter run-time ID that made the decision — useful for debugging policy mismatches
layer_namestringoptionalWFP layer where the event was generated (e.g. FWPM_LAYER_ALE_AUTH_CONNECT_V4)

Timestamps and identity

FieldTypeRequiredDescription
timetimestamp (UTC)requiredEvent timestamp from WFP, not the time the event was sent
device_iduuidrequiredDevice UUID assigned by the ThreatMatic engine
organization_iduuidrequiredOrganization UUID
hostnamestringrequiredDevice hostname at the time of the event

Serialization

Send all fields above in the existing gRPC message. Pack the new fields into the metadata jsonb field using the keys below until the server schema is updated with dedicated columns. Do not pack port into remote_address — send IP and port as separate fields.

{
  "time": "2026-06-11T15:44:28Z",
  "device_id": "019c1b10-a819-7aca-b796-f0bd2b85b8df",
  "organization_id": "019e7988-247a-7c1d-9a8b-b6b3cc96fd05",
  "hostname": "DESKTOP-EJFTOMV",
  "remote_address": "160.79.104.10",
  "application": "\\device\\harddiskvolume3\\users\\mohan\\.vscode\\extensions\\anthropic.claude-code-2.1.165-win32-x64\\resources\\native-binary\\claude.exe",
  "direction": "egress",
  "action": "permit",
  "policy_id": "019d4171-cdf7-7088-8935-c1d35b5676ae",
  "tm_enabled": true,
  "metadata": {
    "local_address": "192.168.50.10",
    "local_port": 52341,
    "remote_port": 443,
    "protocol": "tcp",
    "application_name": "claude.exe",
    "process_id": 18432,
    "filter_id": 68174,
    "layer_name": "FWPM_LAYER_ALE_AUTH_CONNECT_V4"
  }
}

Edge Cases

ICMP — no ports. Send local_port: 0 and remote_port: 0. Set protocol to icmp or icmpv6. Optionally include icmp_type and icmp_code in metadata.

IPv6 — remote_address must be the clean IPv6 address with no brackets or port suffix. The current agent sends [: for some IPv6 events — this is a parsing bug and must be fixed.

Loopback / link-local — send as-is. Do not filter out 127.x, 169.254.x, or ::1 events — they are useful for detecting local lateral movement.

Missing process info — WFP does not always provide a process path (e.g. kernel-originated packets). Send application: "" and omit process_id rather than sending a placeholder string.

Batching — if the agent batches events before sending, metadata.count should reflect the actual batch size. All other fields should represent a single canonical event from the batch (e.g. the first occurrence).


What the Server Will Do With This Data

Once the agent sends the full payload, the server will:

  1. Promote fields to columns — local_address, local_port, remote_port, protocol, process_id will be added as proper columns on device_access_events (a TimescaleDB hypertable). The metadata bag is a bridge until then.
  2. Per-port analytics — "top destination ports", "devices listening on unusual ports", "protocol breakdown"
  3. app_catalog auto-population — distinct (organization_id, application_name) pairs seen in events will seed the app catalog automatically
  4. Richer policy debugging — filter_id + layer_name lets the server explain exactly which rule matched and at what WFP layer

Questions for the Agent Developer

  1. Which WFP event source is used — Security Event Log (event IDs 5156/5157) or the WFP kernel API directly? This affects what fields are available and their format.
  2. Is process_id always available, or only for user-space processes?
  3. What is the current batching strategy — are events sent individually or aggregated over a time window?
  4. Is the gRPC proto under version control in the agent repo? The metadata map should already support arbitrary string/int values without a proto change.

How is this guide?

Last updated on

On this page