Cron Ran curl at Three in the Morning
Rewrite one rule six times
Goal
Write six Sigma rules against 24 synthetic process execution records. You narrow the rules down while actually running them each time and confirming with numbers how many they hit and how many they missed.
Why it matters
The reason detection rules die in the field is usually not the syntax. If they are too broad and false positives pile up, people stop looking at the alerts, and if they are too narrow, a single address change means they catch nothing. The only way to find the place between the two is to run them against data. The habit of writing a rule, reading it with your eyes, thinking "that will probably match," and deploying it is the biggest cause of detection gaps.
That is why in this lab you apply each rule to the incident records every time you write one. Once the hits and the wrongly caught items come out as numbers, you learn "what comes in along with this condition" through observation, not intuition. This is why detection engineering, even among security work, is so similar to software engineering — rules are code, and incident records are test data.
Steps
- Count the incident records by parent process and save the result to
/root/sigma/01-parents.txt. - Write the minimal rule that catches every execution whose parent is cron to
/root/sigma/02-cron-any.yml. - Narrow it so that it catches only those that have
curlorwgetin the command line, and write it to/root/sigma/03-outbound.yml. Use only one selection. - Remove the normal backups sent to the internal repository (
repo.corp.internal) with a filter and write it to/root/sigma/04-filtered.yml. Usenotincondition. - Write a rule that catches by behavior, without using addresses, to
/root/sigma/05-behaviour.yml. The two behaviors are piping what was downloaded into a shell and attaching a shell and connecting outward. You must use the|remodifier. - Split the scheduled job's outbound request and the reverse shell into two selections whose names start with
selection_, group them with1 of selection_*, and write it to/root/sigma/06-combined.yml. Apply the internal repository filter to both in common. - Fill in the release metadata on the rule from step 6 and write it to
/root/sigma/07-release.yml. - Write the verdicts of the rules from steps 3 and 7 to
/root/sigma/08-review.mdas numbers and a judgment.
Notes
- The material is
/opt/lab/fixtures/detection/proc_events.jsonl, and each line is one event. Read it as injq -r '.ParentImage' <파일>(replace the placeholder with the file). - A tool for running the rules is provided.
python3 /opt/lab/fixtures/detection/sigma_eval.py --lint <규칙>looks only at the structure, and--ids <규칙> <사건파일>prints the ids of the matched events (the placeholders are the rule file and the event file). Check the supported scope with--help. - Common mistake: using
|containsto find a short word.ncis also insidersync. - Common mistake: mistaking a list of values inside a selection for AND. A list inside one field is OR.
- The ground truth set for steps 6–7 (4 real compromises) becomes visible when you combine what you saw in steps 5 and 3.
Skim the data first
Count /opt/lab/fixtures/detection/proc_events.jsonl by parent process (ParentImage) and save it to /root/sigma/01-parents.txt. It is enough for each line to have one path and its count together.
Extract only the values with jq -r '.ParentImage' and then count with sort | uniq -c. This step is for building the habit of seeing what exists and how much before writing a rule.
The smallest rule
Write the minimal Sigma rule that catches every execution whose parent is cron to /root/sigma/02-cron-any.yml. Only the required items (title, logsource, detection) are needed.
Put one named selection under detection and write that name in condition. What the ParentImage value of the events is can be found in the result of step 1.
Only what went outside
Narrow the rule from step 2 so that it catches only those with curl or wget in the command line, and write it to /root/sigma/03-outbound.yml. Keep only one selection in detection.
Writing two fields in one selection is AND, and giving a list of values to one field is OR. To look for a substring, attach a modifier after the field name.
Remove one normal case
From the result of step 3, remove the normal backups sent to the internal repository (repo.corp.internal) and write it to /root/sigma/04-filtered.yml. Do not narrow the selection; put a filter separately and attach it to condition with not.
Put two selections under detection and give them different names. condition takes the shape selection and not <필터이름> (the placeholder is the name of your filter).
By behavior, not by address
Write a rule that catches two things, piping what was downloaded into a shell and attaching a shell and connecting outward, to /root/sigma/05-behaviour.yml. Use the |re modifier, and do not write a destination address in the rule.
The regex modifier has the shape 필드|re: '<정규식>' (field name, then the pattern). Reread the command lines you saw in step 1 for what the pipe character and fragments like -e /bin/sh mean. You can split the two branches into two selections and group them with 1 of.
Two branches into one
Split the scheduled job's outbound request and the reverse shell into two selections whose names start with selection_, group them with 1 of selection_*, and write it to /root/sigma/06-combined.yml. The internal repository filter applies to both branches.
1 of selection_* groups the selections whose names match that pattern with OR. You attach the filter to that whole group with and not. The target is the 4 real compromises.
Into a deployable rule
Fill in id, status, description, author, date, references, falsepositives, level, and tags on the rule from step 6 and write it to /root/sigma/07-release.yml. The events it catches must be the same as in step 6.
id must be in UUID form (python3 -c 'import uuid;print(uuid.uuid4())'). For status, pick the value for a rule that has not yet been run against production data, and set level to suit a rule that also catches reverse shells. tags needs at least one entry that starts with attack..
Leave it as numbers
Compare the rules from steps 3 and 7 against the ground truth set (the 4 real compromises) and write it to /root/sigma/08-review.md. For each rule, write the file name and the four values matched=, tp=, fp=, and fn= on one line, and in a ## 판단 section (the judgment section; keep this heading as is) write, in at least 120 characters, what you would ship and why.
You can count the values yourself by comparing the --ids result with the ground truth set. The ground truth set is the two found in step 5 plus those of the ones found in step 3 that are not to the internal repository.