detkit-cli 0.1.5__tar.gz → 0.1.6__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 (37) hide show
  1. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/PKG-INFO +21 -4
  2. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/README.md +20 -3
  3. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/detkit/cli.py +22 -0
  4. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/detkit/docs.py +48 -0
  5. detkit_cli-0.1.6/detkit-navigator.json +50 -0
  6. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/detkit_cli.egg-info/PKG-INFO +21 -4
  7. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/detkit_cli.egg-info/SOURCES.txt +1 -0
  8. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/detkit_cli.egg-info/scm_file_list.json +1 -0
  9. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/detkit_cli.egg-info/scm_version.json +2 -2
  10. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/tests/test_docs.py +20 -1
  11. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  12. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  13. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/.github/workflows/ci.yml +0 -0
  14. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/.github/workflows/publish.yml +0 -0
  15. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/.gitignore +0 -0
  16. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/.pre-commit-hooks.yaml +0 -0
  17. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/CONTRIBUTING.md +0 -0
  18. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/LICENSE +0 -0
  19. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/action.yml +0 -0
  20. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/detkit/__init__.py +0 -0
  21. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/detkit/__main__.py +0 -0
  22. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/detkit/attack_enterprise.json +0 -0
  23. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/detkit/evaluator.py +0 -0
  24. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/detkit-demo.gif +0 -0
  25. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/detkit-demo.tape +0 -0
  26. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/detkit_cli.egg-info/dependency_links.txt +0 -0
  27. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/detkit_cli.egg-info/entry_points.txt +0 -0
  28. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/detkit_cli.egg-info/requires.txt +0 -0
  29. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/detkit_cli.egg-info/top_level.txt +0 -0
  30. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/examples/rules/powershell_encoded_command.test.yml +0 -0
  31. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/examples/rules/powershell_encoded_command.yml +0 -0
  32. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/examples/rules/whoami_execution.test.yml +0 -0
  33. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/examples/rules/whoami_execution.yml +0 -0
  34. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/pyproject.toml +0 -0
  35. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/setup.cfg +0 -0
  36. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/tests/test_cli_json.py +0 -0
  37. {detkit_cli-0.1.5 → detkit_cli-0.1.6}/tests/test_evaluator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detkit-cli
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: dbt for detections — test, validate, and CI-gate Sigma detection rules as code.
5
5
  Author: detkit
6
6
  License-Expression: MIT
@@ -105,11 +105,28 @@ repos:
105
105
 
106
106
  There's a `detkit-validate` hook too, for the lighter structural check.
107
107
 
108
+ ## Coverage
109
+
110
+ Past a handful of rules you want to see what you're *not* covering. `detkit docs` builds a single self-contained HTML page: a catalog of every rule plus a MITRE ATT&CK heatmap of which techniques you detect, which you test, and which are gaps.
111
+
112
+ ```bash
113
+ detkit docs rules -o coverage.html
114
+ ```
115
+
116
+ Green is a technique with a tested rule, amber is a rule with no test, and the faint cells are the gaps. It's one file with no external assets, so you can commit it or publish it to GitHub Pages.
117
+
118
+ `detkit navigator` exports the same coverage as a [MITRE ATT&CK Navigator](https://mitre-attack.github.io/attack-navigator/) layer, so it drops straight into the tool your team already uses:
119
+
120
+ ```bash
121
+ detkit navigator rules -o coverage.json
122
+ # then Open Existing Layer -> Upload at the Navigator
123
+ ```
124
+
108
125
  ## What it handles
109
126
 
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.
127
+ 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`, nested/dotted fields (`DeviceDetail.deviceId`), keyword lists, and `X of` / `all of` conditions.
111
128
 
112
- It won't guess at the rest. If a rule uses something detkit can't evaluate yet, like nested fields (`DeviceDetail.deviceId`) or `base64` modifiers, `test` and `validate` print a WARN naming the feature instead of returning an answer that might be wrong. A detection tool that's quietly wrong is worse than no tool.
129
+ It won't guess at the rest. If a rule uses something detkit can't evaluate yet, like `base64`/`windash` modifiers or arrays of objects, `test` and `validate` print a WARN naming the feature instead of returning an answer that might be wrong. A detection tool that's quietly wrong is worse than no tool.
113
130
 
114
131
  ## Why
115
132
 
@@ -117,7 +134,7 @@ Detections live in Git now, but the testing habit that comes with the rest of so
117
134
 
118
135
  ## Roadmap
119
136
 
120
- - Nested/dotted fields and `base64` modifiers (the two things it warns on today), probably via [pySigma](https://github.com/SigmaHQ/pySigma).
137
+ - `base64`/`windash` modifiers and arrays of objects (what it warns on today), probably via [pySigma](https://github.com/SigmaHQ/pySigma).
121
138
  - `detkit generate`: draft a rule and its tests from a plain-English description. The tests come with it, not as an afterthought.
122
139
  - Field mapping, so one rule can be checked against more than one log schema.
123
140
  - A hosted option down the line for teams that want shared runs and history. The CLI stays free.
@@ -84,11 +84,28 @@ repos:
84
84
 
85
85
  There's a `detkit-validate` hook too, for the lighter structural check.
86
86
 
87
+ ## Coverage
88
+
89
+ Past a handful of rules you want to see what you're *not* covering. `detkit docs` builds a single self-contained HTML page: a catalog of every rule plus a MITRE ATT&CK heatmap of which techniques you detect, which you test, and which are gaps.
90
+
91
+ ```bash
92
+ detkit docs rules -o coverage.html
93
+ ```
94
+
95
+ Green is a technique with a tested rule, amber is a rule with no test, and the faint cells are the gaps. It's one file with no external assets, so you can commit it or publish it to GitHub Pages.
96
+
97
+ `detkit navigator` exports the same coverage as a [MITRE ATT&CK Navigator](https://mitre-attack.github.io/attack-navigator/) layer, so it drops straight into the tool your team already uses:
98
+
99
+ ```bash
100
+ detkit navigator rules -o coverage.json
101
+ # then Open Existing Layer -> Upload at the Navigator
102
+ ```
103
+
87
104
  ## What it handles
88
105
 
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.
106
+ 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`, nested/dotted fields (`DeviceDetail.deviceId`), keyword lists, and `X of` / `all of` conditions.
90
107
 
91
- It won't guess at the rest. If a rule uses something detkit can't evaluate yet, like nested fields (`DeviceDetail.deviceId`) or `base64` modifiers, `test` and `validate` print a WARN naming the feature instead of returning an answer that might be wrong. A detection tool that's quietly wrong is worse than no tool.
108
+ It won't guess at the rest. If a rule uses something detkit can't evaluate yet, like `base64`/`windash` modifiers or arrays of objects, `test` and `validate` print a WARN naming the feature instead of returning an answer that might be wrong. A detection tool that's quietly wrong is worse than no tool.
92
109
 
93
110
  ## Why
94
111
 
@@ -96,7 +113,7 @@ Detections live in Git now, but the testing habit that comes with the rest of so
96
113
 
97
114
  ## Roadmap
98
115
 
99
- - Nested/dotted fields and `base64` modifiers (the two things it warns on today), probably via [pySigma](https://github.com/SigmaHQ/pySigma).
116
+ - `base64`/`windash` modifiers and arrays of objects (what it warns on today), probably via [pySigma](https://github.com/SigmaHQ/pySigma).
100
117
  - `detkit generate`: draft a rule and its tests from a plain-English description. The tests come with it, not as an afterthought.
101
118
  - Field mapping, so one rule can be checked against more than one log schema.
102
119
  - A hosted option down the line for teams that want shared runs and history. The CLI stays free.
@@ -272,6 +272,23 @@ def cmd_docs(args):
272
272
  return 0
273
273
 
274
274
 
275
+ def cmd_navigator(args):
276
+ rules = _discover_rules(args.path)
277
+ if not rules:
278
+ print(f"no rules found under {args.path!r}")
279
+ return 1
280
+ items = [
281
+ {"path": rp, "rule": _load_yaml(rp) or {}, "has_tests": _test_file_for(rp) is not None}
282
+ for rp in rules
283
+ ]
284
+ layer = docs.navigator_layer(items, _load_attack_data())
285
+ with open(args.output, "w", encoding="utf-8") as f:
286
+ json.dump(layer, f, indent=2)
287
+ print(f"exported {len(layer['techniques'])} techniques -> {args.output}")
288
+ print("import it at https://mitre-attack.github.io/attack-navigator/ (Open Existing Layer -> Upload)")
289
+ return 0
290
+
291
+
275
292
  def cmd_generate(args):
276
293
  print(
277
294
  "`detkit generate` is not implemented yet.\n"
@@ -307,6 +324,11 @@ def main(argv=None):
307
324
  p_docs.add_argument("-o", "--output", default="detkit-docs.html")
308
325
  p_docs.set_defaults(func=cmd_docs)
309
326
 
327
+ p_nav = sub.add_parser("navigator", help="export coverage as a MITRE ATT&CK Navigator layer (JSON)")
328
+ p_nav.add_argument("path", nargs="?", default=".")
329
+ p_nav.add_argument("-o", "--output", default="detkit-navigator.json")
330
+ p_nav.set_defaults(func=cmd_navigator)
331
+
310
332
  p_gen = sub.add_parser("generate", help="(planned) AI-draft a rule + tests")
311
333
  p_gen.set_defaults(func=cmd_generate)
312
334
 
@@ -124,6 +124,54 @@ def render(items, attack_data=None):
124
124
  return _page(rules, covered, other, full, names, matrix_on, summary), summary
125
125
 
126
126
 
127
+ def _covered_by_technique(items):
128
+ """{parent technique id: tested?} across all rules — folds sub-techniques to parent."""
129
+ out = {}
130
+ for it in items:
131
+ r = _extract(it)
132
+ for tech in r["techniques"]:
133
+ parent = tech.split(".")[0]
134
+ out[parent] = out.get(parent, False) or r["has_tests"]
135
+ return out
136
+
137
+
138
+ def navigator_layer(items, attack_data=None, name="detkit coverage"):
139
+ """Build a MITRE ATT&CK Navigator layer (dict) from rule coverage.
140
+
141
+ score 100 = has detkit tests (green), 50 = covered but untested (amber);
142
+ gaps are left out so Navigator shows them uncolored. Import at
143
+ https://mitre-attack.github.io/attack-navigator/.
144
+ """
145
+ covered = _covered_by_technique(items)
146
+ full, _ = _matrix(attack_data)
147
+ universe = {t for techs in full.values() for t in techs}
148
+ version = str((attack_data or {}).get("attack_version", "")).lstrip("vV") or "17"
149
+ techniques = [
150
+ {
151
+ "techniqueID": tech,
152
+ "score": 100 if tested else 50,
153
+ "enabled": True,
154
+ "comment": "tested" if tested else "covered (no detkit test)",
155
+ }
156
+ for tech, tested in sorted(covered.items())
157
+ if not universe or tech in universe
158
+ ]
159
+ return {
160
+ "name": name,
161
+ "domain": "enterprise-attack",
162
+ "description": "detkit detection coverage. score 100 = has detkit tests, 50 = covered but untested.",
163
+ "versions": {"attack": version, "navigator": "5.1.0", "layer": "4.5"},
164
+ "sorting": 0,
165
+ "hideDisabled": False,
166
+ "techniques": techniques,
167
+ "gradient": {"colors": ["#fff8c5", "#8ec843"], "minValue": 50, "maxValue": 100},
168
+ "legendItems": [
169
+ {"label": "covered, no test", "color": "#fff8c5"},
170
+ {"label": "tested", "color": "#8ec843"},
171
+ ],
172
+ }
173
+
174
+
127
175
  def _cells(cov, official, names):
128
176
  """cov: {tech: tested?}. official: {tech: name} (full matrix) or None."""
129
177
  if official is None: # v1: only covered techniques
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "detkit coverage",
3
+ "domain": "enterprise-attack",
4
+ "description": "detkit detection coverage. score 100 = has detkit tests, 50 = covered but untested.",
5
+ "versions": {
6
+ "attack": "19",
7
+ "navigator": "5.1.0",
8
+ "layer": "4.5"
9
+ },
10
+ "sorting": 0,
11
+ "hideDisabled": false,
12
+ "techniques": [
13
+ {
14
+ "techniqueID": "T1027",
15
+ "score": 100,
16
+ "enabled": true,
17
+ "comment": "tested"
18
+ },
19
+ {
20
+ "techniqueID": "T1033",
21
+ "score": 100,
22
+ "enabled": true,
23
+ "comment": "tested"
24
+ },
25
+ {
26
+ "techniqueID": "T1059",
27
+ "score": 100,
28
+ "enabled": true,
29
+ "comment": "tested"
30
+ }
31
+ ],
32
+ "gradient": {
33
+ "colors": [
34
+ "#fff8c5",
35
+ "#8ec843"
36
+ ],
37
+ "minValue": 50,
38
+ "maxValue": 100
39
+ },
40
+ "legendItems": [
41
+ {
42
+ "label": "covered, no test",
43
+ "color": "#fff8c5"
44
+ },
45
+ {
46
+ "label": "tested",
47
+ "color": "#8ec843"
48
+ }
49
+ ]
50
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detkit-cli
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: dbt for detections — test, validate, and CI-gate Sigma detection rules as code.
5
5
  Author: detkit
6
6
  License-Expression: MIT
@@ -105,11 +105,28 @@ repos:
105
105
 
106
106
  There's a `detkit-validate` hook too, for the lighter structural check.
107
107
 
108
+ ## Coverage
109
+
110
+ Past a handful of rules you want to see what you're *not* covering. `detkit docs` builds a single self-contained HTML page: a catalog of every rule plus a MITRE ATT&CK heatmap of which techniques you detect, which you test, and which are gaps.
111
+
112
+ ```bash
113
+ detkit docs rules -o coverage.html
114
+ ```
115
+
116
+ Green is a technique with a tested rule, amber is a rule with no test, and the faint cells are the gaps. It's one file with no external assets, so you can commit it or publish it to GitHub Pages.
117
+
118
+ `detkit navigator` exports the same coverage as a [MITRE ATT&CK Navigator](https://mitre-attack.github.io/attack-navigator/) layer, so it drops straight into the tool your team already uses:
119
+
120
+ ```bash
121
+ detkit navigator rules -o coverage.json
122
+ # then Open Existing Layer -> Upload at the Navigator
123
+ ```
124
+
108
125
  ## What it handles
109
126
 
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.
127
+ 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`, nested/dotted fields (`DeviceDetail.deviceId`), keyword lists, and `X of` / `all of` conditions.
111
128
 
112
- It won't guess at the rest. If a rule uses something detkit can't evaluate yet, like nested fields (`DeviceDetail.deviceId`) or `base64` modifiers, `test` and `validate` print a WARN naming the feature instead of returning an answer that might be wrong. A detection tool that's quietly wrong is worse than no tool.
129
+ It won't guess at the rest. If a rule uses something detkit can't evaluate yet, like `base64`/`windash` modifiers or arrays of objects, `test` and `validate` print a WARN naming the feature instead of returning an answer that might be wrong. A detection tool that's quietly wrong is worse than no tool.
113
130
 
114
131
  ## Why
115
132
 
@@ -117,7 +134,7 @@ Detections live in Git now, but the testing habit that comes with the rest of so
117
134
 
118
135
  ## Roadmap
119
136
 
120
- - Nested/dotted fields and `base64` modifiers (the two things it warns on today), probably via [pySigma](https://github.com/SigmaHQ/pySigma).
137
+ - `base64`/`windash` modifiers and arrays of objects (what it warns on today), probably via [pySigma](https://github.com/SigmaHQ/pySigma).
121
138
  - `detkit generate`: draft a rule and its tests from a plain-English description. The tests come with it, not as an afterthought.
122
139
  - Field mapping, so one rule can be checked against more than one log schema.
123
140
  - A hosted option down the line for teams that want shared runs and history. The CLI stays free.
@@ -6,6 +6,7 @@ README.md
6
6
  action.yml
7
7
  detkit-demo.gif
8
8
  detkit-demo.tape
9
+ detkit-navigator.json
9
10
  pyproject.toml
10
11
  .github/ISSUE_TEMPLATE/bug_report.md
11
12
  .github/ISSUE_TEMPLATE/feature_request.md
@@ -2,6 +2,7 @@
2
2
  "files": [
3
3
  ".gitignore",
4
4
  "detkit-demo.tape",
5
+ "detkit-navigator.json",
5
6
  "README.md",
6
7
  "action.yml",
7
8
  "CONTRIBUTING.md",
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "0.1.5",
2
+ "tag": "0.1.6",
3
3
  "distance": 0,
4
- "node": "g8ebd5d89c44f1c35a218bb1a008163aee32f87cd",
4
+ "node": "gefb596d8a8684369663c94b34680e262fc75fa3c",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-07-21"
@@ -4,7 +4,7 @@ import sys
4
4
 
5
5
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
6
6
 
7
- from detkit.docs import parse_attack, render # noqa: E402
7
+ from detkit.docs import navigator_layer, parse_attack, render # noqa: E402
8
8
 
9
9
 
10
10
  def test_parse_attack():
@@ -39,6 +39,25 @@ def test_render_empty():
39
39
  assert "<!doctype html>" in html_str.lower()
40
40
 
41
41
 
42
+ def test_navigator_layer():
43
+ items = [
44
+ {"path": "a.yml", "has_tests": True,
45
+ "rule": {"title": "A", "tags": ["attack.execution", "attack.t1059.001"]}},
46
+ {"path": "b.yml", "has_tests": False,
47
+ "rule": {"title": "B", "tags": ["attack.persistence", "attack.t1547"]}},
48
+ ]
49
+ attack = {"attack_version": "v19", "techniques": [
50
+ {"id": "T1059", "name": "x", "tactics": ["execution"]},
51
+ {"id": "T1547", "name": "y", "tactics": ["persistence"]},
52
+ ]}
53
+ layer = navigator_layer(items, attack)
54
+ assert layer["domain"] == "enterprise-attack"
55
+ assert layer["versions"]["attack"] == "19" # 'v19' -> '19'
56
+ by = {t["techniqueID"]: t for t in layer["techniques"]}
57
+ assert by["T1059"]["score"] == 100 # sub-technique folds to parent, tested
58
+ assert by["T1547"]["score"] == 50 # covered, no test
59
+
60
+
42
61
  if __name__ == "__main__":
43
62
  fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
44
63
  for fn in fns:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes