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
35 changes: 35 additions & 0 deletions tenzir/changelog/unreleased/ocsf-network-detection-operators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: OCSF network detection operators
type: feature
authors:
- mavam
- codex
prs:
- 172
created: 2026-08-07T20:53:58.221185Z
---

The `tenzir` package now turns OCSF Network Activity streams into Detection
Findings for scan fan-out, beacon cadence, long connections, and outbound volume
bursts.

Run any detector as a user-defined operator between your normalized stream and
the findings topic:

```tql
subscribe "ocsf"
tenzir::detect::network::beacon_cadence
publish "findings"
```

Each operator exposes its time window, late-event tolerance, and detection
thresholds as named arguments for environment-specific tuning.

The beacon and volume detectors measure originator traffic via
`traffic.bytes_out`, the source-to-destination byte count that the NetFlow,
Zeek, and Suricata mappings populate consistently.

Detectors live in the `tenzir::detect::network` namespace, leaving room for
future detector families over other OCSF classes. Every finding carries MITRE
ATT&CK tactic and technique mappings in `finding_info.attacks` and a versioned
analytic identity for traceability.
9 changes: 9 additions & 0 deletions tenzir/examples/detect-beacon-cadence.tql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
name: Detect beacon cadence
description: |
Detects periodic network activity and publishes OCSF Detection Findings.
---

subscribe "ocsf"
tenzir::detect::network::beacon_cadence
publish "findings"
116 changes: 116 additions & 0 deletions tenzir/operators/detect/network/beacon_cadence.tql
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
description: Detects periodic network activity with robust timing and size statistics.
args:
named:
- name: window_size
type: duration
default: 30min
description: Event-time window over which to measure cadence.
- name: tolerance
type: duration
default: 1min
description: Maximum expected lateness for out-of-order events.
- name: idle_timeout
type: duration
default: 5min
description: Inactivity after which the current window closes.
- name: min_samples
type: int
default: 8
description: Minimum number of flows for one tuple.
- name: min_period
type: duration
default: 10s
description: Minimum median inter-arrival interval.
- name: max_period
type: duration
default: 3min
description: Maximum median inter-arrival interval.
- name: max_interval_dispersion
type: float
default: 0.2
description: Maximum interval MAD as a fraction of the median.
- name: max_size
type: int
default: 4096
description: Maximum median flow size in bytes.
- name: max_size_dispersion
type: float
default: 0.35
description: Maximum size MAD as a fraction of the median.
- name: max_skew
type: float
default: 0.6
description: Maximum absolute Bowley skewness for timing and size.
---

where class_uid == 4001 and activity_id == 6
window size=$window_size, on=time, tolerance=$tolerance,
idle_timeout=$idle_timeout {
group {
src_ip: src_endpoint.ip,
dst_ip: dst_endpoint.ip,
dst_port: dst_endpoint.port,
} {
sort time
summarize times=collect(time),
sizes=collect(traffic.bytes_out),
samples=count()
intervals = times.deltas()
interval_median = intervals.median()
interval_mad = intervals.mad()
interval_skew = intervals.skewness(method="bowley")
size_median = sizes.median()
size_mad = sizes.mad()
size_skew = sizes.skewness(method="bowley")
src_ip = $group.src_ip
dst_ip = $group.dst_ip
dst_port = $group.dst_port
start = $window.start
end = $window.end
drop times, sizes, intervals
}
}
where samples >= $min_samples and
interval_median >= $min_period and interval_median <= $max_period and
interval_mad <= interval_median * $max_interval_dispersion and
abs(interval_skew) <= $max_skew and
size_median <= $max_size and
size_mad <= size_median * $max_size_dispersion and
abs(size_skew) <= $max_skew
this = {
time: now(),
metadata: {
product: {name: "Tenzir", vendor_name: "Tenzir"},
uid: uuid(),
version: "1.9.0",
},
category_uid: 2,
class_uid: 2004,
activity_id: 1,
type_uid: 200401,
severity_id: 3,
status_id: 1,
is_alert: true,
start_time: start,
end_time: end,
finding_info: {
uid: uuid(),
title: f"Periodic network activity from {src_ip} to {dst_ip}:{dst_port}",
desc: f"{samples} flows; interval median {interval_median}, MAD {interval_mad}",
analytic: {
name: "Beacon cadence",
uid: "tenzir::detect::network::beacon_cadence",
type_id: 3,
version: "1.0",
},
attacks: [{
tactic: {uid: "TA0011", name: "Command and Control"},
technique: {uid: "T1071", name: "Application Layer Protocol"},
}],
},
evidences: [{
src_endpoint: {ip: src_ip},
dst_endpoint: {ip: dst_ip, port: dst_port},
}],
}
75 changes: 75 additions & 0 deletions tenzir/operators/detect/network/long_connection.tql
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
description: Detects long-lived network connections split across flow observations.
args:
named:
- name: window_size
type: duration
default: 1h
description: Event-time window over which to sum observed duration.
- name: tolerance
type: duration
default: 1min
description: Maximum expected lateness for out-of-order events.
- name: idle_timeout
type: duration
default: 15min
description: Inactivity after which the current window closes.
- name: min_observed
type: duration
default: 45min
description: Minimum accumulated connection duration.
---

where class_uid == 4001 and activity_id == 6
window size=$window_size, on=time, tolerance=$tolerance,
idle_timeout=$idle_timeout {
group {
src_ip: src_endpoint.ip,
dst_ip: dst_endpoint.ip,
dst_port: dst_endpoint.port,
} {
summarize observed_ms=sum(traffic.timespan.duration), flows=count()
src_ip = $group.src_ip
dst_ip = $group.dst_ip
dst_port = $group.dst_port
start = $window.start
end = $window.end
}
}
where observed_ms >= count_milliseconds($min_observed)
this = {
time: now(),
metadata: {
product: {name: "Tenzir", vendor_name: "Tenzir"},
uid: uuid(),
version: "1.9.0",
},
category_uid: 2,
class_uid: 2004,
activity_id: 1,
type_uid: 200401,
severity_id: 3,
status_id: 1,
is_alert: true,
start_time: start,
end_time: end,
finding_info: {
uid: uuid(),
title: f"Long network connection from {src_ip} to {dst_ip}:{dst_port}",
desc: f"{flows} flow observations cover {observed_ms}ms",
analytic: {
name: "Long network connection",
uid: "tenzir::detect::network::long_connection",
type_id: 1,
version: "1.0",
},
attacks: [{
tactic: {uid: "TA0011", name: "Command and Control"},
technique: {uid: "T1071", name: "Application Layer Protocol"},
}],
},
evidences: [{
src_endpoint: {ip: src_ip},
dst_endpoint: {ip: dst_ip, port: dst_port},
}],
}
69 changes: 69 additions & 0 deletions tenzir/operators/detect/network/outbound_volume_burst.tql
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
description: Detects outbound byte volume from an internal asset above a fixed threshold.
args:
named:
- name: window_size
type: duration
default: 15min
description: Event-time window over which to sum outbound bytes.
- name: tolerance
type: duration
default: 1min
description: Maximum expected lateness for out-of-order events.
- name: idle_timeout
type: duration
default: 5min
description: Inactivity after which the current window closes.
- name: min_bytes_out
type: int
default: 500M
description: Minimum outbound bytes from one internal asset.
---

where class_uid == 4001 and activity_id == 6
where src_endpoint.network_scope_id == 1 and
dst_endpoint.network_scope_id == 2
window size=$window_size, on=time, tolerance=$tolerance,
idle_timeout=$idle_timeout {
group src_endpoint.ip {
summarize bytes_out=sum(traffic.bytes_out),
destinations=count_distinct(dst_endpoint.ip)
asset = $group
start = $window.start
end = $window.end
}
}
where bytes_out >= $min_bytes_out
this = {
time: now(),
metadata: {
product: {name: "Tenzir", vendor_name: "Tenzir"},
uid: uuid(),
version: "1.9.0",
},
category_uid: 2,
class_uid: 2004,
activity_id: 1,
type_uid: 200401,
severity_id: 3,
status_id: 1,
is_alert: true,
start_time: start,
end_time: end,
finding_info: {
uid: uuid(),
title: f"Outbound volume burst from {asset}",
desc: f"{asset} sent {bytes_out} bytes to {destinations} external destinations",
analytic: {
name: "Outbound volume threshold",
uid: "tenzir::detect::network::outbound_volume_burst",
type_id: 1,
version: "1.0",
},
attacks: [{
tactic: {uid: "TA0010", name: "Exfiltration"},
technique: {uid: "T1030", name: "Data Transfer Size Limits"},
}],
},
evidences: [{src_endpoint: {ip: asset, network_scope_id: 1}}],
}
Loading
Loading