Skip to content

This guide shows you how to enrich OCSF events with AI-generated summaries, classifications, and annotations by using ai::prompt.

The ai::prompt operator sends one request per input event to an OpenAI-compatible Responses API endpoint. Use it when a deterministic rule or lookup table is too rigid, and keep the prompt payload small and explicit. The examples use compact OCSF-style records as from {...} starting points so you can focus on the enrichment pattern.

OCSF events have an enrichments field for inline enrichment data associated with an event or finding. The examples write the model result to a temporary field, use fnadd to add an OCSF enrichment object to enrichments, and then remove the temporary field.

Use data to send only the fields that the model needs. This example enriches an OCSF detection_finding event with a one-sentence summary and adds it to the event’s enrichments field:

from {
time: 2024-08-22T09:12:34,
category_uid: 2,
class_uid: 2004,
activity_id: 1,
activity_name: "Create",
type_uid: 200401,
severity_id: 4,
message: "Suspicious PowerShell command downloaded a script from a newly registered domain.",
metadata: {
version: "1.8.0",
},
finding_info: {
uid: "finding-42",
title: "Suspicious PowerShell download",
desc: "PowerShell used a hidden window to download and execute a remote script.",
},
enrichments: [],
confidence_id: 3,
confidence: "High",
}
ai::prompt model="qwen3",
system="Summarize this OCSF detection finding in one sentence for a security analyst.",
data={
severity_id: severity_id,
confidence: confidence,
message: message,
finding: finding_info,
},
into=ai.summary
where ai.summary != null
enrichments = enrichments.add({
name: "finding_info.uid",
value: finding_info.uid,
type: "ai_summary",
provider: "Tenzir ai::prompt",
short_desc: ai.summary.text,
src_url: "http://127.0.0.1:11434/v1/responses",
created_time: now(),
data: {
text: ai.summary.text,
model: ai.summary.model,
usage: ai.summary.usage,
latency_ms: ai.summary.latency / 1ms,
},
})
drop ai

The default endpoint is http://127.0.0.1:11434/v1, which matches the standard Ollama OpenAI-compatible API port. The example works with a local model if Ollama is running and the qwen3 model is available.

For fields that you want to use later in a pipeline, ask for a compact answer. The operator stores the model response as text and doesn’t parse JSON-looking output into structured fields.

This example classifies an OCSF dns_activity event. It sends the DNS query, response, and endpoints instead of the entire event:

from {
time: 2024-08-22T09:13:01,
category_uid: 4,
class_uid: 4003,
activity_id: 2,
activity_name: "Response",
type_uid: 400302,
severity_id: 1,
message: "DNS response for suspicious-update.example resolved to 198.51.100.42.",
metadata: {
version: "1.8.0",
},
query: {
hostname: "suspicious-update.example",
},
rcode_id: 0,
rcode: "NoError",
src_endpoint: {
ip: 10.10.2.17,
port: 54213,
hostname: "workstation-17",
},
dst_endpoint: {
ip: 192.0.2.53,
port: 53,
hostname: "resolver-1",
},
answers: [{
rdata: "198.51.100.42",
type: "A",
ttl: 60,
}],
enrichments: [],
}
ai::prompt model="qwen3",
system="Classify this OCSF DNS event as benign, suspicious, or malicious. Reply with one label and a short reason.",
data={
query: query.hostname,
rcode: rcode,
answers: answers,
src_endpoint: src_endpoint,
dst_endpoint: dst_endpoint,
},
into=ai.classification,
timeout=10s,
concurrency=4
where ai.classification != null
enrichments = enrichments.add({
name: "query.hostname",
value: query.hostname,
type: "ai_classification",
provider: "Tenzir ai::prompt",
short_desc: ai.classification.text,
src_url: "http://127.0.0.1:11434/v1/responses",
created_time: now(),
data: {
text: ai.classification.text,
model: ai.classification.model,
usage: ai.classification.usage,
latency_ms: ai.classification.latency / 1ms,
},
})
drop ai

Use concurrency to control how many requests can run at the same time. Keep the value low until you understand your provider’s rate limits and your model latency.

To use a hosted OpenAI-compatible endpoint, set endpoint and api_key. Both arguments are resolved as secrets, so you can keep provider details out of the pipeline text:

from {
time: 2024-08-22T09:14:10,
category_uid: 2,
class_uid: 2004,
activity_id: 2,
activity_name: "Update",
type_uid: 200402,
severity_id: 3,
message: "Detection confidence increased after related network activity.",
metadata: {
version: "1.8.0",
},
finding_info: {
uid: "finding-43",
title: "Suspicious domain contact",
desc: "A workstation contacted a domain that appeared in a related DNS activity event.",
},
enrichments: [],
confidence_id: 2,
confidence: "Medium",
}
ai::prompt model="gpt-4.1-mini",
endpoint=secret("openai-endpoint"),
api_key=secret("openai-api-key"),
system="Suggest one next investigation step for this OCSF finding.",
data={
severity_id: severity_id,
confidence: confidence,
message: message,
finding: finding_info,
},
into=ai.next_step
where ai.next_step != null
enrichments = enrichments.add({
name: "finding_info.uid",
value: finding_info.uid,
type: "ai_next_step",
provider: "Tenzir ai::prompt",
short_desc: ai.next_step.text,
src_url: "https://api.openai.com/v1/responses",
created_time: now(),
data: {
text: ai.next_step.text,
model: ai.next_step.model,
usage: ai.next_step.usage,
latency_ms: ai.next_step.latency / 1ms,
},
})
drop ai

If a request fails, Tenzir keeps the input event and writes null to the temporary result field. Filter or route failed enrichments before you add an OCSF enrichment object:

where ai.next_step != null

Model calls can send sensitive event data to another process or service. Prefer data={...} over sending the whole event, omit fields that aren’t needed for the task, and use secrets for endpoint URLs and API keys.

Last updated: