siem
Get alerts into a SIEM: format any map (or an alert from detect.run()) as CEF, LEEF or a syslog line, send syslog over UDP/TCP, and forward events to Splunk (HTTP Event Collector), Elasticsearch (bulk API), Microsoft Sentinel (Log Analytics data collector API), IBM QRadar (HTTP Receiver and reference sets) or Google SecOps / Chronicle (webhook feeds). Every sender has a dry-run mode that gives back the exact payload instead of sending it. Tokens and keys are never printed.
Formatting (cef, leef, syslog_line, event) and dry runs work everywhere, including the playground. Sending to a SIEM (send_syslog, splunk, elastic, sentinel) needs the TB app on your computer.
import siem
Example
import siem
let alert = {
"rule": "Brute force",
"level": "high",
"by": "203.0.113.42",
"by_field": "ip",
"count": 12,
}
say(siem.cef(alert))
say(siem.syslog_line("disk almost full", {"host": "web01", "app": "tb", "severity": "warning"}))
# Dry run: see the exact Splunk HEC payload without sending anything.
let hec = siem.splunk("https://splunk.example:8088", "not-a-real-token", {"dry_run": true})
let preview = hec.send([alert])
say(preview.headers.Authorization, preview.body)
Functions
cef(event, options?)
returns textOne line of CEF (ArcSight's Common Event Format), like: "CEF:0|TB|detect|3.0.0|brute_force|Brute force|8|src=... suser=... cnt=..."
event is an alert from detect.run() or any map.
Event fields
"vendor", "product", "version", "id"override the header."severity"overrides the header's severity (0-10)."level"sets the severity (critical = 10).- Other fields become extensions with standard CEF keys: ip -> src, user -> suser, message -> msg, and so on.
Options
keysa map {your_field: cef_key} for your own key names.
Visualizehow siem.cef works
siem.cef(alert, {"keep_names": false}) gives "CEF:0|TB|detect|3.2.0|brute_force|Brute force|10|rt=1773306845000 src=203.0.113.42 cnt=2…
Real result: worked out by running this example with TB while the site was built.
leef(event, options?)
returns textOne line of LEEF 2.0 (IBM QRadar's format), tab-separated attributes: "LEEF:2.0|TB|detect|3.0.0|brute_force|sev=8<tab>src=...". Same event shape as cef(). options: {"delimiter": "^", "keys": {...}}.
Visualizehow siem.leef works
siem.leef({"name": "Brute force", "level": "high", "ip": "203.0.113.42", "user": "root"}, {"delimiter": "^"}) gives "LEEF:2.0|TB|detect|3.2.0|brute_force|^|sev=8^cat=high^eventName=Brute force^src=203.0.11…siem.leef({"name": "Brute force", "level": "high", "ip": "203.0.113.42", "user": "root"}, {}) gives "LEEF:2.0|TB|detect|3.2.0|brute_force|sev=8\tcat=high\teventName=Brute force\tsrc=203.0.1…
Real result: worked out by running this example with TB while the site was built.
syslog_line(message_or_event, options?)
returns textA syslog line, RFC 5424 by default, like
"<PRI>1 2026-03-12T08:02:11.000Z host app - - [tb@32473 level=\"high\" src=\"...\"] Brute force"
An alert map's fields go into the structured data.
Options
format"rfc3164" for the classic "<PRI>Mar 12 08:02:11 host app: message".facilitylike "local0" (default: user).severitylike "warning", or 0-7 (default: from the alert's level).host, app, time, msgidvalues for those parts of the line.
Visualizehow siem.syslog_line works
siem.syslog_line({"name": "Brute force", "level": "high", "ip": "203.0.113.42"}, {"host": "web01", "app": "tb", "time": 1773300131}) gives "<11>1 2026-03-12T07:22:11.000Z web01 tb - - [tb@32473 level=\"high\" ip=\"203.0.113.42\"…siem.syslog_line({"name": "Brute force", "level": "high", "ip": "203.0.113.42"}, {"host": "web01", "app": "tb", "time": 1773300131, "format": "rfc3164"}) gives "<11>Mar 12 07:22:11 web01 tb: Brute force"
Real result: worked out by running this example with TB while the site was built.
send_syslog(host, port, text_or_lines, proto?)
returns mapSend one syslog line (or a list of lines) to a syslog server over "udp" (default) or "tcp" (one line per message, newline-framed). Gives back {"sent": lines, "bytes"}. Only send to servers you run.
Visualizehow siem.send_syslog works
siem.send_syslog("10.0.0.5", 514, siem.cef(alert), "udp") gives {"sent": 1, "bytes": 318}
Illustration: example values, drawn from what the function does.
event(alert)
returns mapThe flat event that cef(), leef() and the senders build from an alert. Handy to see what will be sent, or to tweak it before sending.
Fields
"name", "level", "id", "time"from the alert."severity"0-10.- The alert's by field, under its own name.
- count, description and tags.
- ip, user, host and message from the first record.
Visualizehow siem.event works
siem.event(alert) gives {"vendor": "TB", "product": "detect", "version": "3.2.0", "id": "brute_force", "name": "B…
Real result: worked out by running this example with TB while the site was built.
splunk(url, token, options?)
returns senderA Splunk HTTP Event Collector client. The token is never printed.
Methods
send(events)posts each map as an event. Alerts from detect.run() are flattened with siem.event().
Options
index, source, hostset on every event.sourcetypethe source type (default: "tb:detect").dry_runtrue to get the payload back instead of sending.timeoutseconds to wait.verifyfalse skips the TLS certificate check. Only for a test server with a self-signed certificate: anyone on the network could then read the token.
Returns (from send)
- "sent", "status", "ok", "response".
- In dry-run: "dry_run": true, "url", "headers" (token hidden), "body", "count".
Example
let s = siem.splunk("https://splunk.example:8088", "my-token", {"dry_run": true})
let out = s.send([{"user": "Tanisha", "action": "login"}])
say(out.body)
Visualizehow siem.splunk works
siem.splunk("https://splunk.example:8088", "TOKEN", {"dry_run": true, "index": "security"}).send([alert]) gives {"dry_run": true, "url": "https://splunk.example:8088/services/collector/event", "headers…
Real result: worked out by running this example with TB while the site was built.
elastic(url, auth?, options?)
returns senderAn Elasticsearch / OpenSearch client for the bulk API.
auth can be:
nilno login.{"user", "password"}basic auth.{"api_key": "..."}an API key.texta bearer token.
Methods
index(name, docs)indexes each map into the index name. Alerts from detect.run() are flattened and get @timestamp.
Options
dry_runtrue to get the payload back instead of sending.timeoutseconds to wait.verifyfalse skips the TLS certificate check. Only for a test server with a self-signed certificate: anyone on the network could then read the token.
Returns (from index)
- "sent", "status", "response".
"errors"how many documents failed.
Example
let auth = {"user": "tb", "password": "secret"}
let es = siem.elastic("https://es.example:9200", auth, {"dry_run": true})
let out = es.index("alerts-tb", [{"user": "Tanisha"}])
say(out.body)
Visualizehow siem.elastic works
siem.elastic("https://es.example:9200", {"user": "tb", "password": "x"}, {"dry_run": true}).index("alerts-tb", [alert]) gives {"dry_run": true, "url": "https://es.example:9200/_bulk", "headers": {"Content-Type": "ap…
Real result: worked out by running this example with TB while the site was built.
sentinel(workspace_id, key, options?)
returns senderA Microsoft Sentinel / Log Analytics client. It uses the HTTP Data Collector API with the shared-key signature.
key is the workspace's primary or secondary key (base64).
Methods
send(log_type, records)posts records to a custom table. send("TBAlerts", records) creates the table TBAlerts_CL.
Options
dry_runtrue to get the payload back instead of sending.timeoutseconds to wait.verifyfalse skips the TLS certificate check. Only for a test server with a self-signed certificate: anyone on the network could then read the token.urlanother endpoint (for tests and sovereign clouds).time_fieldthe record field holding the event time.
Returns (from send)
- "sent", "status", "ok".
Example
let s = siem.sentinel("my-workspace-id", "c2VjcmV0", {"dry_run": true})
let out = s.send("TBAlerts", [{"user": "Tanisha"}])
say(out.url)
Visualizehow siem.sentinel works
siem.sentinel("0b1d2c3e-workspace", "c2hhcmVkLWtleQ==", {"dry_run": true}).send("TBAlerts", [alert]) gives {"dry_run": true, "url": "https://0b1d2c3e-workspace.ods.opinsights.azure.com/api/logs?ap…
Real result: worked out by running this example with TB while the site was built.
qradar(url, options?)
returns senderAn IBM QRadar client.
Methods
send(events)posts events as LEEF lines to url, a log source using QRadar's HTTP Receiver protocol.reference_set(name, values)adds values (IPs, hashes...) to a reference set with the REST API (POST /api/reference_data/sets/bulk_load/name).
reference_set() authenticates with an authorized service token read only from the environment variable QRADAR_SEC_TOKEN; it is never printed.
Options
consolethe console address for the REST API (default: url without its port).versionthe REST API version header (default: "20.0").dry_runtrue to get the payload back instead of sending.timeoutseconds to wait.verifyfalse skips the TLS certificate check. Only for a test server with a self-signed certificate: anyone on the network could then read the token.
Example
let q = siem.qradar("https://qradar.example:12469", {"dry_run": true})
let out = q.reference_set("TB Bad IPs", ["203.0.113.42"])
say(out.url)
Visualizehow siem.qradar works
siem.qradar("https://qradar.example:12469", {"dry_run": true}).send([alert]) gives {"dry_run": true, "url": "https://qradar.example:12469", "headers": {"Content-Type": "tex…
Real result: worked out by running this example with TB while the site was built.
chronicle(url, options?)
returns senderA Google Security Operations (Chronicle) client for an HTTPS webhook feed.
url is the feed's endpoint, ending in :importPushLogs.
The API key and the feed's secret key come only from the environment variables CHRONICLE_API_KEY and CHRONICLE_SECRET. They travel in headers, never in the URL, and are never printed.
Methods
send(events)posts the events as newline-delimited JSON. Alerts from detect.run() are flattened with siem.event().
Options
dry_runtrue to get the payload back instead of sending.timeoutseconds to wait.verifyfalse skips the TLS certificate check. Only for a test server with a self-signed certificate: anyone on the network could then read the token.
Example
let feed = "https://us-chronicle.googleapis.com/v1alpha/projects/p"
feed += "/locations/us/instances/i/feeds/f:importPushLogs"
let c = siem.chronicle(feed, {"dry_run": true})
say(c.send([{"user": "Tanisha", "action": "login"}]).body)
Visualizehow siem.chronicle works
siem.chronicle("https://us-chronicle.googleapis.com/v1alpha/projects/p/locations/us/instances/i/feeds/f:importPushLogs", {"dry_run": true}).send([alert]) gives {"dry_run": true, "url": "https://us-chronicle.googleapis.com/v1alpha/projects/p/location…
Real result: worked out by running this example with TB while the site was built.
Values
| Name | Value | What it is |
|---|---|---|
siem.severities | {"informational": 2, "low": 3, "medium": 5, "high": 8, "critical": 10} | Alert level -> CEF severity (0-10) used by cef() and the senders. Visualizehow siem.severities works
No inputs: call it as it is. Real value, read from TB while the site was built. |