attackmap 0.1.1__tar.gz → 0.2.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.
- {attackmap-0.1.1/src/attackmap.egg-info → attackmap-0.2.0}/PKG-INFO +108 -1
- {attackmap-0.1.1 → attackmap-0.2.0}/README.md +106 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/pyproject.toml +2 -1
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/__init__.py +1 -1
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/analyzer.py +51 -6
- attackmap-0.2.0/src/attackmap/analyzer_contracts.py +190 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/analyzers.py +155 -13
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/cli.py +153 -0
- attackmap-0.2.0/src/attackmap/config_scanner.py +291 -0
- attackmap-0.2.0/src/attackmap/cve.py +436 -0
- attackmap-0.2.0/src/attackmap/diagrams.py +224 -0
- attackmap-0.2.0/src/attackmap/diff.py +179 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/llm_review.py +25 -2
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/merge.py +19 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/models.py +146 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/report.py +36 -1
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/review_eval.py +70 -16
- attackmap-0.2.0/src/attackmap/sarif.py +231 -0
- attackmap-0.2.0/src/attackmap/sbom.py +384 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/scanner.py +278 -11
- attackmap-0.2.0/src/attackmap/suggest.py +386 -0
- attackmap-0.2.0/src/attackmap/taint.py +369 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/threat_model.py +397 -19
- attackmap-0.2.0/src/attackmap/topology.py +316 -0
- {attackmap-0.1.1 → attackmap-0.2.0/src/attackmap.egg-info}/PKG-INFO +108 -1
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap.egg-info/SOURCES.txt +21 -1
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap.egg-info/requires.txt +1 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_analyzer.py +101 -0
- attackmap-0.2.0/tests/test_analyzer_metadata_schema.py +194 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_analyzers.py +127 -10
- attackmap-0.2.0/tests/test_config_scanner.py +265 -0
- attackmap-0.2.0/tests/test_cve.py +403 -0
- attackmap-0.2.0/tests/test_diagrams.py +256 -0
- attackmap-0.2.0/tests/test_diff.py +368 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_llm_review.py +38 -0
- attackmap-0.2.0/tests/test_provenance.py +208 -0
- attackmap-0.2.0/tests/test_review_eval.py +165 -0
- attackmap-0.2.0/tests/test_sarif.py +219 -0
- attackmap-0.2.0/tests/test_sbom.py +250 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_scanner.py +378 -0
- attackmap-0.2.0/tests/test_suggest.py +259 -0
- attackmap-0.2.0/tests/test_taint.py +205 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_threat_model.py +374 -2
- attackmap-0.2.0/tests/test_topology.py +220 -0
- attackmap-0.1.1/src/attackmap/analyzer_contracts.py +0 -104
- attackmap-0.1.1/tests/test_review_eval.py +0 -68
- {attackmap-0.1.1 → attackmap-0.2.0}/LICENSE +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/setup.cfg +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/asset_model.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/attack_taxonomy.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/context_pack.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/control_model.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/defensive_review.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/detection_opportunities.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/graph.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/insights.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/recon_models.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/recon_to_analysis.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/review_json.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/review_prompts.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/sdk/__init__.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/sdk/contracts.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/sdk/models.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap/security_overlay.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap.egg-info/dependency_links.txt +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap.egg-info/entry_points.txt +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/src/attackmap.egg-info/top_level.txt +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_artifact_schemas.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_context_pack.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_defensive_review.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_graph.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_merge.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_recon_to_analysis.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_report.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_review_json.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_review_prompts.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_security_overlay.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_shared_contract_imports.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_signal_v2.py +0 -0
- {attackmap-0.1.1 → attackmap-0.2.0}/tests/test_threat_intel.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: attackmap
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: AI-assisted defensive security analyzer for codebases — scans a repository, models assets and controls, finds cross-cutting weaknesses, and generates an evidence-grounded review with MITRE ATT&CK mappings and detection-engineering hints.
|
|
5
5
|
Author: AttackMap Contributors
|
|
6
6
|
Author-email: Matthew Davis <matthewd@matthewd.xyz>
|
|
@@ -51,6 +51,7 @@ Requires-Dist: attackmap-analyzer-atproto>=0.1.0; extra == "all"
|
|
|
51
51
|
Requires-Dist: attackmap-analyzer-php-web>=0.1.0; extra == "all"
|
|
52
52
|
Requires-Dist: attackmap-analyzer-php-laminas>=0.1.0; extra == "all"
|
|
53
53
|
Requires-Dist: attackmap-analyzer-omeka-s>=0.1.0; extra == "all"
|
|
54
|
+
Requires-Dist: attackmap-analyzer-iac>=0.1.0; extra == "all"
|
|
54
55
|
Provides-Extra: dev
|
|
55
56
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
56
57
|
Requires-Dist: build>=1.2.0; extra == "dev"
|
|
@@ -116,6 +117,17 @@ You can also install individual analyzer plugins on demand:
|
|
|
116
117
|
pip install attackmap-analyzer-python attackmap-analyzer-go
|
|
117
118
|
```
|
|
118
119
|
|
|
120
|
+
Not sure which plugins your repo needs? Let AttackMap tell you:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
attackmap suggest ./path/to/repo # print ranked pip lines
|
|
124
|
+
attackmap suggest ./path/to/repo --install # and install them (prompts once)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`suggest` inspects the repo's manifest files, extensions, and directory layout
|
|
128
|
+
and recommends only the plugins that would give it deeper signal — useful
|
|
129
|
+
when you want a smaller install footprint than `[all]`.
|
|
130
|
+
|
|
119
131
|
### With Docker
|
|
120
132
|
|
|
121
133
|
```bash
|
|
@@ -150,9 +162,102 @@ Every `attackmap analyze` run writes:
|
|
|
150
162
|
| `defensive-review.json` | Structured equivalent (schema v1.2.0) |
|
|
151
163
|
| `review-context-pack.json` | Structured evidence pack consumed by the LLM stage |
|
|
152
164
|
| `attackmap-report.json` | Everything bundled |
|
|
165
|
+
| `attackmap-report.sarif` | SARIF 2.1.0 log — ingestable by GitHub Code Scanning, VS Code, and other SARIF consumers |
|
|
166
|
+
| `attackmap-paths.md` | Mermaid flowcharts of each attack path — renders inline on GitHub |
|
|
167
|
+
| `attackmap-topology.md` | Mermaid graph of the service topology, with edge kinds styled per relationship type |
|
|
168
|
+
| `attackmap-paths.dot` / `attackmap-topology.dot` | Graphviz DOT versions of the two diagrams — feed into `dot -Tsvg` for slide-quality graphics |
|
|
153
169
|
| `defensive-review-llm.md` *(with `--llm`)* | Claude-narrated review |
|
|
154
170
|
| `defensive-review-llm.meta.json` *(with `--llm`)* | Backend, model, token usage |
|
|
155
171
|
|
|
172
|
+
### GitHub Code Scanning integration
|
|
173
|
+
|
|
174
|
+
Drop this into `.github/workflows/attackmap.yml` to get AttackMap findings inline on every PR:
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
name: AttackMap
|
|
178
|
+
on: [pull_request, push]
|
|
179
|
+
jobs:
|
|
180
|
+
scan:
|
|
181
|
+
runs-on: ubuntu-latest
|
|
182
|
+
permissions:
|
|
183
|
+
security-events: write
|
|
184
|
+
contents: read
|
|
185
|
+
steps:
|
|
186
|
+
- uses: actions/checkout@v4
|
|
187
|
+
- uses: actions/setup-python@v5
|
|
188
|
+
with: { python-version: "3.12" }
|
|
189
|
+
- run: pip install "attackmap[all]"
|
|
190
|
+
- run: attackmap analyze . --output reports
|
|
191
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
192
|
+
with:
|
|
193
|
+
sarif_file: reports/attackmap-report.sarif
|
|
194
|
+
category: attackmap
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Diff mode (PR gating)
|
|
198
|
+
|
|
199
|
+
For a lighter CI integration than Code Scanning — a bot comment, a JSON delta,
|
|
200
|
+
or a hard fail on newly-introduced HIGH findings — point `--baseline` at a
|
|
201
|
+
prior report and AttackMap will emit a Markdown diff alongside the fresh
|
|
202
|
+
report:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
attackmap analyze . --output reports \
|
|
206
|
+
--baseline path/to/previous/attackmap-report.json \
|
|
207
|
+
--diff-output reports/attackmap-diff.md \
|
|
208
|
+
--fail-on-new-high # exit non-zero if the PR introduces any HIGH finding
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Findings get a stable id (hash of the finding title) that survives line drift
|
|
212
|
+
on unrelated commits, so a finding that persists across scans has the same id
|
|
213
|
+
in both. The diff has three sections — **New**, **Persisted**, **Resolved** —
|
|
214
|
+
which drop cleanly into a PR comment.
|
|
215
|
+
|
|
216
|
+
### SBOM inventory
|
|
217
|
+
|
|
218
|
+
Every scan also produces a lightweight SBOM by parsing direct dependencies out
|
|
219
|
+
of the common manifest files:
|
|
220
|
+
|
|
221
|
+
| Ecosystem | Files parsed |
|
|
222
|
+
|---|---|
|
|
223
|
+
| PyPI (Python) | `pyproject.toml` (PEP 621 + Poetry), `requirements.txt` |
|
|
224
|
+
| npm (Node.js) | `package.json` (dependencies + devDependencies + peer/optional) |
|
|
225
|
+
| Go | `go.mod` (single-line + block-form `require`, `// indirect` flagged) |
|
|
226
|
+
| Cargo (Rust) | `Cargo.toml` (dependencies + dev-dependencies + build-dependencies) |
|
|
227
|
+
| Composer (PHP) | `composer.json` (require + require-dev; platform reqs skipped) |
|
|
228
|
+
|
|
229
|
+
Each entry appears in `attackmap-report.json` under `scan.dependencies` with
|
|
230
|
+
`{name, version, ecosystem, file, dev}`. Version ranges are kept verbatim
|
|
231
|
+
(`^4.16.0`, `>=2,<3`, `latest`) — this slice does not resolve lockfiles.
|
|
232
|
+
|
|
233
|
+
### CVE cross-reference (opt-in)
|
|
234
|
+
|
|
235
|
+
`attackmap analyze --cve` cross-references every SBOM entry against
|
|
236
|
+
[OSV.dev](https://osv.dev) and emits one finding per vulnerable dependency
|
|
237
|
+
(all known advisories aggregated in the evidence list). CVSS scores map into
|
|
238
|
+
low/medium/high — anything ≥ 7.0 is HIGH.
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
attackmap analyze . --cve
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
- **Off by default.** The flag exists precisely because CVE lookup does
|
|
245
|
+
network I/O; regular scans stay offline.
|
|
246
|
+
- **Cached** under `~/.attackmap/cache/osv/` keyed by
|
|
247
|
+
`sha256(ecosystem+name+version)`. TTL is 24h by default, overridable via
|
|
248
|
+
`ATTACKMAP_OSV_CACHE_TTL_HOURS`. Repeat scans of the same repo don't hit
|
|
249
|
+
the network.
|
|
250
|
+
- **Offline-tolerant.** If the network's unavailable but the cache is warm,
|
|
251
|
+
cached results still surface; only fresh queries are skipped.
|
|
252
|
+
- **Version resolution is best-effort.** Manifest ranges (`^4.16.0`,
|
|
253
|
+
`>=2.28,<3`) resolve to a queryable lower-bound; OSV does the range math.
|
|
254
|
+
Unpinned specs (`*`, `latest`) are skipped.
|
|
255
|
+
|
|
256
|
+
The structured vulnerability list is available under
|
|
257
|
+
`scan.vulnerabilities` in `attackmap-report.json` with
|
|
258
|
+
`{id, aliases, summary, severity, cvss_score, references, affected_range,
|
|
259
|
+
package_name, package_version, ecosystem}`.
|
|
260
|
+
|
|
156
261
|
---
|
|
157
262
|
|
|
158
263
|
## How it works
|
|
@@ -222,6 +327,8 @@ attackmap analyze <path> --output dir # write outputs to `dir/`
|
|
|
222
327
|
attackmap analyze <path> --module python --module rust # only these analyzers
|
|
223
328
|
attackmap analyze <path> --llm # add LLM narrative (auto-resolve auth)
|
|
224
329
|
attackmap analyze <path> --llm --llm-backend cli # force Claude CLI
|
|
330
|
+
attackmap suggest ./repo # recommend plugins for a repo shape
|
|
331
|
+
attackmap suggest ./repo --install # and pip-install the missing ones
|
|
225
332
|
attackmap modules # list installed analyzers
|
|
226
333
|
```
|
|
227
334
|
|
|
@@ -58,6 +58,17 @@ You can also install individual analyzer plugins on demand:
|
|
|
58
58
|
pip install attackmap-analyzer-python attackmap-analyzer-go
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
Not sure which plugins your repo needs? Let AttackMap tell you:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
attackmap suggest ./path/to/repo # print ranked pip lines
|
|
65
|
+
attackmap suggest ./path/to/repo --install # and install them (prompts once)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`suggest` inspects the repo's manifest files, extensions, and directory layout
|
|
69
|
+
and recommends only the plugins that would give it deeper signal — useful
|
|
70
|
+
when you want a smaller install footprint than `[all]`.
|
|
71
|
+
|
|
61
72
|
### With Docker
|
|
62
73
|
|
|
63
74
|
```bash
|
|
@@ -92,9 +103,102 @@ Every `attackmap analyze` run writes:
|
|
|
92
103
|
| `defensive-review.json` | Structured equivalent (schema v1.2.0) |
|
|
93
104
|
| `review-context-pack.json` | Structured evidence pack consumed by the LLM stage |
|
|
94
105
|
| `attackmap-report.json` | Everything bundled |
|
|
106
|
+
| `attackmap-report.sarif` | SARIF 2.1.0 log — ingestable by GitHub Code Scanning, VS Code, and other SARIF consumers |
|
|
107
|
+
| `attackmap-paths.md` | Mermaid flowcharts of each attack path — renders inline on GitHub |
|
|
108
|
+
| `attackmap-topology.md` | Mermaid graph of the service topology, with edge kinds styled per relationship type |
|
|
109
|
+
| `attackmap-paths.dot` / `attackmap-topology.dot` | Graphviz DOT versions of the two diagrams — feed into `dot -Tsvg` for slide-quality graphics |
|
|
95
110
|
| `defensive-review-llm.md` *(with `--llm`)* | Claude-narrated review |
|
|
96
111
|
| `defensive-review-llm.meta.json` *(with `--llm`)* | Backend, model, token usage |
|
|
97
112
|
|
|
113
|
+
### GitHub Code Scanning integration
|
|
114
|
+
|
|
115
|
+
Drop this into `.github/workflows/attackmap.yml` to get AttackMap findings inline on every PR:
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
name: AttackMap
|
|
119
|
+
on: [pull_request, push]
|
|
120
|
+
jobs:
|
|
121
|
+
scan:
|
|
122
|
+
runs-on: ubuntu-latest
|
|
123
|
+
permissions:
|
|
124
|
+
security-events: write
|
|
125
|
+
contents: read
|
|
126
|
+
steps:
|
|
127
|
+
- uses: actions/checkout@v4
|
|
128
|
+
- uses: actions/setup-python@v5
|
|
129
|
+
with: { python-version: "3.12" }
|
|
130
|
+
- run: pip install "attackmap[all]"
|
|
131
|
+
- run: attackmap analyze . --output reports
|
|
132
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
133
|
+
with:
|
|
134
|
+
sarif_file: reports/attackmap-report.sarif
|
|
135
|
+
category: attackmap
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Diff mode (PR gating)
|
|
139
|
+
|
|
140
|
+
For a lighter CI integration than Code Scanning — a bot comment, a JSON delta,
|
|
141
|
+
or a hard fail on newly-introduced HIGH findings — point `--baseline` at a
|
|
142
|
+
prior report and AttackMap will emit a Markdown diff alongside the fresh
|
|
143
|
+
report:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
attackmap analyze . --output reports \
|
|
147
|
+
--baseline path/to/previous/attackmap-report.json \
|
|
148
|
+
--diff-output reports/attackmap-diff.md \
|
|
149
|
+
--fail-on-new-high # exit non-zero if the PR introduces any HIGH finding
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Findings get a stable id (hash of the finding title) that survives line drift
|
|
153
|
+
on unrelated commits, so a finding that persists across scans has the same id
|
|
154
|
+
in both. The diff has three sections — **New**, **Persisted**, **Resolved** —
|
|
155
|
+
which drop cleanly into a PR comment.
|
|
156
|
+
|
|
157
|
+
### SBOM inventory
|
|
158
|
+
|
|
159
|
+
Every scan also produces a lightweight SBOM by parsing direct dependencies out
|
|
160
|
+
of the common manifest files:
|
|
161
|
+
|
|
162
|
+
| Ecosystem | Files parsed |
|
|
163
|
+
|---|---|
|
|
164
|
+
| PyPI (Python) | `pyproject.toml` (PEP 621 + Poetry), `requirements.txt` |
|
|
165
|
+
| npm (Node.js) | `package.json` (dependencies + devDependencies + peer/optional) |
|
|
166
|
+
| Go | `go.mod` (single-line + block-form `require`, `// indirect` flagged) |
|
|
167
|
+
| Cargo (Rust) | `Cargo.toml` (dependencies + dev-dependencies + build-dependencies) |
|
|
168
|
+
| Composer (PHP) | `composer.json` (require + require-dev; platform reqs skipped) |
|
|
169
|
+
|
|
170
|
+
Each entry appears in `attackmap-report.json` under `scan.dependencies` with
|
|
171
|
+
`{name, version, ecosystem, file, dev}`. Version ranges are kept verbatim
|
|
172
|
+
(`^4.16.0`, `>=2,<3`, `latest`) — this slice does not resolve lockfiles.
|
|
173
|
+
|
|
174
|
+
### CVE cross-reference (opt-in)
|
|
175
|
+
|
|
176
|
+
`attackmap analyze --cve` cross-references every SBOM entry against
|
|
177
|
+
[OSV.dev](https://osv.dev) and emits one finding per vulnerable dependency
|
|
178
|
+
(all known advisories aggregated in the evidence list). CVSS scores map into
|
|
179
|
+
low/medium/high — anything ≥ 7.0 is HIGH.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
attackmap analyze . --cve
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- **Off by default.** The flag exists precisely because CVE lookup does
|
|
186
|
+
network I/O; regular scans stay offline.
|
|
187
|
+
- **Cached** under `~/.attackmap/cache/osv/` keyed by
|
|
188
|
+
`sha256(ecosystem+name+version)`. TTL is 24h by default, overridable via
|
|
189
|
+
`ATTACKMAP_OSV_CACHE_TTL_HOURS`. Repeat scans of the same repo don't hit
|
|
190
|
+
the network.
|
|
191
|
+
- **Offline-tolerant.** If the network's unavailable but the cache is warm,
|
|
192
|
+
cached results still surface; only fresh queries are skipped.
|
|
193
|
+
- **Version resolution is best-effort.** Manifest ranges (`^4.16.0`,
|
|
194
|
+
`>=2.28,<3`) resolve to a queryable lower-bound; OSV does the range math.
|
|
195
|
+
Unpinned specs (`*`, `latest`) are skipped.
|
|
196
|
+
|
|
197
|
+
The structured vulnerability list is available under
|
|
198
|
+
`scan.vulnerabilities` in `attackmap-report.json` with
|
|
199
|
+
`{id, aliases, summary, severity, cvss_score, references, affected_range,
|
|
200
|
+
package_name, package_version, ecosystem}`.
|
|
201
|
+
|
|
98
202
|
---
|
|
99
203
|
|
|
100
204
|
## How it works
|
|
@@ -164,6 +268,8 @@ attackmap analyze <path> --output dir # write outputs to `dir/`
|
|
|
164
268
|
attackmap analyze <path> --module python --module rust # only these analyzers
|
|
165
269
|
attackmap analyze <path> --llm # add LLM narrative (auto-resolve auth)
|
|
166
270
|
attackmap analyze <path> --llm --llm-backend cli # force Claude CLI
|
|
271
|
+
attackmap suggest ./repo # recommend plugins for a repo shape
|
|
272
|
+
attackmap suggest ./repo --install # and pip-install the missing ones
|
|
167
273
|
attackmap modules # list installed analyzers
|
|
168
274
|
```
|
|
169
275
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "attackmap"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "AI-assisted defensive security analyzer for codebases — scans a repository, models assets and controls, finds cross-cutting weaknesses, and generates an evidence-grounded review with MITRE ATT&CK mappings and detection-engineering hints."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -72,6 +72,7 @@ all = [
|
|
|
72
72
|
"attackmap-analyzer-php-web>=0.1.0",
|
|
73
73
|
"attackmap-analyzer-php-laminas>=0.1.0",
|
|
74
74
|
"attackmap-analyzer-omeka-s>=0.1.0",
|
|
75
|
+
"attackmap-analyzer-iac>=0.1.0",
|
|
75
76
|
]
|
|
76
77
|
dev = [
|
|
77
78
|
"pytest>=8.0.0",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.2.0"
|
|
@@ -4,15 +4,61 @@ from collections import Counter
|
|
|
4
4
|
|
|
5
5
|
import networkx as nx
|
|
6
6
|
|
|
7
|
-
from .models import AttackSurface, ScanResult
|
|
7
|
+
from .models import AttackSurface, AuthHint, Route, ScanResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Bounded line window used to attribute auth hints to a specific route
|
|
11
|
+
# handler. Chosen so a hint at the top of the handler body still lands
|
|
12
|
+
# on the route even if the handler is 40 lines long. Bigger than a typical
|
|
13
|
+
# handler; smaller than the distance between two adjacent routes. See #41
|
|
14
|
+
# / Bluesky FINDINGS §55 for the noise problem this fixes.
|
|
15
|
+
_ROUTE_AUTH_LINE_WINDOW = 40
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _auth_hints_for_route(
|
|
19
|
+
route: Route,
|
|
20
|
+
scan_auth_hints: list[AuthHint],
|
|
21
|
+
hints_by_file: dict[str, list[AuthHint]],
|
|
22
|
+
) -> list[str]:
|
|
23
|
+
"""Return the auth-hint strings that plausibly attach to `route`.
|
|
24
|
+
|
|
25
|
+
If the route has a known `line`, only hints in the same file whose
|
|
26
|
+
line falls within `_ROUTE_AUTH_LINE_WINDOW` above OR below the
|
|
27
|
+
route are counted. If the route lacks line info (framework
|
|
28
|
+
config-driven routes commonly do), fall back to file-scoped
|
|
29
|
+
attribution — same as the old behavior.
|
|
30
|
+
|
|
31
|
+
Deliberately no global fallback: the "if no hints in this file,
|
|
32
|
+
inherit every auth hint in the repo" behavior was the biggest
|
|
33
|
+
noise source per FINDINGS §55. Downstream code handles empty
|
|
34
|
+
auth_signals correctly.
|
|
35
|
+
"""
|
|
36
|
+
file_hints = hints_by_file.get(route.file, [])
|
|
37
|
+
if not file_hints:
|
|
38
|
+
return []
|
|
39
|
+
if route.line is None:
|
|
40
|
+
return sorted({hint.hint for hint in file_hints})
|
|
41
|
+
lo = route.line - _ROUTE_AUTH_LINE_WINDOW
|
|
42
|
+
hi = route.line + _ROUTE_AUTH_LINE_WINDOW
|
|
43
|
+
windowed = {
|
|
44
|
+
hint.hint
|
|
45
|
+
for hint in file_hints
|
|
46
|
+
if hint.line is None or lo <= hint.line <= hi
|
|
47
|
+
}
|
|
48
|
+
if windowed:
|
|
49
|
+
return sorted(windowed)
|
|
50
|
+
# Hints exist in the file but none are within the window. That's the
|
|
51
|
+
# signal-to-noise fix: the previous route in this file had auth code,
|
|
52
|
+
# this one doesn't. Return empty — the surface's rationale below will
|
|
53
|
+
# say "no auth indicators near this route".
|
|
54
|
+
return []
|
|
8
55
|
|
|
9
56
|
|
|
10
57
|
def identify_attack_surfaces(scan: ScanResult) -> list[AttackSurface]:
|
|
11
58
|
surfaces: list[AttackSurface] = []
|
|
12
|
-
auth_hints_by_file: dict[str,
|
|
59
|
+
auth_hints_by_file: dict[str, list[AuthHint]] = {}
|
|
13
60
|
for hint in scan.auth_hints:
|
|
14
|
-
auth_hints_by_file.setdefault(hint.file,
|
|
15
|
-
global_auth_hints = sorted({hint.hint for hint in scan.auth_hints})
|
|
61
|
+
auth_hints_by_file.setdefault(hint.file, []).append(hint)
|
|
16
62
|
|
|
17
63
|
supporting_hints_by_file: dict[str, set[str]] = {}
|
|
18
64
|
all_supporting_hints = [
|
|
@@ -29,8 +75,7 @@ def identify_attack_surfaces(scan: ScanResult) -> list[AttackSurface]:
|
|
|
29
75
|
|
|
30
76
|
for route in scan.routes:
|
|
31
77
|
path_lower = route.path.lower()
|
|
32
|
-
|
|
33
|
-
auth_signals = file_auth_hints or global_auth_hints
|
|
78
|
+
auth_signals = _auth_hints_for_route(route, scan.auth_hints, auth_hints_by_file)
|
|
34
79
|
supporting_signals = sorted(supporting_hints_by_file.get(route.file, set())) or sorted(
|
|
35
80
|
{hint.hint for hint in all_supporting_hints}
|
|
36
81
|
)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
8
|
+
|
|
9
|
+
from .recon_models import ScanResult
|
|
10
|
+
|
|
11
|
+
# Phase-1 shared repository analyzer contract.
|
|
12
|
+
# Keep AnalyzerResult mapped to ScanResult for backward compatibility.
|
|
13
|
+
AnalyzerResult = ScanResult
|
|
14
|
+
|
|
15
|
+
# `name` must be a slug: lowercase letters, digits, hyphens. This is the
|
|
16
|
+
# entry-point key AttackMap uses to identify the analyzer, so it needs
|
|
17
|
+
# to be safe in URLs, filesystem paths, and CLI arguments.
|
|
18
|
+
_ANALYZER_NAME_PATTERN = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AnalyzerMetadata(BaseModel):
|
|
22
|
+
"""Static, self-describing metadata every analyzer exposes.
|
|
23
|
+
|
|
24
|
+
Every analyzer plugin ships a `metadata: AnalyzerMetadata` attribute so
|
|
25
|
+
core (and future tooling) can reason about the plugin without loading
|
|
26
|
+
or running it. The field set is stable: adding fields is safe on a
|
|
27
|
+
minor release, removing or renaming is a major-version break.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
name: str = Field(
|
|
31
|
+
description="Slug identifier. Must match /[a-z0-9][a-z0-9-]*/. "
|
|
32
|
+
"This is the entry-point key AttackMap uses to select the analyzer "
|
|
33
|
+
"(e.g. from --module) and to attribute signals via provenance.",
|
|
34
|
+
)
|
|
35
|
+
display_name: str = Field(
|
|
36
|
+
default="",
|
|
37
|
+
description="Human-friendly name shown in `attackmap modules` output "
|
|
38
|
+
"and reports. Defaults to `name` when omitted.",
|
|
39
|
+
)
|
|
40
|
+
version: str = Field(
|
|
41
|
+
default="0.1.0",
|
|
42
|
+
description="Semver of the analyzer plugin itself (not attackmap). "
|
|
43
|
+
"Should match the analyzer package's pyproject.toml version.",
|
|
44
|
+
)
|
|
45
|
+
description: str = Field(
|
|
46
|
+
default="",
|
|
47
|
+
description="One-sentence summary of what the analyzer detects. "
|
|
48
|
+
"Shown in module listings and matter briefings.",
|
|
49
|
+
)
|
|
50
|
+
scope: str = Field(
|
|
51
|
+
default="",
|
|
52
|
+
description="One-sentence description of the kinds of repositories "
|
|
53
|
+
"this analyzer targets (e.g. 'Node/TypeScript backend service repos').",
|
|
54
|
+
)
|
|
55
|
+
targets: list[str] = Field(
|
|
56
|
+
default_factory=list,
|
|
57
|
+
description="Frameworks, protocols, or platform tokens this analyzer "
|
|
58
|
+
"specializes in (e.g. ['react-native', 'expo']). Used with `languages` "
|
|
59
|
+
"to compute `ecosystems`.",
|
|
60
|
+
)
|
|
61
|
+
languages: list[str] = Field(
|
|
62
|
+
default_factory=list,
|
|
63
|
+
description="Programming languages this analyzer parses (e.g. "
|
|
64
|
+
"['javascript', 'typescript']). Used with `targets` to compute "
|
|
65
|
+
"`ecosystems`.",
|
|
66
|
+
)
|
|
67
|
+
priority: int = Field(
|
|
68
|
+
default=100,
|
|
69
|
+
ge=0,
|
|
70
|
+
description="Discovery ordering hint. Lower runs first when multiple "
|
|
71
|
+
"analyzers match. Ties are broken by entry-point name. Convention: "
|
|
72
|
+
"0-49 is broad language analyzers (python, node-service); 50-149 is "
|
|
73
|
+
"framework analyzers (spring, laminas); 150+ is app-specific "
|
|
74
|
+
"analyzers (omeka-s).",
|
|
75
|
+
)
|
|
76
|
+
experimental: bool = Field(
|
|
77
|
+
default=True,
|
|
78
|
+
description="Whether this analyzer is still stabilizing. Doesn't "
|
|
79
|
+
"affect execution, just how core presents it in listings.",
|
|
80
|
+
)
|
|
81
|
+
enabled_by_default: bool = Field(
|
|
82
|
+
default=False,
|
|
83
|
+
description="Whether to run this analyzer without an explicit "
|
|
84
|
+
"--module flag. Broad language analyzers set this True; specialty "
|
|
85
|
+
"framework/app analyzers usually leave it False.",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@field_validator("name")
|
|
89
|
+
@classmethod
|
|
90
|
+
def _validate_name(cls, value: str) -> str:
|
|
91
|
+
if not value:
|
|
92
|
+
raise ValueError("AnalyzerMetadata.name must not be empty")
|
|
93
|
+
if not _ANALYZER_NAME_PATTERN.match(value):
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"AnalyzerMetadata.name {value!r} must match "
|
|
96
|
+
f"/{_ANALYZER_NAME_PATTERN.pattern}/ — lowercase letters, "
|
|
97
|
+
f"digits, and hyphens only, starting with a letter or digit."
|
|
98
|
+
)
|
|
99
|
+
return value
|
|
100
|
+
|
|
101
|
+
@field_validator("version")
|
|
102
|
+
@classmethod
|
|
103
|
+
def _validate_version(cls, value: str) -> str:
|
|
104
|
+
if not value:
|
|
105
|
+
raise ValueError("AnalyzerMetadata.version must not be empty")
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
@model_validator(mode="before")
|
|
109
|
+
@classmethod
|
|
110
|
+
def _coerce_legacy_ecosystems(cls, value: object) -> object:
|
|
111
|
+
if not isinstance(value, dict):
|
|
112
|
+
return value
|
|
113
|
+
payload = dict(value)
|
|
114
|
+
# Backward-compat: older call sites may pass ecosystems directly.
|
|
115
|
+
# Use it as a fallback source for languages when newer fields are omitted.
|
|
116
|
+
ecosystems = payload.pop("ecosystems", None)
|
|
117
|
+
if ecosystems and not payload.get("languages") and not payload.get("targets"):
|
|
118
|
+
payload["languages"] = list(ecosystems)
|
|
119
|
+
if not payload.get("display_name") and payload.get("name"):
|
|
120
|
+
payload["display_name"] = str(payload["name"])
|
|
121
|
+
return payload
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def ecosystems(self) -> tuple[str, ...]:
|
|
125
|
+
"""Deduplicated tuple of languages + targets, lowercased.
|
|
126
|
+
|
|
127
|
+
Convenience view for callers that don't care about the language vs
|
|
128
|
+
target distinction (e.g. picking analyzers by ecosystem token).
|
|
129
|
+
"""
|
|
130
|
+
values = [*self.languages, *self.targets]
|
|
131
|
+
seen: set[str] = set()
|
|
132
|
+
ordered: list[str] = []
|
|
133
|
+
for value in values:
|
|
134
|
+
lowered = value.lower()
|
|
135
|
+
if lowered in seen:
|
|
136
|
+
continue
|
|
137
|
+
seen.add(lowered)
|
|
138
|
+
ordered.append(lowered)
|
|
139
|
+
return tuple(ordered)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class AnalyzerRepositoryModule(BaseModel):
|
|
143
|
+
analyzer_name: str
|
|
144
|
+
repo_name: str
|
|
145
|
+
web_url: str
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class AnalyzerProtocol(Protocol):
|
|
149
|
+
metadata: AnalyzerMetadata
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def name(self) -> str: ...
|
|
153
|
+
|
|
154
|
+
def detect(self, root: str | Path) -> bool: ...
|
|
155
|
+
|
|
156
|
+
def analyze(self, root: str | Path) -> AnalyzerResult: ...
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
__all__ = [
|
|
160
|
+
"AnalyzerResult",
|
|
161
|
+
"AnalyzerMetadata",
|
|
162
|
+
"AnalyzerRepositoryModule",
|
|
163
|
+
"AnalyzerProtocol",
|
|
164
|
+
"normalize_analyzer_metadata",
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def normalize_analyzer_metadata(value: object) -> AnalyzerMetadata:
|
|
169
|
+
if isinstance(value, AnalyzerMetadata):
|
|
170
|
+
return value
|
|
171
|
+
if isinstance(value, dict):
|
|
172
|
+
return AnalyzerMetadata.model_validate(value)
|
|
173
|
+
|
|
174
|
+
payload: dict[str, object] = {}
|
|
175
|
+
for key in (
|
|
176
|
+
"name",
|
|
177
|
+
"display_name",
|
|
178
|
+
"version",
|
|
179
|
+
"description",
|
|
180
|
+
"scope",
|
|
181
|
+
"targets",
|
|
182
|
+
"languages",
|
|
183
|
+
"priority",
|
|
184
|
+
"experimental",
|
|
185
|
+
"enabled_by_default",
|
|
186
|
+
"ecosystems",
|
|
187
|
+
):
|
|
188
|
+
if hasattr(value, key):
|
|
189
|
+
payload[key] = getattr(value, key)
|
|
190
|
+
return AnalyzerMetadata.model_validate(payload)
|