Skip to content

Commit d0d9272

Browse files
committed
Initial scaffold: structure, CI, starter content
0 parents  commit d0d9272

17 files changed

Lines changed: 1230 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
14+
- uses: actions/setup-python@v5
15+
with:
16+
python-version: "3.11"
17+
18+
- run: pip install uv
19+
20+
- run: uv pip install --system -e ".[dev]"
21+
22+
- name: Ruff
23+
run: ruff check src/
24+
25+
- name: Tests
26+
run: pytest -q

.gitignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
__pycache__/
2+
*.py[cod]
3+
.venv/
4+
venv/
5+
.env
6+
.pytest_cache/
7+
.ruff_cache/
8+
.mypy_cache/
9+
*.egg-info/
10+
dist/
11+
build/
12+
13+
.vscode/
14+
.idea/
15+
.DS_Store
16+
Thumbs.db
17+
18+
# Generated outputs
19+
converted/
20+
*.jsonl
21+
navigator_layer.json

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Isaac Ojeh
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# detection-tooling
2+
3+
Python utilities I use to operate detection content as code: convert Sigma rules between SIEM backends, score MITRE ATT&CK coverage, validate rule metadata, generate test telemetry, and normalize logs.
4+
5+
These are intended as small, focused tools — each one does a single job well, reads its inputs from stdin or `--file`, and writes structured output suitable for piping into the next tool.
6+
7+
## What's inside
8+
9+
| Script | One-liner | Reads | Writes |
10+
|---|---|---|---|
11+
| [`sigma_to_sentinel`](src/detection_tooling/sigma_to_sentinel.md) | Convert a Sigma rule to a Sentinel analytic rule (KQL + ARM template) | Sigma YAML | ARM JSON |
12+
| [`attack_coverage`](src/detection_tooling/attack_coverage.md) | Build an ATT&CK Navigator layer from a folder of Sigma rules | Sigma YAML | Navigator JSON |
13+
| [`rule_validator`](src/detection_tooling/rule_validator.md) | Lint Sigma rules — schema, UUIDs, naming, required tags | Sigma YAML | Pass/fail report |
14+
| [`telemetry_generator`](src/detection_tooling/telemetry_generator.md) | Emit synthetic Sysmon-style events to test detections | Template YAML | JSONL events |
15+
| [`log_normalizer`](src/detection_tooling/log_normalizer.md) | Map raw Windows/Linux/JSON logs to ECS-style fields | Raw logs | ECS JSON |
16+
17+
## Install
18+
19+
```bash
20+
git clone https://github.com/liliyke/detection-tooling
21+
cd detection-tooling
22+
uv sync
23+
```
24+
25+
If you're on plain pip:
26+
27+
```bash
28+
python -m venv .venv && source .venv/bin/activate
29+
pip install -e ".[dev]"
30+
```
31+
32+
## Usage pattern
33+
34+
Each tool exposes a CLI named after the module:
35+
36+
```bash
37+
uv run sigma-to-sentinel --rule path/to/rule.yml --out converted/
38+
uv run attack-coverage --rules-dir ../detection-rules/rules > navigator_layer.json
39+
uv run rule-validator --rules-dir ../detection-rules/rules
40+
uv run telemetry-generator --template templates/lsass_access.yml --count 10 > events.jsonl
41+
uv run log-normalizer --input /var/log/auth.log --source linux_auth > normalized.jsonl
42+
```
43+
44+
`--help` on any one lists every flag.
45+
46+
## Tests
47+
48+
```bash
49+
uv run pytest -q
50+
```
51+
52+
CI runs the same. Tests cover happy-path conversion, schema validation, and one round-trip per backend.
53+
54+
## License
55+
56+
MIT.

pyproject.toml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[project]
2+
name = "detection-tooling"
3+
version = "0.1.0"
4+
description = "Small CLI utilities for operating Sigma-based detection content"
5+
requires-python = ">=3.10"
6+
dependencies = [
7+
"pysigma>=0.11.0",
8+
"pysigma-backend-kusto>=0.4.0",
9+
"pysigma-backend-splunk>=1.1.0",
10+
"pysigma-backend-elasticsearch>=1.1.0",
11+
"pyyaml>=6.0",
12+
"click>=8.1",
13+
"rich>=13.7",
14+
]
15+
16+
[project.optional-dependencies]
17+
dev = [
18+
"pytest>=8.0",
19+
"ruff>=0.5.0",
20+
]
21+
22+
[project.scripts]
23+
sigma-to-sentinel = "detection_tooling.sigma_to_sentinel:cli"
24+
attack-coverage = "detection_tooling.attack_coverage:cli"
25+
rule-validator = "detection_tooling.rule_validator:cli"
26+
telemetry-generator = "detection_tooling.telemetry_generator:cli"
27+
log-normalizer = "detection_tooling.log_normalizer:cli"
28+
29+
[build-system]
30+
requires = ["setuptools>=68"]
31+
build-backend = "setuptools.build_meta"
32+
33+
[tool.setuptools.packages.find]
34+
where = ["src"]
35+
36+
[tool.pytest.ini_options]
37+
testpaths = ["tests"]

src/detection_tooling/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Detection engineering CLI tools."""
2+
3+
__version__ = "0.1.0"
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# attack-coverage
2+
3+
Build an [ATT&CK Navigator](https://mitre-attack.github.io/attack-navigator/) layer from a folder of Sigma rules. Useful for showing "here's what my detection content covers" in a single picture, and for spotting tactics where you're light.
4+
5+
## What it does
6+
7+
1. Walks every `.yml` under `--rules-dir`.
8+
2. Reads each rule's `tags:` list and pulls out `attack.tXXXX[.YYY]` entries.
9+
3. Tallies how many rules touch each technique.
10+
4. Writes a Navigator layer JSON. Score = rule count. Heatmap colors run from light blue (one rule) to dark blue (many).
11+
12+
## Usage
13+
14+
```bash
15+
# Print layer JSON to stdout
16+
uv run attack-coverage --rules-dir ../detection-rules/rules
17+
18+
# Write to a file + print a table to stderr for quick inspection
19+
uv run attack-coverage --rules-dir ../detection-rules/rules \
20+
--out coverage.json \
21+
--table
22+
```
23+
24+
Then drop `coverage.json` into the Navigator: open https://mitre-attack.github.io/attack-navigator/, click "Open Existing Layer → Upload from local", select the JSON.
25+
26+
## Sample table output
27+
28+
```
29+
Technique coverage
30+
┏━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
31+
┃ Technique ┃ Rules ┃ Files ┃
32+
┡━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
33+
│ T1003 │ 2 │ lsass_memory_access_un... │
34+
│ T1003.001 │ 2 │ lsass_memory_access_un... │
35+
│ T1547.001 │ 1 │ registry_run_key_modif... │
36+
│ T1218.011 │ 1 │ rundll32_unusual_parent.yml │
37+
│ T1547 │ 0 │ — │
38+
└───────────┴───────┴───────────────────────────────┘
39+
```
40+
41+
## How to read the Navigator output
42+
43+
- **Solid colored cells** — techniques with at least one rule. Darker = more rules.
44+
- **Empty cells in a tactic column** — gaps. Worth a hunt design exercise to figure out whether you can cover them.
45+
- **Hover text** — shows the source rule filenames, so you can pivot back to the YAML.
46+
47+
## Hooking into CI
48+
49+
Commit `coverage.json` to your repo on every push so the file is always current:
50+
51+
```yaml
52+
- name: Generate coverage layer
53+
run: uv run attack-coverage --rules-dir rules --out coverage.json
54+
55+
- name: Commit if changed
56+
uses: stefanzweifel/git-auto-commit-action@v5
57+
with:
58+
commit_message: "Update ATT&CK coverage layer"
59+
file_pattern: coverage.json
60+
```
61+
62+
## Tuning ideas
63+
64+
- **Two-color heatmap.** Add `--threshold N` so techniques with ≥N rules render in one color and others in another. Easier-to-read at a glance.
65+
- **Sub-technique aggregation.** Currently each subtechnique is its own row. Add a `--rollup` mode that aggregates a subtechnique's count into its parent.
66+
- **Per-platform layers.** Filter by `logsource.product` and emit one layer per platform — "Windows coverage", "Linux coverage", "cloud coverage".
67+
- **Diff mode.** Two `--rules-dir` flags. Output highlights techniques *gained* and *lost* between two repo states.
68+
- **Coverage-quality scoring.** Currently coverage is binary (any rule fires count). Add a `quality:` field to your Sigma rules (`level` could substitute) and weight the score — `critical=4, high=3, medium=2, low=1`.
69+
- **Combine with adversary-emulation results.** If you have a YAML mapping of "atomic tests we run", layer that too — green where you have *and* test, yellow where you have but don't test, red where you have neither.
70+
71+
## References
72+
73+
- [ATT&CK Navigator GitHub](https://github.com/mitre-attack/attack-navigator)
74+
- [Layer file format spec](https://github.com/mitre-attack/attack-navigator/blob/master/layers/LAYERFORMATv4_5.md)
75+
- [Detection-as-code: ATT&CK coverage tracking — SpecterOps](https://posts.specterops.io/)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Build an ATT&CK Navigator layer JSON from a folder of Sigma rules.
2+
3+
Walks every .yml under --rules-dir, harvests `attack.tXXXX[.YYY]` tags, then
4+
emits a Navigator layer where each technique's score is the count of rules
5+
covering it. Higher score = more redundancy / depth.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import json
11+
import re
12+
import sys
13+
from collections import Counter
14+
from pathlib import Path
15+
16+
import click
17+
import yaml
18+
from rich.console import Console
19+
from rich.table import Table
20+
21+
console = Console()
22+
23+
TECH_RE = re.compile(r"^attack\.(t\d{4}(?:\.\d{3})?)$")
24+
25+
26+
def _iter_techniques(rules_dir: Path):
27+
for p in rules_dir.rglob("*.yml"):
28+
with p.open(encoding="utf-8") as f:
29+
rule = yaml.safe_load(f) or {}
30+
for tag in rule.get("tags", []) or []:
31+
m = TECH_RE.match(tag)
32+
if m:
33+
yield m.group(1).upper(), p
34+
35+
36+
@click.command()
37+
@click.option("--rules-dir", type=click.Path(exists=True, path_type=Path), required=True,
38+
help="Root directory containing Sigma .yml files (recurses).")
39+
@click.option("--name", default="Sigma Coverage",
40+
help="Layer name shown in Navigator.")
41+
@click.option("--description", default="Generated from local Sigma rules",
42+
help="Layer description.")
43+
@click.option("--out", "out_path", type=click.Path(path_type=Path), default=None,
44+
help="Write JSON to this file. If omitted, prints to stdout.")
45+
@click.option("--table", is_flag=True,
46+
help="Also print a coverage table to stderr.")
47+
def cli(rules_dir: Path, name: str, description: str, out_path: Path | None, table: bool) -> None:
48+
"""Output an ATT&CK Navigator layer JSON summarizing rule coverage."""
49+
counts: Counter[str] = Counter()
50+
by_tech: dict[str, list[str]] = {}
51+
52+
for tech, path in _iter_techniques(rules_dir):
53+
counts[tech] += 1
54+
by_tech.setdefault(tech, []).append(path.name)
55+
56+
layer = {
57+
"name": name,
58+
"versions": {"attack": "14", "navigator": "5.0.0", "layer": "4.5"},
59+
"domain": "enterprise-attack",
60+
"description": description,
61+
"techniques": [
62+
{
63+
"techniqueID": tech,
64+
"score": cnt,
65+
"color": "",
66+
"comment": "; ".join(by_tech[tech]),
67+
"enabled": True,
68+
}
69+
for tech, cnt in counts.most_common()
70+
],
71+
"gradient": {
72+
"colors": ["#a4d3f5", "#1e6091"],
73+
"minValue": 1,
74+
"maxValue": max(counts.values()) if counts else 1,
75+
},
76+
"legendItems": [],
77+
"showTacticRowBackground": False,
78+
"tacticRowBackground": "#dddddd",
79+
"selectTechniquesAcrossTactics": True,
80+
}
81+
82+
output = json.dumps(layer, indent=2)
83+
if out_path:
84+
out_path.write_text(output, encoding="utf-8")
85+
console.print(f"[green]wrote[/green] {out_path}", file=sys.stderr)
86+
else:
87+
sys.stdout.write(output + "\n")
88+
89+
if table:
90+
t = Table(title="Technique coverage")
91+
t.add_column("Technique")
92+
t.add_column("Rules", justify="right")
93+
t.add_column("Files")
94+
for tech, cnt in counts.most_common():
95+
t.add_row(tech, str(cnt), ", ".join(by_tech[tech]))
96+
console.print(t, file=sys.stderr)
97+
98+
99+
if __name__ == "__main__":
100+
cli()

0 commit comments

Comments
 (0)