This guide shows you how to write OCSF mapping operators in TQL. You’ll learn to organize mappings by attribute groups, handle unmapped fields, and validate your output. The guide assumes you’ve already identified your target OCSF event class and profiles.
Write the OCSF mapping
Section titled “Write the OCSF mapping”This section uses Palo Alto NGFW traffic logs as a running example. The
namespace follows the vendor::product:: convention, resulting in
paloalto::ngfw::ocsf::map.
Operator template
Section titled “Operator template”Use this annotated template as a starting point for creating a user-defined operator as part of a TQL package.
We recommend organizing fields by OCSF attribute groups: classification, occurrence, context, primary.
// --- Preamble ---------------------------------
// Move source data into a dedicated field to prevent name clashes// and enable automatic unmapped field collection.this = { src: this }
// --- OCSF: classification attributes ----------
ocsf.category_uid = 4ocsf.class_uid = 4001ocsf.activity_id = 6ocsf.severity_id = 1ocsf.type_uid = ocsf.class_uid * 100 + ocsf.activity_id
// --- OCSF: occurrence attributes --------------
ocsf.time = move src.time_generatedocsf.start_time = move src.startocsf.duration = move src.elapsedocsf.end_time = ocsf.start_time + ocsf.duration if ocsf.duration != null
// --- OCSF: context attributes -----------------
// Metadata about the event source.ocsf.metadata = { log_name: "traffic", product: { cpe_name: "cpe:/a:paloaltonetworks:pan-os", name: "NGFW", vendor_name: "Palo Alto Networks", }, uid: move src.sessionid, version: "1.7.0",}ocsf.app_name = move src.app
// --- OCSF: primary attributes -----------------
// Primary attributes reflect the core semantic meaning of the event.
ocsf.src_endpoint = { ip: move src.src, port: move src.sport,}
ocsf.dst_endpoint = { ip: move src.dst, port: move src.dport,}
let $proto_nums = {tcp: 6, udp: 17, icmp: 1}ocsf.connection_info = { protocol_num: $proto_nums[src.proto]? else -1, protocol_name: src.proto,}drop src.proto
// --- OCSF: profile-specific attributes --------
// Add fields for declared profiles (host, network_proxy, etc.)// ocsf.device = {...}// ocsf.proxy = {...}
// --- Epilogue ---------------------------------
// Hoist OCSF fields to root, collect unmapped.this = {...ocsf, unmapped: src}drop_null_fields unmapped
// Derive sibling fields (activity_name, category_name, etc.)ocsf::derive
// Set TQL-internal schema name for easier dispatching.@name = "ocsf.network_activity"Key principles
Section titled “Key principles”- Isolate source data:
this = { src: this }prevents name clashes and makes unmapped field collection automatic. - Use
move: Transfer fields withmoveto simultaneously assign and remove from source, for exampleocsf.time = move src.time_generated. - Only use
dropfor multi-use fields: When a field appears in multiple mappings, drop it after the last use. Prefermoveand single assignments. - Collect unmapped:
this = {...ocsf, unmapped: src}gathers any fields you didn’t map.
Package structure
Section titled “Package structure”Organize OCSF mappings as a package with a dispatcher and per-event-type operators:
Directorypaloalto/
Directoryoperators/
Directoryngfw/
Directoryocsf/
- map.tql Main dispatcher
- base.tql Fallback to OCSF base event
Directoryevent/
- network.tql Traffic logs → Network Activity
- dns.tql DNS logs → DNS Activity
- threat.tql Threat logs → Security Finding
Directorytests/
Directoryinputs/
- traffic.json Input shared across multiple tests
Directoryngfw/
Directoryocsf/
- map.tql
- map.txt
- base.tql
- base.txt
Directoryevent/
- network.stdin Log sample(s)
- network.tql Mapping for network event type
- network.txt Mapped OCSF event(s)
- dns.stdin
- dns.tql
- dns.txt
- package.yaml
Dispatcher operator
Section titled “Dispatcher operator”Your package should include one dispatching operator. The dispatcher routes events to an event-specific mapping operator:
if src.type == "TRAFFIC" { paloalto::ngfw::ocsf::event::network} else if src.type == "DNS" { paloalto::ngfw::ocsf::event::dns} else if src.type == "THREAT" { paloalto::ngfw::ocsf::event::threat} else { // Map to base event. paloalto::ngfw::ocsf::base}If the parser package does not set a type field, dispatch on a different field in the log that differentiates the event types.
Test structure
Section titled “Test structure”Create one test file per event type:
paloalto::ngfw::parsepaloalto::ngfw::cleanpaloalto::ngfw::ocsf::event::networkocsf::castThis requires that your test file has a sibling .stdin input that the test
framework picks up automatically.
Validation gate
Section titled “Validation gate”The ocsf::cast operator is the primary
schema validation gate. It ensures that your mapping produces schema-compliant
output.
Your mapping is complete once ocsf::cast no
longer emits warnings.