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.
Files changed (32) hide show
  1. detkit_cli-0.1.4/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
  2. detkit_cli-0.1.4/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
  3. detkit_cli-0.1.4/.github/workflows/ci.yml +21 -0
  4. detkit_cli-0.1.4/.github/workflows/publish.yml +34 -0
  5. detkit_cli-0.1.4/.gitignore +9 -0
  6. detkit_cli-0.1.4/.pre-commit-hooks.yaml +26 -0
  7. detkit_cli-0.1.4/CONTRIBUTING.md +44 -0
  8. {detkit_cli-0.1.1 → detkit_cli-0.1.4}/PKG-INFO +19 -4
  9. {detkit_cli-0.1.1 → detkit_cli-0.1.4}/README.md +17 -1
  10. detkit_cli-0.1.4/action.yml +27 -0
  11. detkit_cli-0.1.4/detkit/__init__.py +6 -0
  12. {detkit_cli-0.1.1 → detkit_cli-0.1.4}/detkit/cli.py +20 -7
  13. {detkit_cli-0.1.1 → detkit_cli-0.1.4}/detkit/evaluator.py +22 -8
  14. detkit_cli-0.1.4/detkit-demo.gif +0 -0
  15. detkit_cli-0.1.4/detkit-demo.tape +51 -0
  16. {detkit_cli-0.1.1 → detkit_cli-0.1.4}/detkit_cli.egg-info/PKG-INFO +19 -4
  17. detkit_cli-0.1.4/detkit_cli.egg-info/SOURCES.txt +28 -0
  18. detkit_cli-0.1.4/detkit_cli.egg-info/scm_file_list.json +24 -0
  19. detkit_cli-0.1.4/detkit_cli.egg-info/scm_version.json +8 -0
  20. detkit_cli-0.1.4/examples/rules/whoami_execution.test.yml +23 -0
  21. detkit_cli-0.1.4/examples/rules/whoami_execution.yml +15 -0
  22. {detkit_cli-0.1.1 → detkit_cli-0.1.4}/pyproject.toml +37 -36
  23. {detkit_cli-0.1.1 → detkit_cli-0.1.4}/tests/test_evaluator.py +18 -5
  24. detkit_cli-0.1.1/detkit/__init__.py +0 -1
  25. detkit_cli-0.1.1/detkit_cli.egg-info/SOURCES.txt +0 -14
  26. {detkit_cli-0.1.1 → detkit_cli-0.1.4}/LICENSE +0 -0
  27. {detkit_cli-0.1.1 → detkit_cli-0.1.4}/detkit/__main__.py +0 -0
  28. {detkit_cli-0.1.1 → detkit_cli-0.1.4}/detkit_cli.egg-info/dependency_links.txt +0 -0
  29. {detkit_cli-0.1.1 → detkit_cli-0.1.4}/detkit_cli.egg-info/entry_points.txt +0 -0
  30. {detkit_cli-0.1.1 → detkit_cli-0.1.4}/detkit_cli.egg-info/requires.txt +0 -0
  31. {detkit_cli-0.1.1 → detkit_cli-0.1.4}/detkit_cli.egg-info/top_level.txt +0 -0
  32. {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,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ venv/
8
+ .pytest_cache/
9
+ .eggs/
@@ -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.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
- [![CI](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml/badge.svg)](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/detkit-cli)](https://pypi.org/project/detkit-cli/) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/ELSATOAH/detkit/blob/main/LICENSE)
24
+ [![CI](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml/badge.svg)](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/detkit-cli)](https://pypi.org/project/detkit-cli/) [![Python](https://img.shields.io/pypi/pyversions/detkit-cli)](https://pypi.org/project/detkit-cli/) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
- [![CI](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml/badge.svg)](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/detkit-cli)](https://pypi.org/project/detkit-cli/) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/ELSATOAH/detkit/blob/main/LICENSE)
3
+ [![CI](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml/badge.svg)](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/detkit-cli)](https://pypi.org/project/detkit-cli/) [![Python](https://img.shields.io/pypi/pyversions/detkit-cli)](https://pypi.org/project/detkit-cli/) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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 }}"
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("detkit-cli")
5
+ except PackageNotFoundError:
6
+ __version__ = "0+unknown"
@@ -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
- print(f"\n{passed} passed, {failed} failed")
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
- print(f" {'x' if errors else '!' if warns else '.'} {rule_path}")
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 ?, list-OR, `N of them | N of prefix*`, and/or/not/parens). Ceilings:
8
- # no nested-field (dot) access, no |base64/|windash modifiers, no count()/timeframe aggregation,
9
- # matching is case-insensitive by default (real SIEM backends differ). Anything unsupported is
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
- if field not in event:
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.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
- [![CI](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml/badge.svg)](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/detkit-cli)](https://pypi.org/project/detkit-cli/) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/ELSATOAH/detkit/blob/main/LICENSE)
24
+ [![CI](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml/badge.svg)](https://github.com/ELSATOAH/detkit/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/detkit-cli)](https://pypi.org/project/detkit-cli/) [![Python](https://img.shields.io/pypi/pyversions/detkit-cli)](https://pypi.org/project/detkit-cli/) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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,8 @@
1
+ {
2
+ "tag": "0.1.4",
3
+ "distance": 0,
4
+ "node": "g80ef32737be8e15897b06afcf76eeb397325fcb4",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-20"
8
+ }
@@ -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
- version = "0.1.1"
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 = { text = "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
- "License :: OSI Approved :: MIT License",
18
- "Programming Language :: Python :: 3",
19
- "Operating System :: OS Independent",
20
- ]
21
- dependencies = ["pyyaml>=6"]
22
-
23
- [project.urls]
24
- Homepage = "https://github.com/ELSATOAH/detkit"
25
- Repository = "https://github.com/ELSATOAH/detkit"
26
- Issues = "https://github.com/ELSATOAH/detkit/issues"
27
-
28
- [project.scripts]
29
- detkit = "detkit.cli:main"
30
-
31
- [build-system]
32
- requires = ["setuptools>=61"]
33
- build-backend = "setuptools.build_meta"
34
-
35
- [tool.setuptools.packages.find]
36
- include = ["detkit*"]
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
- assert any("id.orig_h" in f for f in findings)
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