detkit-cli 0.1.1__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- detkit_cli-0.1.4/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
- detkit_cli-0.1.4/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
- detkit_cli-0.1.4/.github/workflows/ci.yml +21 -0
- detkit_cli-0.1.4/.github/workflows/publish.yml +34 -0
- detkit_cli-0.1.4/.gitignore +9 -0
- detkit_cli-0.1.4/.pre-commit-hooks.yaml +26 -0
- detkit_cli-0.1.4/CONTRIBUTING.md +44 -0
- {detkit_cli-0.1.1 → detkit_cli-0.1.4}/PKG-INFO +19 -4
- {detkit_cli-0.1.1 → detkit_cli-0.1.4}/README.md +17 -1
- detkit_cli-0.1.4/action.yml +27 -0
- detkit_cli-0.1.4/detkit/__init__.py +6 -0
- {detkit_cli-0.1.1 → detkit_cli-0.1.4}/detkit/cli.py +20 -7
- {detkit_cli-0.1.1 → detkit_cli-0.1.4}/detkit/evaluator.py +22 -8
- detkit_cli-0.1.4/detkit-demo.gif +0 -0
- detkit_cli-0.1.4/detkit-demo.tape +51 -0
- {detkit_cli-0.1.1 → detkit_cli-0.1.4}/detkit_cli.egg-info/PKG-INFO +19 -4
- detkit_cli-0.1.4/detkit_cli.egg-info/SOURCES.txt +28 -0
- detkit_cli-0.1.4/detkit_cli.egg-info/scm_file_list.json +24 -0
- detkit_cli-0.1.4/detkit_cli.egg-info/scm_version.json +8 -0
- detkit_cli-0.1.4/examples/rules/whoami_execution.test.yml +23 -0
- detkit_cli-0.1.4/examples/rules/whoami_execution.yml +15 -0
- {detkit_cli-0.1.1 → detkit_cli-0.1.4}/pyproject.toml +37 -36
- {detkit_cli-0.1.1 → detkit_cli-0.1.4}/tests/test_evaluator.py +18 -5
- detkit_cli-0.1.1/detkit/__init__.py +0 -1
- detkit_cli-0.1.1/detkit_cli.egg-info/SOURCES.txt +0 -14
- {detkit_cli-0.1.1 → detkit_cli-0.1.4}/LICENSE +0 -0
- {detkit_cli-0.1.1 → detkit_cli-0.1.4}/detkit/__main__.py +0 -0
- {detkit_cli-0.1.1 → detkit_cli-0.1.4}/detkit_cli.egg-info/dependency_links.txt +0 -0
- {detkit_cli-0.1.1 → detkit_cli-0.1.4}/detkit_cli.egg-info/entry_points.txt +0 -0
- {detkit_cli-0.1.1 → detkit_cli-0.1.4}/detkit_cli.egg-info/requires.txt +0 -0
- {detkit_cli-0.1.1 → detkit_cli-0.1.4}/detkit_cli.egg-info/top_level.txt +0 -0
- {detkit_cli-0.1.1 → detkit_cli-0.1.4}/setup.cfg +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug report
|
|
3
|
+
about: A rule evaluates the wrong way, or detkit crashes
|
|
4
|
+
labels: bug
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**The rule** — paste the Sigma (a minimal version is fine):
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**The event and what you expected** — the sample event, whether it should match or not, and what detkit did instead:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Version:** output of `detkit --version`
|
|
20
|
+
**Command:** how you ran it, e.g. `detkit test rules`
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Feature request
|
|
3
|
+
about: Suggest something detkit should do
|
|
4
|
+
labels: enhancement
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**What are you trying to do?** Describe the workflow, not just the feature.
|
|
8
|
+
|
|
9
|
+
**What would help?**
|
|
10
|
+
|
|
11
|
+
**A Sigma rule or example, if it's relevant:**
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
|
|
15
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
|
|
4
|
+
jobs:
|
|
5
|
+
test:
|
|
6
|
+
runs-on: ubuntu-latest
|
|
7
|
+
strategy:
|
|
8
|
+
matrix:
|
|
9
|
+
python-version: ['3.9', '3.12']
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
with:
|
|
13
|
+
fetch-depth: 0
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: ${{ matrix.python-version }}
|
|
17
|
+
- run: pip install -e .
|
|
18
|
+
- name: Unit checks (evaluator core)
|
|
19
|
+
run: python tests/test_evaluator.py
|
|
20
|
+
- name: Example rules must pass their tests
|
|
21
|
+
run: detkit test examples/rules
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
release:
|
|
6
|
+
types: [published]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: pypi
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
with:
|
|
17
|
+
fetch-depth: 0
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: '3.x'
|
|
21
|
+
|
|
22
|
+
- name: Build
|
|
23
|
+
run: |
|
|
24
|
+
pip install build
|
|
25
|
+
python -m build
|
|
26
|
+
|
|
27
|
+
- name: Don't ship a broken build
|
|
28
|
+
run: |
|
|
29
|
+
pip install dist/*.whl
|
|
30
|
+
python tests/test_evaluator.py
|
|
31
|
+
detkit test examples/rules
|
|
32
|
+
|
|
33
|
+
- name: Publish to PyPI
|
|
34
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Lets other repos use detkit as a pre-commit hook. In a consuming repo's
|
|
2
|
+
# .pre-commit-config.yaml:
|
|
3
|
+
#
|
|
4
|
+
# repos:
|
|
5
|
+
# - repo: https://github.com/ELSATOAH/detkit
|
|
6
|
+
# rev: v0.1.3
|
|
7
|
+
# hooks:
|
|
8
|
+
# - id: detkit-test
|
|
9
|
+
# args: [rules] # path to your rules
|
|
10
|
+
- id: detkit-test
|
|
11
|
+
name: detkit test
|
|
12
|
+
description: Test Sigma detection rules against their sample events
|
|
13
|
+
entry: detkit test
|
|
14
|
+
language: python
|
|
15
|
+
types: [yaml]
|
|
16
|
+
args: [rules]
|
|
17
|
+
pass_filenames: false
|
|
18
|
+
|
|
19
|
+
- id: detkit-validate
|
|
20
|
+
name: detkit validate
|
|
21
|
+
description: Structurally validate Sigma detection rules
|
|
22
|
+
entry: detkit validate
|
|
23
|
+
language: python
|
|
24
|
+
types: [yaml]
|
|
25
|
+
args: [rules]
|
|
26
|
+
pass_filenames: false
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Contributing to detkit
|
|
2
|
+
|
|
3
|
+
Thanks for helping out. detkit is small on purpose, so getting started is quick.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/ELSATOAH/detkit
|
|
9
|
+
cd detkit
|
|
10
|
+
pip install -e . # a venv or pipx works too
|
|
11
|
+
python3 tests/test_evaluator.py # the whole test suite, no framework needed
|
|
12
|
+
detkit test examples/rules # should print all green
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Where things live
|
|
16
|
+
|
|
17
|
+
- `detkit/evaluator.py` — the core: evaluate a Sigma `detection`/`condition` against one event. Most changes go here.
|
|
18
|
+
- `detkit/cli.py` — the `test` / `validate` / `init` commands.
|
|
19
|
+
- `tests/test_evaluator.py` — plain `assert`s; run it directly or with pytest.
|
|
20
|
+
- `examples/rules/` — a rule and its `*.test.yml`, which doubles as a spec for the format.
|
|
21
|
+
|
|
22
|
+
## The one rule
|
|
23
|
+
|
|
24
|
+
Never return a confident wrong answer. What the evaluator can't handle yet is listed by `scan_unsupported()` and printed as a `WARN` at runtime instead of guessing. If you add a feature, either handle it properly or leave the warning in place.
|
|
25
|
+
|
|
26
|
+
## Adding support for a Sigma feature
|
|
27
|
+
|
|
28
|
+
1. Add the logic in `evaluator.py` — `_match_scalar` for modifiers, `_resolve_field` for field access, `_eval_condition` for conditions.
|
|
29
|
+
2. If it was in `scan_unsupported()`'s warn list, drop it from there.
|
|
30
|
+
3. Add a test: one event that should match, one that shouldn't.
|
|
31
|
+
|
|
32
|
+
Known ceilings are marked with `# ponytail:` comments in `evaluator.py`.
|
|
33
|
+
|
|
34
|
+
## Good places to start
|
|
35
|
+
|
|
36
|
+
- `|base64` / `|base64offset` modifier support (currently warns).
|
|
37
|
+
- Nested fields that descend into arrays of objects (right now it only walks dicts).
|
|
38
|
+
- A `--json` output mode for `detkit test`, for pipelines that want structured results.
|
|
39
|
+
|
|
40
|
+
## Releases
|
|
41
|
+
|
|
42
|
+
Maintainers only, and it's one step: `gh release create vX.Y.Z --generate-notes`. The tag sets the version (setuptools-scm) and GitHub Actions builds and publishes to PyPI.
|
|
43
|
+
|
|
44
|
+
MIT licensed. By contributing, you agree your work ships under it.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: detkit-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: dbt for detections — test, validate, and CI-gate Sigma detection rules as code.
|
|
5
5
|
Author: detkit
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/ELSATOAH/detkit
|
|
8
8
|
Project-URL: Repository, https://github.com/ELSATOAH/detkit
|
|
9
9
|
Project-URL: Issues, https://github.com/ELSATOAH/detkit/issues
|
|
@@ -11,7 +11,6 @@ Keywords: sigma,detection-engineering,detection-as-code,siem,security,detections
|
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Information Technology
|
|
13
13
|
Classifier: Topic :: Security
|
|
14
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
15
14
|
Classifier: Programming Language :: Python :: 3
|
|
16
15
|
Classifier: Operating System :: OS Independent
|
|
17
16
|
Requires-Python: >=3.9
|
|
@@ -22,7 +21,7 @@ Dynamic: license-file
|
|
|
22
21
|
|
|
23
22
|
# detkit
|
|
24
23
|
|
|
25
|
-
[](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml) [](https://pypi.org/project/detkit-cli/) [](https://github.com/ELSATOAH/detkit/blob/main/LICENSE)
|
|
24
|
+
[](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml) [](https://pypi.org/project/detkit-cli/) [](https://pypi.org/project/detkit-cli/) [](https://github.com/ELSATOAH/detkit/blob/main/LICENSE)
|
|
26
25
|
|
|
27
26
|
Unit tests for your Sigma detection rules. Write a rule, add a couple of example log events, and detkit tells you whether it fires on the ones that should trip it and stays quiet on the ones that shouldn't. Wire it into CI and a broken detection fails the pull request instead of failing silently in production.
|
|
28
27
|
|
|
@@ -90,6 +89,22 @@ jobs:
|
|
|
90
89
|
path: rules
|
|
91
90
|
```
|
|
92
91
|
|
|
92
|
+
## pre-commit
|
|
93
|
+
|
|
94
|
+
Prefer to catch it before the commit lands? Add detkit to your [pre-commit](https://pre-commit.com) config:
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
# .pre-commit-config.yaml
|
|
98
|
+
repos:
|
|
99
|
+
- repo: https://github.com/ELSATOAH/detkit
|
|
100
|
+
rev: v0.1.3
|
|
101
|
+
hooks:
|
|
102
|
+
- id: detkit-test
|
|
103
|
+
args: [rules] # path to your rules
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
There's a `detkit-validate` hook too, for the lighter structural check.
|
|
107
|
+
|
|
93
108
|
## What it handles
|
|
94
109
|
|
|
95
110
|
detkit runs your rules locally. No SIEM, no credentials, nothing leaves your machine or CI runner. I ran it against every rule in the SigmaHQ repo, and about 91% use only features it understands today: `contains`, `startswith`, `endswith`, `re`, value wildcards (`*` and `?`), `|cidr`, keyword lists, and `X of` / `all of` conditions.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# detkit
|
|
2
2
|
|
|
3
|
-
[](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml) [](https://pypi.org/project/detkit-cli/) [](https://github.com/ELSATOAH/detkit/blob/main/LICENSE)
|
|
3
|
+
[](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml) [](https://pypi.org/project/detkit-cli/) [](https://pypi.org/project/detkit-cli/) [](https://github.com/ELSATOAH/detkit/blob/main/LICENSE)
|
|
4
4
|
|
|
5
5
|
Unit tests for your Sigma detection rules. Write a rule, add a couple of example log events, and detkit tells you whether it fires on the ones that should trip it and stays quiet on the ones that shouldn't. Wire it into CI and a broken detection fails the pull request instead of failing silently in production.
|
|
6
6
|
|
|
@@ -68,6 +68,22 @@ jobs:
|
|
|
68
68
|
path: rules
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
## pre-commit
|
|
72
|
+
|
|
73
|
+
Prefer to catch it before the commit lands? Add detkit to your [pre-commit](https://pre-commit.com) config:
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
# .pre-commit-config.yaml
|
|
77
|
+
repos:
|
|
78
|
+
- repo: https://github.com/ELSATOAH/detkit
|
|
79
|
+
rev: v0.1.3
|
|
80
|
+
hooks:
|
|
81
|
+
- id: detkit-test
|
|
82
|
+
args: [rules] # path to your rules
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
There's a `detkit-validate` hook too, for the lighter structural check.
|
|
86
|
+
|
|
71
87
|
## What it handles
|
|
72
88
|
|
|
73
89
|
detkit runs your rules locally. No SIEM, no credentials, nothing leaves your machine or CI runner. I ran it against every rule in the SigmaHQ repo, and about 91% use only features it understands today: `contains`, `startswith`, `endswith`, `re`, value wildcards (`*` and `?`), `|cidr`, keyword lists, and `X of` / `all of` conditions.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: 'detkit — test detections'
|
|
2
|
+
description: 'Run detkit against your Sigma detection rules to gate pull requests in CI.'
|
|
3
|
+
branding:
|
|
4
|
+
icon: 'shield'
|
|
5
|
+
color: 'purple'
|
|
6
|
+
inputs:
|
|
7
|
+
path:
|
|
8
|
+
description: 'Directory containing your Sigma rules and their *.test.yml files.'
|
|
9
|
+
required: false
|
|
10
|
+
default: '.'
|
|
11
|
+
command:
|
|
12
|
+
description: 'Which detkit command to run: test or validate.'
|
|
13
|
+
required: false
|
|
14
|
+
default: 'test'
|
|
15
|
+
runs:
|
|
16
|
+
using: 'composite'
|
|
17
|
+
steps:
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: '3.11'
|
|
22
|
+
- name: Install detkit
|
|
23
|
+
shell: bash
|
|
24
|
+
run: pip install "${{ github.action_path }}"
|
|
25
|
+
- name: Run detkit
|
|
26
|
+
shell: bash
|
|
27
|
+
run: detkit "${{ inputs.command }}" "${{ inputs.path }}"
|
|
@@ -5,8 +5,18 @@ import os
|
|
|
5
5
|
import re
|
|
6
6
|
import sys
|
|
7
7
|
|
|
8
|
+
from . import __version__
|
|
8
9
|
from .evaluator import event_matches_rule, scan_unsupported
|
|
9
10
|
|
|
11
|
+
# Color only when writing to a terminal, and honor the NO_COLOR convention,
|
|
12
|
+
# so piped and CI output stays plain.
|
|
13
|
+
_COLOR = sys.stdout.isatty() and not os.environ.get("NO_COLOR")
|
|
14
|
+
_GREEN, _RED, _YELLOW, _DIM = "32", "31", "33", "2"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _c(text, code):
|
|
18
|
+
return f"\033[{code}m{text}\033[0m" if _COLOR else text
|
|
19
|
+
|
|
10
20
|
REQUIRED_KEYS = ("title", "logsource", "detection")
|
|
11
21
|
|
|
12
22
|
|
|
@@ -47,11 +57,11 @@ def cmd_test(args):
|
|
|
47
57
|
detection = (rule or {}).get("detection")
|
|
48
58
|
if isinstance(detection, dict):
|
|
49
59
|
for warn in scan_unsupported(detection):
|
|
50
|
-
print(f" ! {rule_path} WARN: {warn} (result may be unreliable)")
|
|
60
|
+
print(f" {_c('!', _YELLOW)} {rule_path} WARN: {warn} (result may be unreliable)")
|
|
51
61
|
test_path = _test_file_for(rule_path)
|
|
52
62
|
if not test_path:
|
|
53
63
|
untested += 1
|
|
54
|
-
print(f" ? {rule_path} (no .test.yml)")
|
|
64
|
+
print(f" {_c('?', _DIM)} {rule_path} (no .test.yml)")
|
|
55
65
|
continue
|
|
56
66
|
cases = (_load_yaml(test_path) or {}).get("tests", [])
|
|
57
67
|
for case in cases:
|
|
@@ -60,18 +70,19 @@ def cmd_test(args):
|
|
|
60
70
|
got = event_matches_rule(detection, case.get("event", {}))
|
|
61
71
|
except Exception as exc: # a rule error should fail the test, not crash the run
|
|
62
72
|
failed += 1
|
|
63
|
-
print(f" x {rule_path} :: {case.get('name', '?')} ERROR: {exc}")
|
|
73
|
+
print(f" {_c('x', _RED)} {rule_path} :: {case.get('name', '?')} ERROR: {exc}")
|
|
64
74
|
continue
|
|
65
75
|
if got == want:
|
|
66
76
|
passed += 1
|
|
67
|
-
print(f" . {rule_path} :: {case.get('name', '?')}")
|
|
77
|
+
print(f" {_c('.', _GREEN)} {rule_path} :: {case.get('name', '?')}")
|
|
68
78
|
else:
|
|
69
79
|
failed += 1
|
|
70
80
|
verb = "fired" if got else "did not fire"
|
|
71
|
-
print(f" x {rule_path} :: {case.get('name', '?')} (rule {verb}, expected {case['expect']})")
|
|
81
|
+
print(f" {_c('x', _RED)} {rule_path} :: {case.get('name', '?')} (rule {verb}, expected {case['expect']})")
|
|
72
82
|
tested = len(rules) - untested
|
|
73
83
|
pct = round(100 * tested / len(rules)) if rules else 0
|
|
74
|
-
|
|
84
|
+
failed_str = _c(f"{failed} failed", _RED) if failed else f"{failed} failed"
|
|
85
|
+
print(f"\n{_c(f'{passed} passed', _GREEN)}, {failed_str}")
|
|
75
86
|
print(f"{tested} of {len(rules)} rules have tests ({pct}% coverage)")
|
|
76
87
|
return 1 if failed else 0
|
|
77
88
|
|
|
@@ -113,7 +124,8 @@ def cmd_validate(args):
|
|
|
113
124
|
warns = scan_unsupported(detection) if isinstance(detection, dict) else []
|
|
114
125
|
total_errors += len(errors)
|
|
115
126
|
total_warnings += len(warns)
|
|
116
|
-
|
|
127
|
+
marker = _c("x", _RED) if errors else _c("!", _YELLOW) if warns else _c(".", _GREEN)
|
|
128
|
+
print(f" {marker} {rule_path}")
|
|
117
129
|
for e in errors:
|
|
118
130
|
print(f" - error: {e}")
|
|
119
131
|
for w in warns:
|
|
@@ -204,6 +216,7 @@ def cmd_generate(args):
|
|
|
204
216
|
|
|
205
217
|
def main(argv=None):
|
|
206
218
|
parser = argparse.ArgumentParser(prog="detkit", description="Test Sigma detection rules as code.")
|
|
219
|
+
parser.add_argument("--version", action="version", version=f"detkit {__version__}")
|
|
207
220
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
208
221
|
|
|
209
222
|
p_test = sub.add_parser("test", help="run rule tests against sample events")
|
|
@@ -4,9 +4,10 @@ This is the core of detkit: unit-testing detection rules locally, the way
|
|
|
4
4
|
dbt tests data models. It implements the common Sigma subset.
|
|
5
5
|
|
|
6
6
|
# ponytail: common-subset evaluator (modifiers contains/startswith/endswith/re/cased/all/cidr,
|
|
7
|
-
# value wildcards * and ?,
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# value wildcards * and ?, dotted field paths a.b.c into nested events, list-OR,
|
|
8
|
+
# `N of them | N of prefix*`, and/or/not/parens). Ceilings: no |base64/|windash modifiers,
|
|
9
|
+
# no count()/timeframe aggregation, no descent into arrays of objects, matching is
|
|
10
|
+
# case-insensitive by default (real SIEM backends differ). Anything unsupported is
|
|
10
11
|
# reported by scan_unsupported() so it fails loud instead of returning a wrong boolean. Upgrade
|
|
11
12
|
# path: delegate to pySigma pipelines or a per-backend evaluator when a rule needs the full spec.
|
|
12
13
|
"""
|
|
@@ -48,15 +49,31 @@ def _matches_map(spec: dict, event: dict) -> bool:
|
|
|
48
49
|
def _matches_field(key: str, expected, event: dict) -> bool:
|
|
49
50
|
parts = key.split("|")
|
|
50
51
|
field, mods = parts[0], [m.lower() for m in parts[1:]]
|
|
51
|
-
|
|
52
|
+
found, ev = _resolve_field(field, event)
|
|
53
|
+
if not found:
|
|
52
54
|
return False
|
|
53
|
-
ev = event[field]
|
|
54
55
|
if isinstance(expected, list):
|
|
55
56
|
results = [_match_scalar(ev, x, mods) for x in expected]
|
|
56
57
|
return all(results) if "all" in mods else any(results)
|
|
57
58
|
return _match_scalar(ev, expected, mods)
|
|
58
59
|
|
|
59
60
|
|
|
61
|
+
def _resolve_field(field: str, event):
|
|
62
|
+
# Exact key first — covers flattened logs that use a literal dotted key
|
|
63
|
+
# (e.g. Elastic/ECS "gcp.audit.method_name"). Otherwise walk a.b.c into nested dicts.
|
|
64
|
+
if isinstance(event, dict) and field in event:
|
|
65
|
+
return True, event[field]
|
|
66
|
+
if "." in field and isinstance(event, dict):
|
|
67
|
+
cur = event
|
|
68
|
+
for part in field.split("."):
|
|
69
|
+
if isinstance(cur, dict) and part in cur:
|
|
70
|
+
cur = cur[part]
|
|
71
|
+
else:
|
|
72
|
+
return False, None
|
|
73
|
+
return True, cur
|
|
74
|
+
return False, None
|
|
75
|
+
|
|
76
|
+
|
|
60
77
|
def _match_scalar(ev, expected, mods) -> bool:
|
|
61
78
|
cased = "cased" in mods
|
|
62
79
|
if "cidr" in mods:
|
|
@@ -123,9 +140,6 @@ def scan_unsupported(detection: dict) -> list:
|
|
|
123
140
|
if mod not in _SUPPORTED_MODS and ("m", mod) not in seen:
|
|
124
141
|
seen.add(("m", mod))
|
|
125
142
|
findings.append(f"unsupported modifier '|{mod}' (on '{field}')")
|
|
126
|
-
if "." in field and ("f", field) not in seen:
|
|
127
|
-
seen.add(("f", field))
|
|
128
|
-
findings.append(f"nested/dotted field '{field}' (matched only as a flat key)")
|
|
129
143
|
|
|
130
144
|
def walk(spec):
|
|
131
145
|
if isinstance(spec, dict):
|
|
Binary file
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# detkit demo — generate the GIF with: vhs detkit-demo.tape
|
|
2
|
+
# Prereqs: `go install github.com/charmbracelet/vhs@latest` (or brew install vhs),
|
|
3
|
+
# run from the detkit repo root, and `pip install -e .` first.
|
|
4
|
+
# Uses GNU sed (Linux/WSL). On macOS, change `sed -i` to `sed -i ''`.
|
|
5
|
+
|
|
6
|
+
Output detkit-demo.gif
|
|
7
|
+
|
|
8
|
+
Require detkit
|
|
9
|
+
|
|
10
|
+
Set Shell bash
|
|
11
|
+
Set Theme "Dracula"
|
|
12
|
+
Set FontSize 18
|
|
13
|
+
Set Width 1200
|
|
14
|
+
Set Height 680
|
|
15
|
+
Set Padding 24
|
|
16
|
+
Set TypingSpeed 45ms
|
|
17
|
+
|
|
18
|
+
Hide
|
|
19
|
+
Type "clear"
|
|
20
|
+
Enter
|
|
21
|
+
Show
|
|
22
|
+
|
|
23
|
+
Type "# A Sigma rule ships with sample events saying what it should catch:"
|
|
24
|
+
Enter
|
|
25
|
+
Sleep 1s
|
|
26
|
+
Type "detkit test examples/rules"
|
|
27
|
+
Enter
|
|
28
|
+
Sleep 3s
|
|
29
|
+
|
|
30
|
+
Type "# A teammate 'tidies up' the rule and quietly breaks the detection..."
|
|
31
|
+
Enter
|
|
32
|
+
Sleep 1s
|
|
33
|
+
Type "sed -i 's/whoami/whoamiXYZ/' examples/rules/whoami_execution.yml"
|
|
34
|
+
Enter
|
|
35
|
+
Sleep 1200ms
|
|
36
|
+
|
|
37
|
+
Type "# detkit catches it. Non-zero exit means CI blocks the pull request:"
|
|
38
|
+
Enter
|
|
39
|
+
Sleep 800ms
|
|
40
|
+
Type "detkit test examples/rules; echo exit=$?"
|
|
41
|
+
Enter
|
|
42
|
+
Sleep 4s
|
|
43
|
+
|
|
44
|
+
Type "# Revert -> green again."
|
|
45
|
+
Enter
|
|
46
|
+
Sleep 800ms
|
|
47
|
+
Type "sed -i 's/whoamiXYZ/whoami/' examples/rules/whoami_execution.yml"
|
|
48
|
+
Enter
|
|
49
|
+
Type "detkit test examples/rules; echo exit=$?"
|
|
50
|
+
Enter
|
|
51
|
+
Sleep 3500ms
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: detkit-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: dbt for detections — test, validate, and CI-gate Sigma detection rules as code.
|
|
5
5
|
Author: detkit
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/ELSATOAH/detkit
|
|
8
8
|
Project-URL: Repository, https://github.com/ELSATOAH/detkit
|
|
9
9
|
Project-URL: Issues, https://github.com/ELSATOAH/detkit/issues
|
|
@@ -11,7 +11,6 @@ Keywords: sigma,detection-engineering,detection-as-code,siem,security,detections
|
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Information Technology
|
|
13
13
|
Classifier: Topic :: Security
|
|
14
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
15
14
|
Classifier: Programming Language :: Python :: 3
|
|
16
15
|
Classifier: Operating System :: OS Independent
|
|
17
16
|
Requires-Python: >=3.9
|
|
@@ -22,7 +21,7 @@ Dynamic: license-file
|
|
|
22
21
|
|
|
23
22
|
# detkit
|
|
24
23
|
|
|
25
|
-
[](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml) [](https://pypi.org/project/detkit-cli/) [](https://github.com/ELSATOAH/detkit/blob/main/LICENSE)
|
|
24
|
+
[](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml) [](https://pypi.org/project/detkit-cli/) [](https://pypi.org/project/detkit-cli/) [](https://github.com/ELSATOAH/detkit/blob/main/LICENSE)
|
|
26
25
|
|
|
27
26
|
Unit tests for your Sigma detection rules. Write a rule, add a couple of example log events, and detkit tells you whether it fires on the ones that should trip it and stays quiet on the ones that shouldn't. Wire it into CI and a broken detection fails the pull request instead of failing silently in production.
|
|
28
27
|
|
|
@@ -90,6 +89,22 @@ jobs:
|
|
|
90
89
|
path: rules
|
|
91
90
|
```
|
|
92
91
|
|
|
92
|
+
## pre-commit
|
|
93
|
+
|
|
94
|
+
Prefer to catch it before the commit lands? Add detkit to your [pre-commit](https://pre-commit.com) config:
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
# .pre-commit-config.yaml
|
|
98
|
+
repos:
|
|
99
|
+
- repo: https://github.com/ELSATOAH/detkit
|
|
100
|
+
rev: v0.1.3
|
|
101
|
+
hooks:
|
|
102
|
+
- id: detkit-test
|
|
103
|
+
args: [rules] # path to your rules
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
There's a `detkit-validate` hook too, for the lighter structural check.
|
|
107
|
+
|
|
93
108
|
## What it handles
|
|
94
109
|
|
|
95
110
|
detkit runs your rules locally. No SIEM, no credentials, nothing leaves your machine or CI runner. I ran it against every rule in the SigmaHQ repo, and about 91% use only features it understands today: `contains`, `startswith`, `endswith`, `re`, value wildcards (`*` and `?`), `|cidr`, keyword lists, and `X of` / `all of` conditions.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
.gitignore
|
|
2
|
+
.pre-commit-hooks.yaml
|
|
3
|
+
CONTRIBUTING.md
|
|
4
|
+
LICENSE
|
|
5
|
+
README.md
|
|
6
|
+
action.yml
|
|
7
|
+
detkit-demo.gif
|
|
8
|
+
detkit-demo.tape
|
|
9
|
+
pyproject.toml
|
|
10
|
+
.github/ISSUE_TEMPLATE/bug_report.md
|
|
11
|
+
.github/ISSUE_TEMPLATE/feature_request.md
|
|
12
|
+
.github/workflows/ci.yml
|
|
13
|
+
.github/workflows/publish.yml
|
|
14
|
+
detkit/__init__.py
|
|
15
|
+
detkit/__main__.py
|
|
16
|
+
detkit/cli.py
|
|
17
|
+
detkit/evaluator.py
|
|
18
|
+
detkit_cli.egg-info/PKG-INFO
|
|
19
|
+
detkit_cli.egg-info/SOURCES.txt
|
|
20
|
+
detkit_cli.egg-info/dependency_links.txt
|
|
21
|
+
detkit_cli.egg-info/entry_points.txt
|
|
22
|
+
detkit_cli.egg-info/requires.txt
|
|
23
|
+
detkit_cli.egg-info/scm_file_list.json
|
|
24
|
+
detkit_cli.egg-info/scm_version.json
|
|
25
|
+
detkit_cli.egg-info/top_level.txt
|
|
26
|
+
examples/rules/whoami_execution.test.yml
|
|
27
|
+
examples/rules/whoami_execution.yml
|
|
28
|
+
tests/test_evaluator.py
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
".gitignore",
|
|
4
|
+
"detkit-demo.tape",
|
|
5
|
+
"README.md",
|
|
6
|
+
"action.yml",
|
|
7
|
+
"CONTRIBUTING.md",
|
|
8
|
+
"pyproject.toml",
|
|
9
|
+
"detkit-demo.gif",
|
|
10
|
+
"LICENSE",
|
|
11
|
+
".pre-commit-hooks.yaml",
|
|
12
|
+
".github/workflows/publish.yml",
|
|
13
|
+
".github/workflows/ci.yml",
|
|
14
|
+
".github/ISSUE_TEMPLATE/bug_report.md",
|
|
15
|
+
".github/ISSUE_TEMPLATE/feature_request.md",
|
|
16
|
+
"examples/rules/whoami_execution.yml",
|
|
17
|
+
"examples/rules/whoami_execution.test.yml",
|
|
18
|
+
"detkit/evaluator.py",
|
|
19
|
+
"detkit/__main__.py",
|
|
20
|
+
"detkit/cli.py",
|
|
21
|
+
"detkit/__init__.py",
|
|
22
|
+
"tests/test_evaluator.py"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Unit tests for whoami_execution.yml
|
|
2
|
+
# every detection ships with sample events proving it fires (and doesn't).
|
|
3
|
+
tests:
|
|
4
|
+
- name: fires on whoami run by a normal user
|
|
5
|
+
event:
|
|
6
|
+
EventID: 4688
|
|
7
|
+
CommandLine: "cmd /c whoami /all"
|
|
8
|
+
User: "alice"
|
|
9
|
+
expect: match
|
|
10
|
+
|
|
11
|
+
- name: suppressed for SYSTEM (the filter)
|
|
12
|
+
event:
|
|
13
|
+
EventID: 4688
|
|
14
|
+
CommandLine: "whoami"
|
|
15
|
+
User: "SYSTEM"
|
|
16
|
+
expect: no_match
|
|
17
|
+
|
|
18
|
+
- name: no match on an unrelated command
|
|
19
|
+
event:
|
|
20
|
+
EventID: 4688
|
|
21
|
+
CommandLine: "ipconfig /all"
|
|
22
|
+
User: "alice"
|
|
23
|
+
expect: no_match
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
title: Whoami Execution
|
|
2
|
+
id: 6f0c3f2e-2a5e-4a1e-9b3a-0d5f1a2b3c4d
|
|
3
|
+
status: experimental
|
|
4
|
+
description: Detects execution of whoami.exe, common in post-exploitation recon.
|
|
5
|
+
logsource:
|
|
6
|
+
category: process_creation
|
|
7
|
+
product: windows
|
|
8
|
+
detection:
|
|
9
|
+
selection:
|
|
10
|
+
EventID: 4688
|
|
11
|
+
CommandLine|contains: 'whoami'
|
|
12
|
+
filter:
|
|
13
|
+
User: 'SYSTEM'
|
|
14
|
+
condition: selection and not filter
|
|
15
|
+
level: low
|
|
@@ -1,36 +1,37 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "detkit-cli"
|
|
3
|
-
|
|
4
|
-
description = "dbt for detections — test, validate, and CI-gate Sigma detection rules as code."
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.9"
|
|
7
|
-
license =
|
|
8
|
-
authors = [{ name = "detkit" }]
|
|
9
|
-
keywords = [
|
|
10
|
-
"sigma", "detection-engineering", "detection-as-code",
|
|
11
|
-
"siem", "security", "detections", "blue-team", "cybersecurity",
|
|
12
|
-
]
|
|
13
|
-
classifiers = [
|
|
14
|
-
"Development Status :: 3 - Alpha",
|
|
15
|
-
"Intended Audience :: Information Technology",
|
|
16
|
-
"Topic :: Security",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
[
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
[
|
|
36
|
-
|
|
1
|
+
[project]
|
|
2
|
+
name = "detkit-cli"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "dbt for detections — test, validate, and CI-gate Sigma detection rules as code."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "detkit" }]
|
|
9
|
+
keywords = [
|
|
10
|
+
"sigma", "detection-engineering", "detection-as-code",
|
|
11
|
+
"siem", "security", "detections", "blue-team", "cybersecurity",
|
|
12
|
+
]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Information Technology",
|
|
16
|
+
"Topic :: Security",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
dependencies = ["pyyaml>=6"]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/ELSATOAH/detkit"
|
|
24
|
+
Repository = "https://github.com/ELSATOAH/detkit"
|
|
25
|
+
Issues = "https://github.com/ELSATOAH/detkit/issues"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
detkit = "detkit.cli:main"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["setuptools>=77", "setuptools-scm>=8"]
|
|
32
|
+
build-backend = "setuptools.build_meta"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
include = ["detkit*"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools_scm]
|
|
@@ -92,16 +92,29 @@ def test_cidr_modifier():
|
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
def test_scan_flags_unsupported():
|
|
95
|
-
det = {
|
|
96
|
-
"sel": {"CommandLine|base64offset|contains": "abc", "id.orig_h": "1.2.3.4"},
|
|
97
|
-
"condition": "sel",
|
|
98
|
-
}
|
|
95
|
+
det = {"sel": {"CommandLine|base64offset|contains": "abc"}, "condition": "sel"}
|
|
99
96
|
findings = scan_unsupported(det)
|
|
100
97
|
assert any("base64offset" in f for f in findings)
|
|
101
|
-
|
|
98
|
+
# nested/dotted fields are supported now, so they must NOT be flagged
|
|
99
|
+
assert scan_unsupported({"sel": {"id.orig_h": "1.2.3.4"}, "condition": "sel"}) == []
|
|
102
100
|
assert scan_unsupported({"sel": {"EventID": 1}, "condition": "sel"}) == []
|
|
103
101
|
|
|
104
102
|
|
|
103
|
+
def test_nested_field_access():
|
|
104
|
+
det = {"sel": {"DeviceDetail.deviceId": "abc123"}, "condition": "sel"}
|
|
105
|
+
assert event_matches_rule(det, {"DeviceDetail": {"deviceId": "abc123"}}) # nested dict
|
|
106
|
+
assert event_matches_rule(det, {"DeviceDetail.deviceId": "abc123"}) # flattened key
|
|
107
|
+
assert not event_matches_rule(det, {"DeviceDetail": {"deviceId": "other"}})
|
|
108
|
+
assert not event_matches_rule(det, {"DeviceDetail": {}})
|
|
109
|
+
assert not event_matches_rule(det, {"EventID": 1})
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_nested_field_with_modifier():
|
|
113
|
+
det = {"sel": {"gcp.audit.method_name|endswith": ".delete"}, "condition": "sel"}
|
|
114
|
+
assert event_matches_rule(det, {"gcp": {"audit": {"method_name": "v1.compute.firewalls.delete"}}})
|
|
115
|
+
assert not event_matches_rule(det, {"gcp": {"audit": {"method_name": "v1.compute.firewalls.list"}}})
|
|
116
|
+
|
|
117
|
+
|
|
105
118
|
if __name__ == "__main__":
|
|
106
119
|
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
|
|
107
120
|
for fn in fns:
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
LICENSE
|
|
2
|
-
README.md
|
|
3
|
-
pyproject.toml
|
|
4
|
-
detkit/__init__.py
|
|
5
|
-
detkit/__main__.py
|
|
6
|
-
detkit/cli.py
|
|
7
|
-
detkit/evaluator.py
|
|
8
|
-
detkit_cli.egg-info/PKG-INFO
|
|
9
|
-
detkit_cli.egg-info/SOURCES.txt
|
|
10
|
-
detkit_cli.egg-info/dependency_links.txt
|
|
11
|
-
detkit_cli.egg-info/entry_points.txt
|
|
12
|
-
detkit_cli.egg-info/requires.txt
|
|
13
|
-
detkit_cli.egg-info/top_level.txt
|
|
14
|
-
tests/test_evaluator.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|