attack-mapper 0.4.0__py3-none-any.whl

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.
@@ -0,0 +1,128 @@
1
+ """Parse Sigma rules and extract referenced MITRE ATT&CK techniques.
2
+
3
+ Sigma rules are YAML files. ATT&CK technique IDs (e.g. ``T1059``) are commonly
4
+ referenced in a few places:
5
+
6
+ * ``tags:`` list, as ``attack.<id>`` (the Sigma convention) or raw ``T1059``.
7
+ * ``references:`` list of URLs containing ``attack.mitre.org/techniques/Txxxx``.
8
+ * Free text in the ``description:`` / ``logsource:`` (rare, optional).
9
+
10
+ This module supports both the canonical ``attack.*`` tag form and URL
11
+ extraction so it works with rules written by different teams.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import glob
17
+ import os
18
+ import re
19
+ from dataclasses import dataclass, field
20
+ from typing import Iterable
21
+
22
+ try:
23
+ import yaml
24
+ except ImportError as exc: # pragma: no cover - surfaced to the user clearly
25
+ raise SystemExit(
26
+ "PyYAML is required. Install it with: pip install pyyaml"
27
+ ) from exc
28
+
29
+
30
+ # Matches T<4 digits> optionally followed by .<2-3 digits> (sub-technique)
31
+ TECH_ID_RE = re.compile(r"\bT\d{4}(?:\.\d{1,3})?\b", re.IGNORECASE)
32
+ # MITRE URLs use slashes (attack.mitre.org/techniques/T1059/001), so accept
33
+ # both '.' and '/' as the sub-technique separator and normalize to '.'.
34
+ URL_RE = re.compile(r"attack\.mitre\.org/techniques/(T\d{4}(?:[./]\d{1,3})?)", re.IGNORECASE)
35
+
36
+
37
+ @dataclass
38
+ class ParsedRule:
39
+ """A Sigma rule reduced to the data we care about for coverage mapping."""
40
+
41
+ path: str
42
+ title: str | None = None
43
+ technique_ids: set[str] = field(default_factory=set)
44
+
45
+ @property
46
+ def techniques(self) -> list[str]:
47
+ return sorted(self.technique_ids, key=lambda t: (len(t), t))
48
+
49
+
50
+ def _extract_from_tags(tags: Iterable[str]) -> set[str]:
51
+ found = set()
52
+ for tag in tags or []:
53
+ if not isinstance(tag, str):
54
+ continue
55
+ # Canonical Sigma form: attack.t1059 / attack.t1059.001 (case-insensitive)
56
+ if tag.lower().startswith("attack."):
57
+ cand = tag.split(".", 1)[1]
58
+ m = TECH_ID_RE.search(cand)
59
+ if m:
60
+ found.add(m.group(0).upper())
61
+ else:
62
+ m = TECH_ID_RE.search(tag)
63
+ if m:
64
+ found.add(m.group(0).upper())
65
+ return found
66
+
67
+
68
+ def _extract_from_references(references: Iterable) -> set[str]:
69
+ found = set()
70
+ for ref in references or []:
71
+ url = ref.get("url") if isinstance(ref, dict) else str(ref)
72
+ if not url:
73
+ continue
74
+ for m in URL_RE.finditer(url):
75
+ # Normalize a slash separator (T1059/001) to a dot (T1059.001)
76
+ found.add(m.group(1).upper().replace("/", "."))
77
+ return found
78
+
79
+
80
+ def parse_rule_text(text: str, path: str = "<string>") -> ParsedRule:
81
+ """Parse a single Sigma rule from a YAML string."""
82
+ try:
83
+ doc = yaml.safe_load(text)
84
+ except yaml.YAMLError:
85
+ return ParsedRule(path=path, title=None, technique_ids=set())
86
+
87
+ if not isinstance(doc, dict):
88
+ return ParsedRule(path=path, title=None, technique_ids=set())
89
+
90
+ rule = ParsedRule(path=path, title=doc.get("title"))
91
+
92
+ found = set()
93
+ found |= _extract_from_tags(doc.get("tags", []))
94
+ found |= _extract_from_references(doc.get("references", []))
95
+ # Fallback: scan description for raw technique IDs
96
+ desc = doc.get("description") or ""
97
+ if isinstance(desc, str):
98
+ found |= {m.group(0).upper() for m in TECH_ID_RE.finditer(desc)}
99
+
100
+ rule.technique_ids = found
101
+ return rule
102
+
103
+
104
+ def parse_rule_file(path: str) -> ParsedRule:
105
+ """Parse a Sigma rule from a file on disk."""
106
+ with open(path, "r", encoding="utf-8") as fh:
107
+ return parse_rule_text(fh.read(), path=path)
108
+
109
+
110
+ def parse_rules(paths: Iterable[str]) -> list[ParsedRule]:
111
+ """Parse many rule files. ``paths`` may include glob patterns."""
112
+ resolved: list[str] = []
113
+ for p in paths:
114
+ if any(ch in p for ch in "*?[]"):
115
+ resolved.extend(sorted(glob.glob(p, recursive=True)))
116
+ elif os.path.isdir(p):
117
+ resolved.extend(sorted(glob.glob(os.path.join(p, "**", "*.yml"), recursive=True)))
118
+ resolved.extend(sorted(glob.glob(os.path.join(p, "**", "*.yaml"), recursive=True)))
119
+ elif os.path.isfile(p):
120
+ resolved.append(p)
121
+ # De-duplicate while preserving order
122
+ seen, rules = set(), []
123
+ for fp in resolved:
124
+ if fp in seen:
125
+ continue
126
+ seen.add(fp)
127
+ rules.append(parse_rule_file(fp))
128
+ return rules
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.4
2
+ Name: attack-mapper
3
+ Version: 0.4.0
4
+ Summary: Map your Sigma detection rules onto the MITRE ATT&CK matrix and find coverage gaps.
5
+ Author: Jose (JoseArgento)
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/JoseArgento/attack-mapper
8
+ Project-URL: Repository, https://github.com/JoseArgento/attack-mapper
9
+ Project-URL: Issues, https://github.com/JoseArgento/attack-mapper/issues
10
+ Keywords: mitre,att-ck,sigma,detection-engineering,soc,blue-team
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Information Technology
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Security
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: pyyaml>=6.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # attack-mapper 🎯
27
+
28
+ [![CI](https://github.com/JoseArgento/attack-mapper/actions/workflows/ci.yml/badge.svg)](https://github.com/JoseArgento/attack-mapper/actions/workflows/ci.yml)
29
+ [![ATT&CK dataset](https://github.com/JoseArgento/attack-mapper/actions/workflows/update-attack-db.yml/badge.svg)](https://github.com/JoseArgento/attack-mapper/actions/workflows/update-attack-db.yml)
30
+ [![PyPI](https://img.shields.io/pypi/v/attack-mapper)](https://pypi.org/project/attack-mapper/)
31
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
32
+
33
+ Map your **Sigma detection rules** onto the **MITRE ATT&CK** matrix and find your
34
+ coverage gaps — fast, offline, and CI-friendly.
35
+
36
+ Part of a detection-engineering toolkit that pairs perfectly with a
37
+ *Detection-as-Code* pipeline (Sigma → CI → SIEM). It answers the question every
38
+ blue team eventually asks: *"Which ATT&CK techniques do my rules actually
39
+ cover, and where are the blind spots?"*
40
+
41
+ ## Features
42
+
43
+ - 🔍 Extracts ATT&CK technique IDs from Sigma rules via:
44
+ - canonical `attack.<id>` tags,
45
+ - raw `T1059` tags,
46
+ - `attack.mitre.org/techniques/...` URLs in `references:`.
47
+ - 📊 Per-tactic coverage bars + overall coverage ratio in the terminal.
48
+ - 🎨 **Four HTML styles** to show off your coverage:
49
+ - `matrix` (default) — tactic cards with technique chips,
50
+ - `rows` — compact tactic progress bars,
51
+ - `heat` — dense heatmap of every technique,
52
+ - `report` — print-ready dossier with canonical vertical ATT&CK columns
53
+ and sub-techniques nested under their parents (PDF it for audits/portfolio).
54
+ - 🟢 Covered techniques (incl. parent techniques of covered sub-techniques)
55
+ render green; gaps render red; explicitly ignored ones render grey.
56
+ - 🔎 **Scope filters** — `--include` and `--ignore` narrow the analysis to the
57
+ techniques/tactics you care about (or exclude the ones you don't). The active
58
+ scope is shown in the report so readers know what was filtered.
59
+ - 🏅 **Portfolio badge** (`--badge`) — a shields.io-style SVG coverage badge.
60
+ - 🧾 **JSON summary** (`--json`) — machine-readable output for dashboards/CI.
61
+ - 🧭 **ATT&CK Navigator export** — every HTML report embeds an
62
+ "Export layer" button that downloads a ready-to-import Navigator layer.
63
+ - 🧱 Uses the **real MITRE ATT&CK enterprise dataset** (v19 structure:
64
+ Stealth + Defense Impairment). Revoked/deprecated techniques are
65
+ filtered out so the coverage denominator matches the official matrix
66
+ (15 tactics, 222 techniques, 475 sub-techniques). Shipped as a compact
67
+ JSON, no network needed at runtime.
68
+ - ✅ Pure Python stdlib + `pyyaml`; tiny dependency footprint.
69
+ - 🤖 Non-zero exit when nothing maps → use it as a **CI gate** in your
70
+ detection repo.
71
+
72
+ ## Screenshots
73
+
74
+ | `report` (print-ready dossier) | `matrix` (cards) |
75
+ | --- | --- |
76
+ | ![report style](docs/screenshot_report.png) | ![matrix style](docs/screenshot_matrix.png) |
77
+
78
+ ## Install
79
+
80
+ ```bash
81
+ pip install attack-mapper
82
+ ```
83
+
84
+ For development:
85
+
86
+ ```bash
87
+ python -m venv .venv && source .venv/bin/activate
88
+ pip install -e ".[dev]"
89
+ pytest
90
+ ```
91
+
92
+ ## Usage
93
+
94
+ ```bash
95
+ # Map a folder of Sigma rules, print the terminal report, emit HTML (matrix)
96
+ attack-mapper rules/ --html sample/coverage_matrix.html
97
+
98
+ # Try the other HTML styles
99
+ attack-mapper rules/ --html sample/coverage_rows.html --style rows
100
+ attack-mapper rules/ --html sample/coverage_heat.html --style heat
101
+ attack-mapper rules/ --html sample/coverage_report.html --style report
102
+
103
+ # Scope the analysis: only Execution + T1053, ignore Reconnaissance
104
+ attack-mapper rules/ --include T1059 TA0002 --ignore Reconnaissance
105
+
106
+ # Portfolio artifacts: badge + JSON
107
+ attack-mapper rules/ --badge sample/coverage_badge.svg --json sample/coverage.json
108
+ ```
109
+
110
+ Run against the bundled sample rules:
111
+
112
+ ```bash
113
+ attack-mapper rules/ --html sample/coverage_matrix.html --badge sample/coverage_badge.svg
114
+ ```
115
+
116
+ ## Why not sigma2attack or DeTT&CT?
117
+
118
+ Both are excellent and you should know they exist. **sigma2attack** (from the
119
+ Sigma tooling) converts Sigma rules into an ATT&CK Navigator heatmap layer —
120
+ if all you need is a layer file, it does the job. **DeTT&CT** is the
121
+ heavyweight: it scores data-source *visibility* as well as detection
122
+ coverage, and is the right tool for a mature SOC doing formal capability
123
+ assessments.
124
+
125
+ attack-mapper sits in between, optimized for Detection-as-Code pipelines:
126
+ it runs offline with a bundled dataset, produces self-contained HTML
127
+ reports you can email or print (plus the Navigator layer, a badge, and
128
+ JSON), gates CI with its exit code, and supports scope filters for
129
+ counting only the techniques relevant to your environment. It is also
130
+ **ATT&CK v19-native** (Stealth + Defense Impairment) with a scheduled
131
+ workflow that flags future MITRE updates automatically.
132
+
133
+ ## How it works
134
+
135
+ ```
136
+ Sigma rules ──▶ sigma_parser ──▶ technique IDs
137
+
138
+ ATT&CK DB ──▶ attack_loader │
139
+
140
+ coverage.build_report (with --include/--ignore) ──▶ CoverageReport
141
+
142
+ renderers (terminal / HTML matrix|rows|heat)
143
+ plugins (badge SVG / JSON)
144
+ ```
145
+
146
+ ## Scope filters
147
+
148
+ `--include` and `--ignore` accept technique IDs (e.g. `T1059`), sub-technique
149
+ prefixes (e.g. `T1059` also covers `T1059.001`), tactic shortnames
150
+ (e.g. `execution`) or tactic IDs (e.g. `TA0002`). A covered technique is always
151
+ recorded as covered even if it falls outside an `--include` scope, but only
152
+ in-scope covered techniques count toward the coverage ratio (both numerator
153
+ and denominator are scoped).
154
+
155
+ ## Updating the ATT&CK dataset
156
+
157
+ The shipped `attack_mapper/data/attack_db.json` is a snapshot (regenerated
158
+ automatically by a monthly GitHub Action that fails when MITRE ships an update). To regenerate from the latest
159
+ official STIX bundle:
160
+
161
+ ```bash
162
+ curl -L -o enterprise-attack.json \
163
+ https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json
164
+ ATTACK_MAPPER_STIX=enterprise-attack.json python -m attack_mapper.build_db
165
+ ```
166
+
167
+ You can also run directly off the STIX bundle at runtime by setting
168
+ `ATTACK_MAPPER_STIX`.
169
+
170
+ ## Project layout
171
+
172
+ ```
173
+ attack-mapper/
174
+ ├── attack_mapper/
175
+ │ ├── cli.py # argparse entry point
176
+ │ ├── sigma_parser.py # Sigma → ATT&CK technique IDs
177
+ │ ├── attack_loader.py # ATT&CK DB (compact JSON / STIX)
178
+ │ ├── coverage.py # coverage + gaps + scope filters
179
+ │ ├── renderers.py # terminal table + 4 HTML styles
180
+ │ ├── plugins.py # badge SVG + JSON summary
181
+ │ ├── build_db.py # regenerate the compact ATT&CK DB
182
+ │ └── data/attack_db.json # real ATT&CK (compact, ships with the package)
183
+ ├── rules/ # sample Sigma rules
184
+ ├── sample/ # generated HTML / badge / JSON examples
185
+ ├── tests/ # pytest suite
186
+ └── pyproject.toml
187
+ ```
188
+
189
+ ## License
190
+
191
+ MIT
@@ -0,0 +1,15 @@
1
+ attack_mapper/__init__.py,sha256=8E18haz50dxYNYdDxJp3C8ojsctzdhhLT6dfxw8L8nM,687
2
+ attack_mapper/attack_loader.py,sha256=v323T6kObJ0e2WEkF2Ro79UFBYcObUSkd4gOeri7RHM,5332
3
+ attack_mapper/build_db.py,sha256=mBV3WGxd4Xu0pV769eqvKzXrK5yGS87oT0r9U7vG5Pk,1173
4
+ attack_mapper/cli.py,sha256=6LUz7XLd7unNnrBhdBnOeIVyo1o8kBTWOfvD-YkBnOo,3099
5
+ attack_mapper/coverage.py,sha256=8JAaH3Dh2iZbjv03ltPk9GYXFPJ6ePlWOKgpFCsfkqg,7457
6
+ attack_mapper/plugins.py,sha256=BJOIHWUubcqTjcdGsK6gF9TDztDoX4kthC6pQrkSS-U,2677
7
+ attack_mapper/renderers.py,sha256=y89X4W6Gs7BWCKv0ZewzUepnaPAf9su5RbbzYHkQcqY,29866
8
+ attack_mapper/sigma_parser.py,sha256=A_YZgbC2R1BhPfFbMmkekZyrRUnXdSLqvg8rmDftNHE,4400
9
+ attack_mapper/data/attack_db.json,sha256=8itrhEmKWjqy7nYKv29wmZJDmAz-H5UpmeOKQ9DEYDI,148132
10
+ attack_mapper-0.4.0.dist-info/licenses/LICENSE,sha256=UQZhhedQt0Hg_y1c16euHxg79FIigwTEWjETNCBz78Y,1075
11
+ attack_mapper-0.4.0.dist-info/METADATA,sha256=ax7hy2u5TPDRE9F_E1siFisIktRd4SL3rTy-S_7hY3o,8177
12
+ attack_mapper-0.4.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
13
+ attack_mapper-0.4.0.dist-info/entry_points.txt,sha256=l1kpky2iHq28vnfzZq-g3uPcjVhNZzyYaz3YSDvX6aI,57
14
+ attack_mapper-0.4.0.dist-info/top_level.txt,sha256=DpO7ULjELDmK02KXSLwvGUSRbKNr6F5trdGEh1HCcjU,14
15
+ attack_mapper-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ attack-mapper = attack_mapper.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jose (JoseArgento)
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.
@@ -0,0 +1 @@
1
+ attack_mapper