Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions netflow/changelog/unreleased/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Keep this directory tracked.
#
# Release creation moves changelog entries into releases/<version>/entries.
# Keeping unreleased/ present prevents Git from inferring a directory
# rename and moving entries from long-lived branches into a release.
26 changes: 26 additions & 0 deletions netflow/changelog/unreleased/netflow-ocsf-mappings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
title: NetFlow OCSF mappings
type: feature
authors:
- mavam
- codex
prs:
- 169
created: 2026-08-01T17:01:03.310754Z
---

The new `netflow` package maps NetFlow v5, NetFlow v9, and IPFIX records to
OCSF 1.9.0:

```tql
accept_udp "0.0.0.0:2055", binary=true
read_netflow
netflow::ocsf::map
ocsf::derive
ocsf::cast
```

Flow records become Network Activity `Traffic` events. The mapper preserves
options records as Base Events, retains source fields without an OCSF
destination under `unmapped`, and marks the connection initiator as unknown.
Flow observation bounds and duration remain within `traffic`.
12 changes: 12 additions & 0 deletions netflow/examples/collect-to-ocsf.tql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: Collect NetFlow and IPFIX and map to OCSF
description: |
Receives NetFlow and IPFIX datagrams over UDP and validates the mapped OCSF
events.
---

accept_udp "0.0.0.0:2055", binary=true
read_netflow
netflow::ocsf::map
ocsf::derive
ocsf::cast
18 changes: 18 additions & 0 deletions netflow/operators/ocsf/base.tql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
description: NetFlow or IPFIX record to OCSF Base Event.
args:
named:
- name: event
description: The mapper working record.
type: field
---

@name = "ocsf.base_event"

$event.ocsf.category_uid = 0
$event.ocsf.class_uid = 0
$event.ocsf.activity_id = 0
$event.ocsf.severity_id = 1
$event.ocsf.status_id = 0
$event.ocsf.type_uid = 0
$event.ocsf.time = $event.source.netflow.export_time?
139 changes: 139 additions & 0 deletions netflow/operators/ocsf/events/network_activity.tql
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
description: NetFlow or IPFIX flow record to OCSF Network Activity.
args:
named:
- name: event
description: The mapper working record.
type: field
---

// One full period of the NetFlow v9 32-bit millisecond uptime counter.
let $sys_uptime_period = 4294967296ms

// --- OCSF: classification attributes ----------

@name = "ocsf.network_activity"
$event.ocsf.category_uid = 4
$event.ocsf.class_uid = 4001
$event.ocsf.activity_id = 6
$event.ocsf.severity_id = 1
$event.ocsf.status_id = 0
$event.ocsf.type_uid = $event.ocsf.class_uid * 100 + $event.ocsf.activity_id
$event.ocsf.initiator_id = 0

// --- OCSF: occurrence attributes --------------

$event.ocsf.traffic.start_time = (
(move $event.source.flow_start?) else
(move $event.source.flow_start_nanoseconds?) else
(move $event.source.flow_start_microseconds?) else
(move $event.source.flow_start_milliseconds?) else
(move $event.source.flow_start_seconds?)
)
$event.ocsf.traffic.end_time = (
(move $event.source.flow_end?) else
(move $event.source.flow_end_nanoseconds?) else
(move $event.source.flow_end_microseconds?) else
(move $event.source.flow_end_milliseconds?) else
(move $event.source.flow_end_seconds?)
)

// Account for the 32-bit exporter uptime counter wrapping every 49.7 days.
$event.ocsf.traffic.start_time = $event.ocsf.traffic.start_time else (
$event.source.netflow.export_time? - (
$event.source.netflow.sys_uptime? -
(move $event.source.flow_start_sys_up_time?).milliseconds()
if $event.source.netflow.sys_uptime? >=
$event.source.flow_start_sys_up_time?.milliseconds()
else $sys_uptime_period -
(move $event.source.flow_start_sys_up_time?).milliseconds() +
$event.source.netflow.sys_uptime?
)
)
$event.ocsf.traffic.end_time = $event.ocsf.traffic.end_time else (
$event.source.netflow.export_time? - (
$event.source.netflow.sys_uptime? -
(move $event.source.flow_end_sys_up_time?).milliseconds()
if $event.source.netflow.sys_uptime? >=
$event.source.flow_end_sys_up_time?.milliseconds()
else $sys_uptime_period -
(move $event.source.flow_end_sys_up_time?).milliseconds() +
$event.source.netflow.sys_uptime?
)
)

$event.ocsf.time = $event.ocsf.traffic.start_time else
$event.ocsf.traffic.end_time else
$event.source.netflow.export_time?
$event.ocsf.traffic.timespan.duration = count_milliseconds(
$event.ocsf.traffic.end_time? - $event.ocsf.traffic.start_time?
).round()

// --- OCSF: primary attributes -----------------

$event.ocsf.src_endpoint = {
ip: (
(move $event.source.source_ipv6_address?)
if $event.source.ip_version? == 6
) else (move $event.source.source_ipv4_address?) else
(move $event.source.source_ipv6_address?),
port: move $event.source.source_transport_port?,
mac: move $event.source.source_mac_address?,
autonomous_system: {
number: move $event.source.bgp_source_as_number?,
},
proxy_endpoint: {
ip: (
(move $event.source.post_nat_source_ipv6_address?)
if $event.source.ip_version? == 6
) else (move $event.source.post_nat_source_ipv4_address?) else
(move $event.source.post_nat_source_ipv6_address?),
port: move $event.source.post_napt_source_transport_port?,
},
}

$event.ocsf.dst_endpoint = {
ip: (
(move $event.source.destination_ipv6_address?)
if $event.source.ip_version? == 6
) else (move $event.source.destination_ipv4_address?) else
(move $event.source.destination_ipv6_address?),
port: move $event.source.destination_transport_port?,
mac: move $event.source.destination_mac_address?,
autonomous_system: {
number: move $event.source.bgp_destination_as_number?,
},
proxy_endpoint: {
ip: (
(move $event.source.post_nat_destination_ipv6_address?)
if $event.source.ip_version? == 6
) else (move $event.source.post_nat_destination_ipv4_address?) else
(move $event.source.post_nat_destination_ipv6_address?),
port: move $event.source.post_napt_destination_transport_port?,
},
}

// IPFIX `flow_direction` is relative to the observation point, whereas OCSF
// direction is relative to the monitored network boundary. Preserve the source
// value in `unmapped` because the latter cannot be inferred here.
$event.ocsf.connection_info = {
direction_id: 0,
Comment thread
mavam marked this conversation as resolved.
protocol_num: move $event.source.protocol_identifier?,
tcp_flags: move $event.source.tcp_control_bits?,
uid: (move $event.source.flow_id?).string(),
}
$event.ocsf.connection_info.protocol_ver_id = (
(move $event.source.ip_version?).int()
if $event.source.ip_version? in [4, 6]
else 6 if $event.ocsf.src_endpoint.ip?.is_v6() or
$event.ocsf.dst_endpoint.ip?.is_v6()
else 4 if $event.ocsf.src_endpoint.ip? != null or
$event.ocsf.dst_endpoint.ip? != null
else 0
)
$event.ocsf.traffic.bytes = move $event.source.octet_delta_count?
$event.ocsf.traffic.packets = move $event.source.packet_delta_count?
$event.ocsf.cumulative_traffic = {
bytes: move $event.source.octet_total_count?,
packets: move $event.source.packet_total_count?,
}
46 changes: 46 additions & 0 deletions netflow/operators/ocsf/map.tql
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
description: |
Map NetFlow and IPFIX records to OCSF.

Flow records become OCSF Network Activity events. Options records and records
from unsupported protocol versions become OCSF Base Event records.
The operator preserves fields without an OCSF destination in `unmapped`.
args:
named:
- name: event
description: The NetFlow or IPFIX record to map.
type: field
default: this
---

// Keep all mapping work inside the selected event scope.
$event = {...$event, source: $event, ocsf: {}}

// IPFIX uses version 10 for both flow and options records. Only flow records
// map to Network Activity.
match $event.source.netflow.version? {
5 | 9 | 10 if $event.source.netflow.record_type? == "flow" => {
netflow::ocsf::events::network_activity event=$event
}
_ => {
netflow::ocsf::base event=$event
}
}

// Construct metadata after dispatch so shared fields move at their last use.
$event.ocsf.metadata = {
version: "1.9.0",
log_name: "netflow",
log_version: (move $event.source.netflow.version?).string(),
type: move $event.source.netflow.record_type?,
sequence: move $event.source.netflow.sequence_number?,
transmit_time: move $event.source.netflow.export_time?,
reporter: {
ip: move $event.source.netflow.exporter?.ip?,
},
product: {
name: "NetFlow/IPFIX",
},
}

$event = {...$event.ocsf, unmapped: $event.source}
16 changes: 16 additions & 0 deletions netflow/package.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
id: netflow
name: NetFlow
author: Tenzir
author_icon: https://raw.githubusercontent.com/tenzir/library/main/.tenzir.svg
package_icon: https://raw.githubusercontent.com/tenzir/library/main/.tenzir.svg

description: |
NetFlow and IPFIX summarize network traffic exported by routers, switches,
firewalls, and other network devices.

This package maps `netflow.v5`, `netflow.v9`, and `netflow.ipfix` flow records
produced by `read_netflow` to OCSF 1.9.0 Network Activity events. It preserves
options records as OCSF Base Event records.

categories:
- mappings
27 changes: 27 additions & 0 deletions netflow/tests/ocsf/event-scope.tql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from {
outer: "preserved",
payload: {
marker: "nested",
netflow: {
version: 10,
record_type: "flow",
export_time: 2026-08-01T12:00:00Z,
sequence_number: 42,
exporter: {
ip: 203.0.113.10,
port: 2055,
},
},
source_ipv6_address: 2001:db8::1,
destination_ipv6_address: 2001:db8::2,
source_transport_port: 5353,
destination_transport_port: 53,
protocol_identifier: 17,
},
}
netflow::ocsf::map event=payload
assert outer == "preserved"
assert payload.class_uid == 4001
this = payload
ocsf::derive
ocsf::cast
85 changes: 85 additions & 0 deletions netflow/tests/ocsf/event-scope.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{
activity_id: 6,
activity_name: "Traffic",
category_name: "Network Activity",
category_uid: 4,
class_name: "Network Activity",
class_uid: 4001,
connection_info: {
direction: "Unknown",
direction_id: 0,
protocol_num: 17,
protocol_ver: "Internet Protocol version 6 (IPv6)",
protocol_ver_id: 6,
tcp_flags: null,
uid: null,
},
cumulative_traffic: {
bytes: null,
packets: null,
},
dst_endpoint: {
autonomous_system: {
number: null,
},
ip: 2001:db8::2,
mac: null,
port: 53,
proxy_endpoint: {
ip: null,
port: null,
},
},
initiator: "Unknown",
initiator_id: 0,
metadata: {
log_name: "netflow",
log_version: "10",
product: {
name: "NetFlow/IPFIX",
},
reporter: {
ip: 203.0.113.10,
},
sequence: 42,
transmit_time: 2026-08-01T12:00:00Z,
type: "flow",
version: "1.9.0",
},
severity: "Informational",
severity_id: 1,
src_endpoint: {
autonomous_system: {
number: null,
},
ip: 2001:db8::1,
mac: null,
port: 5353,
proxy_endpoint: {
ip: null,
port: null,
},
},
status: "Unknown",
status_id: 0,
time: 2026-08-01T12:00:00Z,
traffic: {
bytes: null,
end_time: null,
packets: null,
start_time: null,
timespan: {
duration: null,
},
},
type_name: "Network Activity: Traffic",
type_uid: 400106,
unmapped: {
marker: "nested",
netflow: {
exporter: {
port: 2055,
},
},
},
}
Loading
Loading