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.
- attack_mapper/__init__.py +30 -0
- attack_mapper/attack_loader.py +140 -0
- attack_mapper/build_db.py +44 -0
- attack_mapper/cli.py +94 -0
- attack_mapper/coverage.py +190 -0
- attack_mapper/data/attack_db.json +6545 -0
- attack_mapper/plugins.py +72 -0
- attack_mapper/renderers.py +652 -0
- attack_mapper/sigma_parser.py +128 -0
- attack_mapper-0.4.0.dist-info/METADATA +191 -0
- attack_mapper-0.4.0.dist-info/RECORD +15 -0
- attack_mapper-0.4.0.dist-info/WHEEL +5 -0
- attack_mapper-0.4.0.dist-info/entry_points.txt +2 -0
- attack_mapper-0.4.0.dist-info/licenses/LICENSE +21 -0
- attack_mapper-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
[](https://github.com/JoseArgento/attack-mapper/actions/workflows/ci.yml)
|
|
29
|
+
[](https://github.com/JoseArgento/attack-mapper/actions/workflows/update-attack-db.yml)
|
|
30
|
+
[](https://pypi.org/project/attack-mapper/)
|
|
31
|
+
[](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
|
+
|  |  |
|
|
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,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
|