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:
| Field | Issue |
|---|---|
remote_address | IP and port packed into one string; port missing on egress; IPv6 truncated |
application | Full kernel path only (e.g. \device\harddiskvolume3\...) |
direction | ✓ correct |
action | ✓ correct |
hostname | ✓ correct |
metadata | Only {"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
| Field | Type | Required | Description |
|---|---|---|---|
local_address | string (IP) | required | Local IP address of the connection |
local_port | integer | required | Local port number (0 for ICMP) |
remote_address | string (IP) | required | Remote IP address — no port packed in, clean IP only |
remote_port | integer | required | Remote port number (0 for ICMP) |
protocol | string | required | Transport protocol: tcp, udp, icmp, icmpv6, or other |
Process context
| Field | Type | Required | Description |
|---|---|---|---|
application | string | required | Full kernel path as provided by WFP (existing behaviour) |
application_name | string | optional | Basename only, e.g. chrome.exe — derived by the agent from application |
process_id | integer | optional | PID at the time of the event |
Decision context
| Field | Type | Required | Description |
|---|---|---|---|
direction | enum | required | ingress or egress |
action | enum | required | permit, block, callout_inspection, callout_terminating, or callout_unknown |
policy_id | uuid | optional | ID of the policy that matched, if tm_enabled is true |
tm_enabled | boolean | required | Whether ThreatMatic policy was active on this device at the time |
filter_id | integer | optional | WFP filter run-time ID that made the decision — useful for debugging policy mismatches |
layer_name | string | optional | WFP layer where the event was generated (e.g. FWPM_LAYER_ALE_AUTH_CONNECT_V4) |
Timestamps and identity
| Field | Type | Required | Description |
|---|---|---|---|
time | timestamp (UTC) | required | Event timestamp from WFP, not the time the event was sent |
device_id | uuid | required | Device UUID assigned by the ThreatMatic engine |
organization_id | uuid | required | Organization UUID |
hostname | string | required | Device 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:
- Promote fields to columns —
local_address,local_port,remote_port,protocol,process_idwill be added as proper columns ondevice_access_events(a TimescaleDB hypertable). Themetadatabag is a bridge until then. - Per-port analytics — "top destination ports", "devices listening on unusual ports", "protocol breakdown"
app_catalogauto-population — distinct(organization_id, application_name)pairs seen in events will seed the app catalog automatically- Richer policy debugging —
filter_id+layer_namelets the server explain exactly which rule matched and at what WFP layer
Questions for the Agent Developer
- 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.
- Is
process_idalways available, or only for user-space processes? - What is the current batching strategy — are events sent individually or aggregated over a time window?
- Is the gRPC proto under version control in the agent repo? The
metadatamap should already support arbitrary string/int values without a proto change.
How is this guide?
Last updated on