Detection and blue team
What Is a SIEM? Logs, Queries and Alerting
A SIEM collects logs, normalizes them, runs correlation rules and raises alerts for triage. Learn the pipeline with a runnable example.
Write a Sigma rule for failed Windows logons, validate it, convert it to Splunk, and test it against known-good and known-bad events.
Sigma is an open, vendor-neutral format for describing a detection. You write the logic once, in a small YAML file, and a converter turns it into the query language of the SIEM or log platform you actually run. Splunk, Elastic, Microsoft Sentinel and other backends all have converters, and the same rule can target several of them.
The format is maintained in the SigmaHQ project. The specification lives in the sigma-specification repository, and the rules documentation on sigmahq.io explains the fields one at a time. If you can read a Windows event, you already know the hard part. A Sigma rule is only a way to say "this event, with these field values" in a form that other people and other tools can read.
Most detection logic is written directly in one product's query language. A Splunk search does not run in Sentinel, and a KQL query does not run in Splunk. When a team moves platforms, or when a community shares a detection, the logic has to be rewritten from scratch. That is slow, and the rewrite introduces mistakes.
Sigma separates the detection logic from the query language. The rule describes the log source and the condition. The backend turns that into a query. Two consequences follow:
The second point is the one this article is about. A Sigma rule that parses and converts is not finished. It is only syntactically correct. It becomes useful when you can show that it fires on the events it should fire on and stays quiet on the events it should ignore.
Here is the rule you will build in the lab, with each field explained.
title: Failed Logon Event
id: bd37e49d-15ab-4e88-a3fb-53594e831b27
status: experimental
description: Detects Windows Security event 4625 (an account failed to log on).
logsource:
product: windows
service: security
detection:
selection:
EventID: 4625
condition: selection
falsepositives:
- Users mistyping passwords
level: low
tags:
- attack.credential-access
- attack.t1110Your UUID will be different. Generate your own with any UUID library, for example python -c "import uuid; print(uuid.uuid4())", and keep it when you edit the rule later. The id is how other tools track the same rule across versions.
stable, test, experimental, deprecated and unsupported. A new rule that has not been tested in a real environment belongs under experimental.product: windows and service: security point at the Windows Security event log. Other fields, such as category, let you describe a class of logs, for example process creation, without naming the exact channel.selection, is a set of field conditions. Field names are the ones the log source uses. Values are matched against those fields.selection alone means "events that match the selection block." Later you will add filter blocks and combine them with and and not.informational, low, medium, high and critical. Pick the level based on what a hit means for your environment, not on how exciting the technique sounds.attack. prefix points at a MITRE ATT&CK tactic, and attack.t1110 points at the technique T1110, Brute Force.Two notes on this rule. First, EventID is the field name in the Sigma world, and it refers to the Event ID you read in Event Viewer. Second, level: low is a choice. A single failed logon is common. The rule as written fires on every 4625, so a low level is the honest starting point until you add a threshold.
Sigma does not know your SIEM. A backend does. The converter reads the detection, applies a processing pipeline that renames fields and adds index or source restrictions, and writes a query in the target language.
A processing pipeline is a set of mapping rules. It answers questions such as: what is the Splunk field name for the Event ID, which index holds Windows Security events, and what sourcetype should the query search. Without a pipeline, a converter may output the Sigma field name as written. That output is often not usable on the platform you run.
Each backend has its own pipelines. Some are required, and the converter refuses to continue without one unless you explicitly ask it to skip the pipeline. You will see this in the lab.
The idea is simple: the rule stays the same, and the pipeline adapts it to the platform. When you move from Splunk to another SIEM, you change the backend and the pipeline, not the detection.
The lab runs on your own machine. You do not need a domain, a domain controller or any target system. You are writing and converting a rule. You are not generating failed logons against anything, and you should not try to do that against a system you do not own or have written permission to test.
This lab was run on Windows 11 with Git Bash, Python 3.12, and the default Windows Security log. The commands are the same on Linux and macOS, except for the path to the virtual environment.
Create a folder for the lab, then run:
python3 -m venv .venv
. .venv/bin/activate
pip install sigma-cliOn Windows with Git Bash, python3 may not be on your path. Use the full path to your Python 3.12 installation, or py -3.12. The virtual environment also puts its scripts in .venv/Scripts instead of .venv/bin, so activate it with:
. .venv/Scripts/activateCheck the install:
sigma versionOn the machine used for this article, sigma-cli 4.0.0 installed with pysigma 2.0.2 and Python 3.12.10. Your versions may be newer. The version output is what you should record in your own notes, not the numbers above.
Create a file called failed_logon.yml with the content shown in the previous section. Generate your own UUID for the id field. Save the file as UTF-8 with no tabs in the YAML indentation.
sigma check failed_logon.ymlsigma check parses the file and runs validators. It catches a missing field, a malformed tag, an unknown status or level, and several style issues.
On the machine used for this article, the check step did not complete. The tags validator loads the MITRE ATT&CK data over the network. The request returned an HTTP 504 gateway timeout, and the error repeated on each retry. The output was a traceback ending with RuntimeError: Failed to load MITRE ATT&CK data, and the exit code was 1.
That is a network dependency, not a rule error. You should see a pass result, or a list of validator issues, once the data loads. If the check fails with the same MITRE message, run it again later, or from a network that can reach the upstream data. Do not mark the rule as validated until the check finishes.
If the check reports issues, read each one. Fix the rule, not the validator. An issue about a tag usually means a typo such as attack.t1100 instead of attack.t1110.
sigma plugin install splunkOn the machine used for this article, the command printed Successfully installed plugin 'splunk' and pySigma version is compatible with sigma-cli.
sigma convert -t splunk --without-pipeline failed_logon.ymlThe output was:
Parsing Sigma rules
EventID=4625This is what the rule looks like before any Splunk mapping. The field name is still the Sigma name, EventID. A Splunk search over Windows events usually uses EventCode and a source, so this query would not find the events you want on a standard Windows add-on setup.
Try the same conversion without --without-pipeline:
sigma convert -t splunk failed_logon.ymlThe converter stopped and explained that the Splunk backend requires a processing pipeline. The message told the user to add --without-pipeline only if they knew what they were doing. The pipeline is required here, so the converter refuses to produce a query without one. Read that message as a safety feature.
Now add the Windows pipeline:
sigma list pipelines
sigma convert -t splunk -p splunk_windows failed_logon.ymlThe list showed three Splunk pipelines: splunk_windows, splunk_sysmon_acceleration and splunk_cim. The conversion with splunk_windows printed:
Parsing Sigma rules
source="WinEventLog:Security" EventCode=4625The pipeline changed two things. It mapped EventID to EventCode, and it added a source restriction to WinEventLog:Security. The result is a query that a Splunk instance with the Windows add-on can run. Your index and sourcetype may differ, so treat this as a starting point and check it against your own data.
Trying the CIM pipeline as well failed with Rule type not yet supported by the Splunk data model CIM pipeline!. Not every rule converts to every pipeline. A failure like that is useful information: the rule or the backend does not fit the mapping you asked for.
In the Windows event log article you looked at an event in Event Viewer. Open that event again and compare it with the query:
Event ID. In the Splunk output it appears as EventCode, because the pipeline maps it to the name the Windows add-on uses.source in the query, WinEventLog:Security, corresponds to the Security channel you read in Event Viewer.If your Event Viewer event has an Event ID other than 4625, the rule will not match it. That is correct behavior. The rule is about failed logons, not about every event in the log.
A rule that converts is still a guess until you test it. The minimum test has two halves:
Without the negatives, you have no evidence that the rule is specific. Without the positives, you have no evidence that it works at all.
For this lab, the test uses a small fixture file of synthetic events, events.json, with five entries: two 4625 events and three events that should not match. The script loads the rule's selection block and checks each event. It is a simplified test. It shows that the selection logic does what you wrote. It does not replace testing the converted query on real data.
import json, sys
import yaml
rule = yaml.safe_load(open("failed_logon.yml", encoding="utf-8"))
events = json.load(open("events.json", encoding="utf-8"))
selection = rule["detection"]["selection"]
def selection_matches(event):
return all(str(event.get(k)) == str(v) for k, v in selection.items())
failures = 0
for ev in events:
got = "match" if selection_matches(ev) else "no match"
ok = got == ev["expect"]
failures += 0 if ok else 1
print(f"{'PASS' if ok else 'FAIL'} {ev['name']:<40} expected={ev['expect']:<9} got={got}")
print(f"{len(events) - failures}/{len(events)} passed")
sys.exit(1 if failures else 0)The fixture file looks like this:
[
{"name": "known-bad: failed logon", "EventID": 4625, "expect": "match"},
{"name": "known-bad: failed logon, second try", "EventID": 4625, "expect": "match"},
{"name": "known-good: successful logon", "EventID": 4624, "expect": "no match"},
{"name": "known-good: logoff", "EventID": 4634, "expect": "no match"},
{"name": "known-good: process creation", "EventID": 4688, "expect": "no match"}
]Run it:
python test_failed_logon.pyOn the machine used for this article, the output was:
PASS known-bad: failed logon expected=match got=match
PASS known-bad: failed logon, second try expected=match got=match
PASS known-good: successful logon expected=no match got=no match
PASS known-good: logoff expected=no match got=no match
PASS known-good: process creation expected=no match got=no match
5/5 passedNotice what this test shows and what it does not. It shows that the rule matches the events it should match and ignores the ones it should ignore. It does not show that the rule is good. A rule that matched every 4625 would pass this test, because every 4625 in the fixture is supposed to match. The test is useful, but only as a floor.
The next step, in a real environment, is to run the converted Splunk query against a period of Windows Security logs that you know contains failed logons and ordinary activity. Count the results, and open a sample of them. Each hit should be a 4625 event. If the query returns hits for other Event IDs, the pipeline or the field mapping is wrong.
This rule fires on every 4625. A single mistyped password produces a hit, and so does a service account with an expired password that retries every minute. A brute-force attempt is many failures against the same account in a short time, and the first version cannot express that.
Sigma handles this with correlation rules, defined in a separate specification. A correlation rule does not match events itself. It points at one or more base rules by name, groups their matches by a field, and applies a count over a time window. The event_count type counts matches, and condition: gte: 5 means five or more. Put both rules in one file, separated by ---, and give the base rule a name:
title: Failed Logon Event
name: failed_logon_event
id: bd37e49d-15ab-4e88-a3fb-53594e831b27
status: experimental
description: Detects Windows Security event 4625 (an account failed to log on).
logsource:
product: windows
service: security
detection:
selection:
EventID: 4625
condition: selection
falsepositives:
- Users mistyping passwords
level: low
tags:
- attack.credential-access
- attack.t1110
---
title: Many Failed Logons For One Account
id: 3f6c1d52-8a0e-4b7e-9d2a-6c41e0b7a915
status: experimental
description: Five or more failed logons for the same target account within ten minutes.
correlation:
type: event_count
rules:
- failed_logon_event
group-by:
- TargetUserName
timespan: 10m
condition:
gte: 5
falsepositives:
- Service accounts with an expired password
level: mediumSave it as multi.yml and convert it with the same pipeline:
sigma convert -t splunk -p splunk_windows multi.ymlThe output was:
Parsing Sigma rules
source="WinEventLog:Security" EventCode=4625
| bin _time span=10m
| stats count as event_count by _time TargetUserName
| search event_count >= 5The first line is the base rule. The three lines after it are the correlation: bucket events into ten-minute windows, count them per TargetUserName, keep the groups with five or more. The numbers 5 and 10 minutes are placeholders. Pick them from your own data, and test the result with the same known-good and known-bad events. Not every backend supports every correlation type, so check the converter's output before you rely on it.
Keep the first version of the rule. It is the baseline, and the comparison with the correlated version shows whether the threshold helps.
EventID. Splunk uses EventCode after the pipeline. Mixing the two names in one rule causes silent misses.--without-pipeline is a deliberate flag. It is not a shortcut. The output may not run on your platform.level: high for a noisy rule. Severity should match what a hit means. A common event with no context should start low.sigma check tests the file's structure. It does not test whether the detection finds the right events.Once you have a rule that checks, converts and passes its tests, the next steps are straightforward:
|contains and |endswith, and the category field.The short version: a rule is a claim about events. Write the claim, convert it, and then show it with events that should match and events that should not.
Detection and blue team
A SIEM collects logs, normalizes them, runs correlation rules and raises alerts for triage. Learn the pipeline with a runnable example.
Detection and blue team
Read the Windows Security log: audit policy, event IDs 4624, 4625, 4634, 4672, 4688 and 1102, what Sysmon adds, and a failed logon you find yourself.