One YAML rule, any SIEM: introducing DetectForge
Every detection team I have talked to ends up in the same place: the same detection logic written four times. Once for Splunk, once for Elastic, once for Sentinel, once for the in-house thing nobody wants to touch. The rule that says "whoami.exe just ran" is identical everywhere. The query is not.
DetectForge is my attempt at the boring-but-important layer underneath that problem. It is a Python toolkit that parses portable YAML detection rules into typed objects, validates them, runs them through a processing pipeline, and hands them to a backend that emits the query for the platform you actually run. Repo: https://github.com/kiddsaut07/detectforge - LGPL-2.1, Python 3.11+.
What's actually in the box
- A typed rule model: rules, log sources, detections, detection items, tags, levels, statuses, MITRE ATT&CK references.
- Collections with global / repeat / reset actions,
load_ruleset, and merging, so shared metadata lives in one place. - Correlation rules:
event_count,value_count,temporal,temporal_ordered,value_sum,value_avg,value_percentile,value_median, with dependency-aware ordering. - Filters that rewrite matching rules by appending a negated condition before conversion.
- Processing pipelines: field mapping, prefix/suffix, value and string transformations, placeholders, custom state, external sources, per-rule conditions.
- Conversion backends built on a Backend /
TextQueryBackendframework with per-format finalisers and error collection. - Validation with pluggable validators, exclusions and configuration.
- Plugin discovery for backends, pipelines and validators.
Quick start, output pasted from a real run
pip install "git+https://github.com/kiddsaut07/detectforge.git"
from detectforge.backends.test import TextQueryTestBackend
from detectforge.collection import DetectForgeCollection
rules = DetectForgeCollection.from_yaml(
"""
title: Suspicious Whoami Execution
id: 11111111-1111-1111-1111-111111111111
status: test
description: Detects execution of whoami
logsource:
category: process_creation
product: windows
detection:
selection:
Image|endswith: '\\whoami.exe'
condition: selection
level: low
"""
)
print(TextQueryTestBackend().convert(rules))
['Image endswith "\\whoami.exe"']
TextQueryTestBackend is a reference backend that ships with the project (it exists so the test suite has a target). Real conversions come from plugin backends.
Collections and correlations
The interesting part starts when rules reference each other. This one document defines shared log source metadata, repeats it across rules, and defines a correlation on top:
action: global
logsource:
product: windows
---
action: repeat
title: "Failed Logon"
name: failed-logon
id: 22222222-2222-2222-2222-222222222222
status: test
detection:
selection:
EventID: 4625
condition: selection
---
title: Multiple Failed Logons
name: corr-multiple-failed-logons
status: test
correlation:
type: event_count
rules:
- failed-logon
group-by:
- TargetUserName
timespan: 5m
condition:
gte: 10
References are resolved by id or name, and the collection orders rules so dependencies are converted first. Output from the same run:
EventID=4625 | aggregate window=5min count() as event_count by TargetUserName | where event_count >= 10
The part I care about most: treating rules as untrusted input
Detection content is usually community content. You download a ruleset, you point a converter at it, and suddenly you are parsing ten thousand YAML files written by strangers into a query your SIEM will execute. A rule parser is an attack surface, so DetectForge is defensive by default:
Duplicate keys are rejected, not silently merged. Most YAML loaders keep the last value, which is exactly how you get a rule that does not do what it says it does:
YAMLError -> Duplicate key 'level'
Alias expansion is capped. YAML anchors are shared references when parsed, but rule processing walks the structure as a tree, so a chain of aliases expands exponentially. MAX_ALIAS_EXPANSION stops the billion-laughs pattern dead instead of exhausting memory.
RE2 is the default regex engine. SafePolicy compiles with Google's RE2 - no backtracking, so a pathological pattern in a third-party rule cannot pin a CPU. TrustedPolicy (plain Python re) is opt-in for the cases RE2 cannot express, rather than the other way around.
Template variables and external data sources are opt-in. Placeholders do not read files or pull data from disk unless you explicitly allow it.
There is also a validation framework, which is what catches the boring problems before conversion:
from detectforge.validation import DetectForgeValidator
from detectforge.validators.core import validators
validator = DetectForgeValidator(list(validators.values()))
for issue in validator.validate_rules(iter(rules.rules)):
print(issue)
issue=IdentifierExistenceIssue severity=medium description="Rule has no identifier (UUID)" rules=[None]
How the pipeline fits together
Rule YAML → typed objects → collection (globals applied, references resolved) → processing pipeline (field mapping, transformations, per-rule conditions) → backend → query text.
The backend layer is where most of the work lives: TextQueryBackend handles the expression tree, operator mapping, escaping, wildcards and per-format finalisers. Adding a platform means subclassing it and filling in the pieces your query language needs, not reimplementing rule handling.
Where it stands
Version 0.1.0. 18k lines of library code, a 1,500-test suite, Sphinx docs and Read the Docs config, and a single reference backend - the production backends are meant to arrive as plugins. It is early, and the API is still allowed to move.
What I would genuinely like feedback on:
- the shape of the Backend /
TextQueryBackendcontract for new platforms, - the correlation types that are missing (what do you actually write in production?),
- the plugin registry and its compatibility checks.
If that is useful to you, a star on the repo helps other detection engineers find it: https://github.com/kiddsaut07/detectforge. Issues and rule-format edge cases are even more welcome than stars.
Further reading
The DEV community has been writing about this exact problem, and these shaped how I framed the project:
- I built a portable SIEM detection toolkit that converts Sigma rules to Splunk, Elastic, and Kibana queries by tiltedlunar123
- Building SIEMForge: A Portable SIEM Detection Toolkit with Sigma, Sysmon, and MITRE ATT&CK by tiltedlunar123
- detflow: A Detection-Engineering Copilot You Can pip install by vinayiitkgp
If you have ever maintained the same detection in three query languages, I would like to hear how you solved it.
Comments
No comments yet. Start the discussion.