attack-mapper 0.4.0__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.
@@ -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,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,166 @@
1
+ # attack-mapper ๐ŸŽฏ
2
+
3
+ [![CI](https://github.com/JoseArgento/attack-mapper/actions/workflows/ci.yml/badge.svg)](https://github.com/JoseArgento/attack-mapper/actions/workflows/ci.yml)
4
+ [![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)
5
+ [![PyPI](https://img.shields.io/pypi/v/attack-mapper)](https://pypi.org/project/attack-mapper/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
7
+
8
+ Map your **Sigma detection rules** onto the **MITRE ATT&CK** matrix and find your
9
+ coverage gaps โ€” fast, offline, and CI-friendly.
10
+
11
+ Part of a detection-engineering toolkit that pairs perfectly with a
12
+ *Detection-as-Code* pipeline (Sigma โ†’ CI โ†’ SIEM). It answers the question every
13
+ blue team eventually asks: *"Which ATT&CK techniques do my rules actually
14
+ cover, and where are the blind spots?"*
15
+
16
+ ## Features
17
+
18
+ - ๐Ÿ” Extracts ATT&CK technique IDs from Sigma rules via:
19
+ - canonical `attack.<id>` tags,
20
+ - raw `T1059` tags,
21
+ - `attack.mitre.org/techniques/...` URLs in `references:`.
22
+ - ๐Ÿ“Š Per-tactic coverage bars + overall coverage ratio in the terminal.
23
+ - ๐ŸŽจ **Four HTML styles** to show off your coverage:
24
+ - `matrix` (default) โ€” tactic cards with technique chips,
25
+ - `rows` โ€” compact tactic progress bars,
26
+ - `heat` โ€” dense heatmap of every technique,
27
+ - `report` โ€” print-ready dossier with canonical vertical ATT&CK columns
28
+ and sub-techniques nested under their parents (PDF it for audits/portfolio).
29
+ - ๐ŸŸข Covered techniques (incl. parent techniques of covered sub-techniques)
30
+ render green; gaps render red; explicitly ignored ones render grey.
31
+ - ๐Ÿ”Ž **Scope filters** โ€” `--include` and `--ignore` narrow the analysis to the
32
+ techniques/tactics you care about (or exclude the ones you don't). The active
33
+ scope is shown in the report so readers know what was filtered.
34
+ - ๐Ÿ… **Portfolio badge** (`--badge`) โ€” a shields.io-style SVG coverage badge.
35
+ - ๐Ÿงพ **JSON summary** (`--json`) โ€” machine-readable output for dashboards/CI.
36
+ - ๐Ÿงญ **ATT&CK Navigator export** โ€” every HTML report embeds an
37
+ "Export layer" button that downloads a ready-to-import Navigator layer.
38
+ - ๐Ÿงฑ Uses the **real MITRE ATT&CK enterprise dataset** (v19 structure:
39
+ Stealth + Defense Impairment). Revoked/deprecated techniques are
40
+ filtered out so the coverage denominator matches the official matrix
41
+ (15 tactics, 222 techniques, 475 sub-techniques). Shipped as a compact
42
+ JSON, no network needed at runtime.
43
+ - โœ… Pure Python stdlib + `pyyaml`; tiny dependency footprint.
44
+ - ๐Ÿค– Non-zero exit when nothing maps โ†’ use it as a **CI gate** in your
45
+ detection repo.
46
+
47
+ ## Screenshots
48
+
49
+ | `report` (print-ready dossier) | `matrix` (cards) |
50
+ | --- | --- |
51
+ | ![report style](docs/screenshot_report.png) | ![matrix style](docs/screenshot_matrix.png) |
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install attack-mapper
57
+ ```
58
+
59
+ For development:
60
+
61
+ ```bash
62
+ python -m venv .venv && source .venv/bin/activate
63
+ pip install -e ".[dev]"
64
+ pytest
65
+ ```
66
+
67
+ ## Usage
68
+
69
+ ```bash
70
+ # Map a folder of Sigma rules, print the terminal report, emit HTML (matrix)
71
+ attack-mapper rules/ --html sample/coverage_matrix.html
72
+
73
+ # Try the other HTML styles
74
+ attack-mapper rules/ --html sample/coverage_rows.html --style rows
75
+ attack-mapper rules/ --html sample/coverage_heat.html --style heat
76
+ attack-mapper rules/ --html sample/coverage_report.html --style report
77
+
78
+ # Scope the analysis: only Execution + T1053, ignore Reconnaissance
79
+ attack-mapper rules/ --include T1059 TA0002 --ignore Reconnaissance
80
+
81
+ # Portfolio artifacts: badge + JSON
82
+ attack-mapper rules/ --badge sample/coverage_badge.svg --json sample/coverage.json
83
+ ```
84
+
85
+ Run against the bundled sample rules:
86
+
87
+ ```bash
88
+ attack-mapper rules/ --html sample/coverage_matrix.html --badge sample/coverage_badge.svg
89
+ ```
90
+
91
+ ## Why not sigma2attack or DeTT&CT?
92
+
93
+ Both are excellent and you should know they exist. **sigma2attack** (from the
94
+ Sigma tooling) converts Sigma rules into an ATT&CK Navigator heatmap layer โ€”
95
+ if all you need is a layer file, it does the job. **DeTT&CT** is the
96
+ heavyweight: it scores data-source *visibility* as well as detection
97
+ coverage, and is the right tool for a mature SOC doing formal capability
98
+ assessments.
99
+
100
+ attack-mapper sits in between, optimized for Detection-as-Code pipelines:
101
+ it runs offline with a bundled dataset, produces self-contained HTML
102
+ reports you can email or print (plus the Navigator layer, a badge, and
103
+ JSON), gates CI with its exit code, and supports scope filters for
104
+ counting only the techniques relevant to your environment. It is also
105
+ **ATT&CK v19-native** (Stealth + Defense Impairment) with a scheduled
106
+ workflow that flags future MITRE updates automatically.
107
+
108
+ ## How it works
109
+
110
+ ```
111
+ Sigma rules โ”€โ”€โ–ถ sigma_parser โ”€โ”€โ–ถ technique IDs
112
+ โ”‚
113
+ ATT&CK DB โ”€โ”€โ–ถ attack_loader โ”‚
114
+ โ–ผ
115
+ coverage.build_report (with --include/--ignore) โ”€โ”€โ–ถ CoverageReport
116
+ โ”‚
117
+ renderers (terminal / HTML matrix|rows|heat)
118
+ plugins (badge SVG / JSON)
119
+ ```
120
+
121
+ ## Scope filters
122
+
123
+ `--include` and `--ignore` accept technique IDs (e.g. `T1059`), sub-technique
124
+ prefixes (e.g. `T1059` also covers `T1059.001`), tactic shortnames
125
+ (e.g. `execution`) or tactic IDs (e.g. `TA0002`). A covered technique is always
126
+ recorded as covered even if it falls outside an `--include` scope, but only
127
+ in-scope covered techniques count toward the coverage ratio (both numerator
128
+ and denominator are scoped).
129
+
130
+ ## Updating the ATT&CK dataset
131
+
132
+ The shipped `attack_mapper/data/attack_db.json` is a snapshot (regenerated
133
+ automatically by a monthly GitHub Action that fails when MITRE ships an update). To regenerate from the latest
134
+ official STIX bundle:
135
+
136
+ ```bash
137
+ curl -L -o enterprise-attack.json \
138
+ https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json
139
+ ATTACK_MAPPER_STIX=enterprise-attack.json python -m attack_mapper.build_db
140
+ ```
141
+
142
+ You can also run directly off the STIX bundle at runtime by setting
143
+ `ATTACK_MAPPER_STIX`.
144
+
145
+ ## Project layout
146
+
147
+ ```
148
+ attack-mapper/
149
+ โ”œโ”€โ”€ attack_mapper/
150
+ โ”‚ โ”œโ”€โ”€ cli.py # argparse entry point
151
+ โ”‚ โ”œโ”€โ”€ sigma_parser.py # Sigma โ†’ ATT&CK technique IDs
152
+ โ”‚ โ”œโ”€โ”€ attack_loader.py # ATT&CK DB (compact JSON / STIX)
153
+ โ”‚ โ”œโ”€โ”€ coverage.py # coverage + gaps + scope filters
154
+ โ”‚ โ”œโ”€โ”€ renderers.py # terminal table + 4 HTML styles
155
+ โ”‚ โ”œโ”€โ”€ plugins.py # badge SVG + JSON summary
156
+ โ”‚ โ”œโ”€โ”€ build_db.py # regenerate the compact ATT&CK DB
157
+ โ”‚ โ””โ”€โ”€ data/attack_db.json # real ATT&CK (compact, ships with the package)
158
+ โ”œโ”€โ”€ rules/ # sample Sigma rules
159
+ โ”œโ”€โ”€ sample/ # generated HTML / badge / JSON examples
160
+ โ”œโ”€โ”€ tests/ # pytest suite
161
+ โ””โ”€โ”€ pyproject.toml
162
+ ```
163
+
164
+ ## License
165
+
166
+ MIT
@@ -0,0 +1,30 @@
1
+ """attack-mapper: map your Sigma detection rules onto the MITRE ATT&CK matrix."""
2
+
3
+ from .attack_loader import AttackDB, load_attack
4
+ from .coverage import CoverageReport, build_report
5
+ from .sigma_parser import (
6
+ ParsedRule,
7
+ parse_rule_file,
8
+ parse_rule_text,
9
+ parse_rules,
10
+ )
11
+ from .renderers import render_terminal, render_html, STYLES
12
+ from .plugins import render_badge, render_json
13
+
14
+ __version__ = "0.4.0"
15
+
16
+ __all__ = [
17
+ "AttackDB",
18
+ "load_attack",
19
+ "CoverageReport",
20
+ "build_report",
21
+ "ParsedRule",
22
+ "parse_rule_file",
23
+ "parse_rule_text",
24
+ "parse_rules",
25
+ "render_terminal",
26
+ "render_html",
27
+ "STYLES",
28
+ "render_badge",
29
+ "render_json",
30
+ ]
@@ -0,0 +1,140 @@
1
+ """Load MITRE ATT&CK data for coverage mapping.
2
+
3
+ Two modes are supported:
4
+
5
+ * **Bundled compact DB** (``data/attack_db.json``) โ€” generated by
6
+ :mod:`attack_mapper.build_db` from the official MITRE CTI STIX bundle. This
7
+ is the default because it is small (``~180 KB``) and needs no network.
8
+ * **Full STIX bundle** (``enterprise-attack.json``) โ€” if you point
9
+ ``ATTACK_MAPPER_STIX`` at the raw STIX 2.0/2.1 bundle, it is parsed directly
10
+ so you always have the latest techniques/tactics.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ from dataclasses import dataclass, field
18
+ from typing import Dict, List, Optional
19
+
20
+ DEFAULT_DB_PATH = os.path.join(os.path.dirname(__file__), "data", "attack_db.json")
21
+
22
+
23
+ @dataclass
24
+ class AttackDB:
25
+ """In-memory view of ATT&CK tactics and techniques."""
26
+
27
+ version: str = ""
28
+ tactics: Dict[str, dict] = field(default_factory=dict)
29
+ techniques: Dict[str, dict] = field(default_factory=dict)
30
+
31
+ def technique_name(self, tid: str) -> Optional[str]:
32
+ t = self.techniques.get(tid.upper())
33
+ return t["name"] if t else None
34
+
35
+ def tactic_name(self, shortname: str) -> Optional[str]:
36
+ t = self.tactics.get(shortname)
37
+ return t["name"] if t else shortname
38
+
39
+ def valid_technique(self, tid: str) -> bool:
40
+ return tid.upper() in self.techniques
41
+
42
+ def resolve_tactic_token(self, token: str) -> str:
43
+ """Map a TA tactic ID (e.g. ``TA0002``) to its shortname.
44
+
45
+ Returns the token unchanged if it isn't a known TA id, so scope
46
+ tokens can be technique IDs, shortnames, or TA ids interchangeably.
47
+ Older compact DBs may lack the ``id`` field โ€” fall back to parsing
48
+ the TA id out of the tactic URL.
49
+ """
50
+ wanted = token.strip().upper()
51
+ if not wanted.startswith("TA"):
52
+ return token
53
+ for shortname, tac in self.tactics.items():
54
+ taid = tac.get("id")
55
+ if not taid:
56
+ url = tac.get("url") or ""
57
+ if "/tactics/" in url:
58
+ taid = url.rstrip("/").rsplit("/", 1)[-1]
59
+ if taid and taid.upper() == wanted:
60
+ return shortname
61
+ return token
62
+
63
+
64
+ def _load_compact(path: str) -> AttackDB:
65
+ with open(path, "r", encoding="utf-8") as fh:
66
+ d = json.load(fh)
67
+ return AttackDB(
68
+ version=d.get("version", ""),
69
+ tactics=d.get("tactics", {}),
70
+ techniques=d.get("techniques", {}),
71
+ )
72
+
73
+
74
+ def _load_stix(path: str) -> AttackDB:
75
+ with open(path, "r", encoding="utf-8") as fh:
76
+ bundle = json.load(fh)
77
+ tactics: Dict[str, dict] = {}
78
+ techniques: Dict[str, dict] = {}
79
+ version = ""
80
+ for o in bundle.get("objects", []):
81
+ t = o.get("type")
82
+ # Revoked/deprecated objects are kept in the STIX bundle for history,
83
+ # but they are no longer part of ATT&CK โ€” including them would inflate
84
+ # the coverage denominator with techniques that don't exist anymore.
85
+ if o.get("revoked") or o.get("x_mitre_deprecated"):
86
+ continue
87
+ if t == "x-mitre-matrix":
88
+ # The bundle has no version object; the matrix name + last-modified
89
+ # date is the most honest snapshot identifier available.
90
+ modified = (o.get("modified") or "")[:10]
91
+ version = f'{o.get("name", "ATT&CK")} ({modified})' if modified else o.get("name", "")
92
+ elif t == "x-mitre-tactic":
93
+ sn = o.get("x_mitre_shortname")
94
+ ref = next(
95
+ (e for e in o.get("external_references", []) if e.get("source_name") == "mitre-attack"),
96
+ {},
97
+ )
98
+ tactics[sn] = {
99
+ "id": ref.get("external_id"),
100
+ "name": o.get("name"),
101
+ "shortname": sn,
102
+ "url": ref.get("url"),
103
+ }
104
+ elif t == "attack-pattern":
105
+ refs = [e for e in o.get("external_references", []) if e.get("source_name") == "mitre-attack"]
106
+ if not refs:
107
+ continue
108
+ ext = refs[0].get("external_id")
109
+ if not (ext and ext.startswith("T")):
110
+ continue
111
+ techniques[ext] = {
112
+ "id": ext,
113
+ "name": o.get("name"),
114
+ "url": refs[0].get("url"),
115
+ "tactics": [p.get("phase_name") for p in o.get("kill_chain_phases", [])],
116
+ "is_subtechnique": "." in ext,
117
+ }
118
+ return AttackDB(version=version or "stix", tactics=tactics, techniques=techniques)
119
+
120
+
121
+ def load_attack(db_path: Optional[str] = None) -> AttackDB:
122
+ """Load ATT&CK data.
123
+
124
+ Resolution order:
125
+ 1. ``db_path`` argument if provided.
126
+ 2. ``ATTACK_MAPPER_STIX`` env var -> full STIX bundle.
127
+ 3. ``ATTACK_MAPPER_DB`` env var -> compact DB.
128
+ 4. Bundled compact DB shipped with the package.
129
+ """
130
+ stix = os.environ.get("ATTACK_MAPPER_STIX")
131
+ compact = db_path or os.environ.get("ATTACK_MAPPER_DB") or os.path.abspath(DEFAULT_DB_PATH)
132
+
133
+ if stix and os.path.isfile(stix):
134
+ return _load_stix(stix)
135
+ if os.path.isfile(compact):
136
+ return _load_compact(compact)
137
+ raise FileNotFoundError(
138
+ f"No ATT&CK database found. Looked for compact DB at {compact} "
139
+ f"and STIX at {stix}. Run `python -m attack_mapper.build_db` to generate one."
140
+ )
@@ -0,0 +1,44 @@
1
+ """Generate data/attack_db.json from a MITRE CTI STIX bundle.
2
+
3
+ Usage:
4
+ ATTACK_MAPPER_STIX=path/to/enterprise-attack.json \
5
+ python -m attack_mapper.build_db
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+
13
+ from .attack_loader import _load_stix # noqa: F401 (reuse the parser)
14
+
15
+ DEFAULT_OUT = os.path.join(os.path.dirname(__file__), "data", "attack_db.json")
16
+
17
+
18
+ def main() -> int:
19
+ stix = os.environ.get("ATTACK_MAPPER_STIX")
20
+ if not stix or not os.path.isfile(stix):
21
+ print(
22
+ "Set ATTACK_MAPPER_STIX to a downloaded enterprise-attack.json "
23
+ "(e.g. https://github.com/mitre/cti).",
24
+ file=__import__("sys").stderr,
25
+ )
26
+ return 1
27
+ db = _load_stix(stix)
28
+ out = os.path.abspath(DEFAULT_OUT)
29
+ compact = {
30
+ "version": db.version,
31
+ "tactics": db.tactics,
32
+ "techniques": db.techniques,
33
+ }
34
+ with open(out, "w", encoding="utf-8") as fh:
35
+ json.dump(compact, fh, indent=1)
36
+ print(
37
+ f"Wrote {len(db.techniques)} techniques / {len(db.tactics)} tactics "
38
+ f"to {out}"
39
+ )
40
+ return 0
41
+
42
+
43
+ if __name__ == "__main__":
44
+ raise SystemExit(main())
@@ -0,0 +1,94 @@
1
+ """CLI entry point for attack-mapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+
9
+ from .attack_loader import load_attack
10
+ from .coverage import build_report
11
+ from .renderers import render_terminal, render_html, STYLES
12
+ from .sigma_parser import parse_rules
13
+ from .plugins import render_badge, render_json
14
+
15
+
16
+ def build_parser() -> argparse.ArgumentParser:
17
+ p = argparse.ArgumentParser(
18
+ prog="attack-mapper",
19
+ description="Map your Sigma detection rules onto the MITRE ATT&CK matrix "
20
+ "and find coverage gaps.",
21
+ )
22
+ p.add_argument(
23
+ "rules",
24
+ nargs="+",
25
+ help="Sigma rule file(s), directory(ies), or glob pattern(s).",
26
+ )
27
+ p.add_argument("--db", default=None, help="Path to a compact ATT&CK DB JSON.")
28
+ p.add_argument(
29
+ "--html", metavar="PATH", default=None,
30
+ help="Write a standalone HTML coverage report to PATH.",
31
+ )
32
+ p.add_argument(
33
+ "--style", choices=STYLES, default="matrix",
34
+ help="HTML layout: matrix (cards), rows (bars), or heat (dense).",
35
+ )
36
+ p.add_argument(
37
+ "--include", nargs="*", default=None, metavar="TOKEN",
38
+ help="Only count/show techniques or tactics matching these tokens "
39
+ "(e.g. T1059 TA0002). Others are treated as out-of-scope.",
40
+ )
41
+ p.add_argument(
42
+ "--ignore", nargs="*", default=None, metavar="TOKEN",
43
+ help="Exclude these techniques/tactics from scope (e.g. T1003 Reconnaissance).",
44
+ )
45
+ p.add_argument(
46
+ "--json", metavar="PATH", default=None,
47
+ help="Also write a machine-readable JSON summary to PATH.",
48
+ )
49
+ p.add_argument(
50
+ "--badge", metavar="PATH", default=None,
51
+ help="Also write an SVG coverage badge to PATH (great for a README).",
52
+ )
53
+ p.add_argument("--quiet", action="store_true", help="Suppress the terminal table.")
54
+ p.add_argument("--version", action="version", version="%(prog)s 0.4.0")
55
+ return p
56
+
57
+
58
+ def main(argv=None) -> int:
59
+ args = build_parser().parse_args(argv)
60
+ try:
61
+ db = load_attack(args.db)
62
+ except FileNotFoundError as exc:
63
+ print(str(exc), file=sys.stderr)
64
+ return 2
65
+
66
+ rules = parse_rules(args.rules)
67
+ if not rules:
68
+ print("No Sigma rules found at the given path(s).", file=sys.stderr)
69
+ return 1
70
+
71
+ report = build_report(rules, db, include=args.include, ignore=args.ignore)
72
+
73
+ if not args.quiet:
74
+ print(render_terminal(report))
75
+
76
+ if args.html:
77
+ out = render_html(report, args.html, style=args.style)
78
+ print(f"\nHTML report written to: {os.path.abspath(out)}")
79
+
80
+ if args.json:
81
+ with open(args.json, "w", encoding="utf-8") as fh:
82
+ fh.write(render_json(report))
83
+ print(f"JSON summary written to: {os.path.abspath(args.json)}")
84
+
85
+ if args.badge:
86
+ with open(args.badge, "w", encoding="utf-8") as fh:
87
+ fh.write(render_badge(report))
88
+ print(f"Badge written to: {os.path.abspath(args.badge)}")
89
+
90
+ return 0 if report.covered else 1
91
+
92
+
93
+ if __name__ == "__main__":
94
+ raise SystemExit(main())