attackmap 0.4.15__tar.gz → 0.4.25__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.
Potentially problematic release.
This version of attackmap might be problematic. Click here for more details.
- {attackmap-0.4.15/src/attackmap.egg-info → attackmap-0.4.25}/PKG-INFO +84 -3
- {attackmap-0.4.15 → attackmap-0.4.25}/README.md +83 -2
- {attackmap-0.4.15 → attackmap-0.4.25}/pyproject.toml +1 -1
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/__init__.py +1 -1
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/analyzers.py +42 -19
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/anomalies.py +241 -9
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/authz.py +4 -1
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/cli.py +327 -45
- attackmap-0.4.25/src/attackmap/contracts.py +201 -0
- attackmap-0.4.25/src/attackmap/crossrepo.py +351 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/exploitability.py +3 -1
- attackmap-0.4.25/src/attackmap/fleet.py +358 -0
- attackmap-0.4.25/src/attackmap/hunt_harness.py +451 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/llm_review.py +35 -8
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/merge.py +4 -1
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/models.py +28 -1
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/review_prompts.py +172 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/scanner.py +36 -6
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/taint.py +191 -20
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/threat_model.py +63 -2
- {attackmap-0.4.15 → attackmap-0.4.25/src/attackmap.egg-info}/PKG-INFO +84 -3
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap.egg-info/SOURCES.txt +9 -0
- attackmap-0.4.25/tests/test_anomalies.py +663 -0
- attackmap-0.4.25/tests/test_contracts.py +194 -0
- attackmap-0.4.25/tests/test_crossrepo.py +328 -0
- attackmap-0.4.25/tests/test_fleet.py +319 -0
- attackmap-0.4.25/tests/test_hunt_harness.py +558 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_merge.py +11 -5
- attackmap-0.4.25/tests/test_recall.py +310 -0
- attackmap-0.4.15/tests/test_anomalies.py +0 -397
- {attackmap-0.4.15 → attackmap-0.4.25}/LICENSE +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/setup.cfg +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/analyzer.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/analyzer_contracts.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/asset_model.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/attack_taxonomy.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/config_scanner.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/context_pack.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/control_model.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/crypto.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/cve.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/defensive_review.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/detection_opportunities.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/diagrams.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/diff.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/graph.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/insights.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/lockfiles.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/progress.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/recon_models.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/recon_to_analysis.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/report.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/review_eval.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/review_json.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/route_auth_fusion.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/sarif.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/sbom.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/sdk/__init__.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/sdk/contracts.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/sdk/models.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/security_overlay.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/srcpaths.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/suggest.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/suppress.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/topology.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/triage.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/weaknesses.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/webhardening.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/workflow_scanner.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap.egg-info/dependency_links.txt +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap.egg-info/entry_points.txt +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap.egg-info/requires.txt +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap.egg-info/top_level.txt +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_analyzer.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_analyzer_metadata_schema.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_analyzers.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_artifact_schemas.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_authz.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_config_scanner.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_context_pack.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_crypto.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_cve.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_defensive_review.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_diagrams.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_diff.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_exploitability.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_graph.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_hunt.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_injection_sinks.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_llm_review.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_lockfiles.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_modules_cli.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_pr_comment.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_progress.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_provenance.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_recon_to_analysis.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_remediate.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_report.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_review_eval.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_review_json.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_review_prompts.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_route_auth_fusion.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_sarif.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_sbom.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_scanner.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_security_overlay.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_shared_contract_imports.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_signal_v2.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_srcpaths.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_suggest.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_suppress.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_taint.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_taint_sanitizer.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_threat_intel.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_threat_model.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_topology.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_triage.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_weaknesses.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_webhardening.py +0 -0
- {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_workflow_scanner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: attackmap
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.25
|
|
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>
|
|
@@ -77,7 +77,7 @@ managers who need to triage an unfamiliar codebase.
|
|
|
77
77
|
[](https://www.python.org/)
|
|
78
78
|
[](https://pypi.org/project/attackmap/)
|
|
79
79
|
|
|
80
|
-
> **Status: beta (v0.4.
|
|
80
|
+
> **Status: beta (v0.4.18).** Core engine and 14 analyzer plugins are published
|
|
81
81
|
> to PyPI, Homebrew, and GHCR and validated against real-world codebases.
|
|
82
82
|
> AttackMap is heuristic by design — findings are confidence-tiered evidence,
|
|
83
83
|
> not proof. See [Project status](#project-status) for what's solid and what's
|
|
@@ -128,6 +128,31 @@ model ID verbatim.
|
|
|
128
128
|
Read `reports/defensive-review.md` (heuristic) and `reports/defensive-review-llm.md`
|
|
129
129
|
(LLM-narrated) side by side.
|
|
130
130
|
|
|
131
|
+
**Multi-repo (fleet) scan.** Pass two or more repositories to scan a whole fleet
|
|
132
|
+
in one run:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
attackmap analyze ./service-a ./service-b ./gateway --output reports
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Each repo is scanned independently into its own `reports/<repo>/` directory, and
|
|
139
|
+
a `reports/fleet-summary.md` (+ `.json`) indexes the run with per-repo severity
|
|
140
|
+
counts and top findings. AttackMap then **links the seams**: it matches one
|
|
141
|
+
repo's outbound HTTP calls to another repo's routes (by normalized path template
|
|
142
|
+
+ method — `base + "/api/orders/" + id` in a caller aligns with `/api/orders/{id}`
|
|
143
|
+
served elsewhere), lists each client→server link (citing both sides) in the fleet
|
|
144
|
+
summary, and renders the repo-to-repo graph to `reports/fleet-graph.md`.
|
|
145
|
+
|
|
146
|
+
Over those links it then flags **cross-boundary trust** (confused-deputy) risks:
|
|
147
|
+
a value one repo forwards across a link that the callee trusts into a dangerous
|
|
148
|
+
sink or an unguarded object access — each repo looks locally fine; the bug is in
|
|
149
|
+
the seam. These are marked **speculative** (leads to adjudicate, citing both the
|
|
150
|
+
caller and callee sites) and listed in the fleet summary. It also flags
|
|
151
|
+
**trust-assumption gaps** (a state-changing call crossing a link to a route the
|
|
152
|
+
callee serves with no auth — if each side assumes the other enforces, nobody
|
|
153
|
+
does) and **cross-repo anomalies** (the one service that omits an auth control
|
|
154
|
+
its siblings enforce on the same route). Single-repo behavior is unchanged.
|
|
155
|
+
|
|
131
156
|
---
|
|
132
157
|
|
|
133
158
|
## Install
|
|
@@ -384,6 +409,27 @@ excluded from all the heuristic passes above by default, since dangerous
|
|
|
384
409
|
patterns in test scaffolding are rarely real exposure. Set
|
|
385
410
|
`ATTACKMAP_INCLUDE_TESTS=1` to scan them too (e.g. for test-quality reviews).
|
|
386
411
|
|
|
412
|
+
**Recall mode (`--recall`).** Aggressiveness is only useful *behind* a verifier —
|
|
413
|
+
so `--recall` widens taint discovery and marks the extra reach **speculative**
|
|
414
|
+
instead of asserting it. It:
|
|
415
|
+
|
|
416
|
+
- raises the max import-hop depth (2 → 4) and per-route visit budget;
|
|
417
|
+
- surfaces reaches the default pass conservatively hides, like a
|
|
418
|
+
static-literal-looking `eval`/`exec`/deserialize argument;
|
|
419
|
+
- **enumerates capability-reach**: every reach to a powerful capability —
|
|
420
|
+
network (`requests`/`httpx`/`axios`/`fetch`/`urlopen`), template
|
|
421
|
+
(`render_template_string`/`Template`), filesystem (`open`), redirect
|
|
422
|
+
(`redirect`/`res.redirect`) — even with **no known-bad pattern**, i.e. the
|
|
423
|
+
bare call without a request-derived argument (a genuine request-derived sink
|
|
424
|
+
still fires once, as a confirmed finding, not a duplicate).
|
|
425
|
+
|
|
426
|
+
Speculative chains carry downgraded confidence, get their own LOW-severity
|
|
427
|
+
finding (`SPECULATIVE (recall mode)…`), are excluded from every asserted
|
|
428
|
+
downstream pass (exploitability, BOLA, attack paths), and are kept **out of
|
|
429
|
+
`--fail-on-new-high`** — they are leads for `--hunt --verify` / `--triage` to
|
|
430
|
+
adjudicate, not detections. Default behavior is unchanged; pair `--recall` with
|
|
431
|
+
`--hunt --verify` to widen the net and then adjudicate what it catches.
|
|
432
|
+
|
|
387
433
|
### Web hardening gaps
|
|
388
434
|
|
|
389
435
|
Route- and config-level checks for common web misconfigurations, each an
|
|
@@ -516,6 +562,20 @@ within each cohort the odd-one-out is reported:
|
|
|
516
562
|
| `auth_outlier` | siblings carry an auth/authorization signal near the handler; this route doesn't | HIGH |
|
|
517
563
|
| `validation_outlier` | among a cohort's state-changing handlers, peers validate input and this one shows no validation marker | LOW |
|
|
518
564
|
| `method_outlier` | a lone state-changing method in an otherwise read-only cohort | MEDIUM |
|
|
565
|
+
| `invariant_violation` | a mined-invariant outlier: among handlers that reach the same dangerous sink kind, the majority guard the request *before* the sink and this one doesn't — evidence cites the mined rule | HIGH |
|
|
566
|
+
|
|
567
|
+
The `invariant_violation` kind is the **mined-invariant** pass (#149a): its
|
|
568
|
+
cohort is not a route-path prefix but the set of handlers that reach the same
|
|
569
|
+
dangerous sink (from the taint pass). When a strong majority guard the request
|
|
570
|
+
before the sink, that pattern is mined as an implicit invariant and the handler
|
|
571
|
+
that reaches the same sink kind with no preceding guard is flagged, citing the
|
|
572
|
+
mined rule (e.g. "9 of 10 handlers that reach a database (SQL execute) sink
|
|
573
|
+
apply an auth/validation guard before it"). It reasons only about **same-file**
|
|
574
|
+
flows, where guard-before-sink ordering is actually verifiable (a cross-file
|
|
575
|
+
sink could be called before an `authorize(...)` that textually follows it), and
|
|
576
|
+
counts one handler per declaration regardless of how many verbs it exposes.
|
|
577
|
+
Sanitized chains are excluded so only genuinely undefended flows form the cohort
|
|
578
|
+
— it measures the code against its own norm and needs no signature.
|
|
519
579
|
|
|
520
580
|
Everything is peer-relative and **confidence scales with how consistent the
|
|
521
581
|
cohort is** — a lone deviation among many agreeing siblings is likelier a mistake
|
|
@@ -664,6 +724,24 @@ to confirm, not detections.** The same grounding contract as the review applies
|
|
|
664
724
|
assignment, no exploit code, confidence-tiered, and each lead lists exactly what
|
|
665
725
|
a human must verify. Uses the same auth/backend resolution as `--llm`.
|
|
666
726
|
|
|
727
|
+
With `--verify`, each hypothesis is adjudicated against the actual source at its
|
|
728
|
+
cited locations. **`--verify-votes N`** (default 3) turns that into a jury: **N
|
|
729
|
+
independent skeptics** each adjudicate the same fixed hypothesis list. A lead is
|
|
730
|
+
**CONFIRMED only on a strict majority** (NEEDS_REVIEW only when a majority flags
|
|
731
|
+
the evidence as insufficient); ties and uncertainty are **REFUTED** — so a lead
|
|
732
|
+
only one skeptic would confirm is dropped. **`--hunt-lenses N`** additionally
|
|
733
|
+
fans generation out into N independent passes, each specialised in one failure
|
|
734
|
+
mode (auth-bypass, TOCTOU/race, IDOR, deserialization, SSRF-to-internal,
|
|
735
|
+
secret-misuse), and dedupes the leads across passes before verifying — more
|
|
736
|
+
unique novel leads than a single generalist pass. **`--hunt-rounds N`** loops
|
|
737
|
+
generation for up to N rounds, accumulating new (deduped) leads while a
|
|
738
|
+
completeness critic seeds each next round with untried angles, and stops early
|
|
739
|
+
once a round finds nothing new; **`--hunt-budget T`** caps the multi-round output
|
|
740
|
+
tokens.
|
|
741
|
+
This is the verifier the unknown-bug initiative
|
|
742
|
+
([`docs/unknown-bug-epic.md`](docs/unknown-bug-epic.md)) is built on;
|
|
743
|
+
`--verify-votes 1` is the classic single pass.
|
|
744
|
+
|
|
667
745
|
Layered on top: **MITRE ATT&CK technique mappings** on every insight and
|
|
668
746
|
**detection opportunities** (Sigma/KQL/Splunk-style hints) for each weakness.
|
|
669
747
|
|
|
@@ -722,6 +800,9 @@ attackmap analyze <path> --llm --llm-provider openai # use OpenAI/Codex (OPE
|
|
|
722
800
|
attackmap analyze <path> --llm --llm-provider openai --llm-model gpt-5.5 # any OpenAI model ID, passed through
|
|
723
801
|
attackmap analyze <path> --hunt # LLM vulnerability-hypothesis hunt (leads to confirm)
|
|
724
802
|
attackmap analyze <path> --hunt --verify # adjudicate each lead vs. source (confirmed/refuted/review)
|
|
803
|
+
attackmap analyze <path> --hunt --verify --verify-votes 3 # majority vote of N independent skeptics (default 3; 1 = single pass)
|
|
804
|
+
attackmap analyze <path> --hunt --verify --hunt-lenses 4 # N lens-specialised generation passes, deduped, then verified
|
|
805
|
+
attackmap analyze <path> --hunt --verify --hunt-rounds 4 # loop-until-dry generation (completeness critic seeds each round)
|
|
725
806
|
attackmap analyze <path> --remediate # LLM review-first fix suggestions (remediation.md)
|
|
726
807
|
attackmap analyze <path> --triage # cluster/dedupe/rank existing findings (triage.md; deterministic fallback)
|
|
727
808
|
attackmap analyze <path> --pr-comment pr.md # Markdown PR summary comment for CI
|
|
@@ -768,7 +849,7 @@ introduces a new HIGH finding.
|
|
|
768
849
|
|
|
769
850
|
## Project status
|
|
770
851
|
|
|
771
|
-
AttackMap is **beta** (v0.4.
|
|
852
|
+
AttackMap is **beta** (v0.4.18) — published and validated on real codebases, but
|
|
772
853
|
pre-1.0 and heuristic.
|
|
773
854
|
|
|
774
855
|
**Solid today:**
|
|
@@ -16,7 +16,7 @@ managers who need to triage an unfamiliar codebase.
|
|
|
16
16
|
[](https://www.python.org/)
|
|
17
17
|
[](https://pypi.org/project/attackmap/)
|
|
18
18
|
|
|
19
|
-
> **Status: beta (v0.4.
|
|
19
|
+
> **Status: beta (v0.4.18).** Core engine and 14 analyzer plugins are published
|
|
20
20
|
> to PyPI, Homebrew, and GHCR and validated against real-world codebases.
|
|
21
21
|
> AttackMap is heuristic by design — findings are confidence-tiered evidence,
|
|
22
22
|
> not proof. See [Project status](#project-status) for what's solid and what's
|
|
@@ -67,6 +67,31 @@ model ID verbatim.
|
|
|
67
67
|
Read `reports/defensive-review.md` (heuristic) and `reports/defensive-review-llm.md`
|
|
68
68
|
(LLM-narrated) side by side.
|
|
69
69
|
|
|
70
|
+
**Multi-repo (fleet) scan.** Pass two or more repositories to scan a whole fleet
|
|
71
|
+
in one run:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
attackmap analyze ./service-a ./service-b ./gateway --output reports
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Each repo is scanned independently into its own `reports/<repo>/` directory, and
|
|
78
|
+
a `reports/fleet-summary.md` (+ `.json`) indexes the run with per-repo severity
|
|
79
|
+
counts and top findings. AttackMap then **links the seams**: it matches one
|
|
80
|
+
repo's outbound HTTP calls to another repo's routes (by normalized path template
|
|
81
|
+
+ method — `base + "/api/orders/" + id` in a caller aligns with `/api/orders/{id}`
|
|
82
|
+
served elsewhere), lists each client→server link (citing both sides) in the fleet
|
|
83
|
+
summary, and renders the repo-to-repo graph to `reports/fleet-graph.md`.
|
|
84
|
+
|
|
85
|
+
Over those links it then flags **cross-boundary trust** (confused-deputy) risks:
|
|
86
|
+
a value one repo forwards across a link that the callee trusts into a dangerous
|
|
87
|
+
sink or an unguarded object access — each repo looks locally fine; the bug is in
|
|
88
|
+
the seam. These are marked **speculative** (leads to adjudicate, citing both the
|
|
89
|
+
caller and callee sites) and listed in the fleet summary. It also flags
|
|
90
|
+
**trust-assumption gaps** (a state-changing call crossing a link to a route the
|
|
91
|
+
callee serves with no auth — if each side assumes the other enforces, nobody
|
|
92
|
+
does) and **cross-repo anomalies** (the one service that omits an auth control
|
|
93
|
+
its siblings enforce on the same route). Single-repo behavior is unchanged.
|
|
94
|
+
|
|
70
95
|
---
|
|
71
96
|
|
|
72
97
|
## Install
|
|
@@ -323,6 +348,27 @@ excluded from all the heuristic passes above by default, since dangerous
|
|
|
323
348
|
patterns in test scaffolding are rarely real exposure. Set
|
|
324
349
|
`ATTACKMAP_INCLUDE_TESTS=1` to scan them too (e.g. for test-quality reviews).
|
|
325
350
|
|
|
351
|
+
**Recall mode (`--recall`).** Aggressiveness is only useful *behind* a verifier —
|
|
352
|
+
so `--recall` widens taint discovery and marks the extra reach **speculative**
|
|
353
|
+
instead of asserting it. It:
|
|
354
|
+
|
|
355
|
+
- raises the max import-hop depth (2 → 4) and per-route visit budget;
|
|
356
|
+
- surfaces reaches the default pass conservatively hides, like a
|
|
357
|
+
static-literal-looking `eval`/`exec`/deserialize argument;
|
|
358
|
+
- **enumerates capability-reach**: every reach to a powerful capability —
|
|
359
|
+
network (`requests`/`httpx`/`axios`/`fetch`/`urlopen`), template
|
|
360
|
+
(`render_template_string`/`Template`), filesystem (`open`), redirect
|
|
361
|
+
(`redirect`/`res.redirect`) — even with **no known-bad pattern**, i.e. the
|
|
362
|
+
bare call without a request-derived argument (a genuine request-derived sink
|
|
363
|
+
still fires once, as a confirmed finding, not a duplicate).
|
|
364
|
+
|
|
365
|
+
Speculative chains carry downgraded confidence, get their own LOW-severity
|
|
366
|
+
finding (`SPECULATIVE (recall mode)…`), are excluded from every asserted
|
|
367
|
+
downstream pass (exploitability, BOLA, attack paths), and are kept **out of
|
|
368
|
+
`--fail-on-new-high`** — they are leads for `--hunt --verify` / `--triage` to
|
|
369
|
+
adjudicate, not detections. Default behavior is unchanged; pair `--recall` with
|
|
370
|
+
`--hunt --verify` to widen the net and then adjudicate what it catches.
|
|
371
|
+
|
|
326
372
|
### Web hardening gaps
|
|
327
373
|
|
|
328
374
|
Route- and config-level checks for common web misconfigurations, each an
|
|
@@ -455,6 +501,20 @@ within each cohort the odd-one-out is reported:
|
|
|
455
501
|
| `auth_outlier` | siblings carry an auth/authorization signal near the handler; this route doesn't | HIGH |
|
|
456
502
|
| `validation_outlier` | among a cohort's state-changing handlers, peers validate input and this one shows no validation marker | LOW |
|
|
457
503
|
| `method_outlier` | a lone state-changing method in an otherwise read-only cohort | MEDIUM |
|
|
504
|
+
| `invariant_violation` | a mined-invariant outlier: among handlers that reach the same dangerous sink kind, the majority guard the request *before* the sink and this one doesn't — evidence cites the mined rule | HIGH |
|
|
505
|
+
|
|
506
|
+
The `invariant_violation` kind is the **mined-invariant** pass (#149a): its
|
|
507
|
+
cohort is not a route-path prefix but the set of handlers that reach the same
|
|
508
|
+
dangerous sink (from the taint pass). When a strong majority guard the request
|
|
509
|
+
before the sink, that pattern is mined as an implicit invariant and the handler
|
|
510
|
+
that reaches the same sink kind with no preceding guard is flagged, citing the
|
|
511
|
+
mined rule (e.g. "9 of 10 handlers that reach a database (SQL execute) sink
|
|
512
|
+
apply an auth/validation guard before it"). It reasons only about **same-file**
|
|
513
|
+
flows, where guard-before-sink ordering is actually verifiable (a cross-file
|
|
514
|
+
sink could be called before an `authorize(...)` that textually follows it), and
|
|
515
|
+
counts one handler per declaration regardless of how many verbs it exposes.
|
|
516
|
+
Sanitized chains are excluded so only genuinely undefended flows form the cohort
|
|
517
|
+
— it measures the code against its own norm and needs no signature.
|
|
458
518
|
|
|
459
519
|
Everything is peer-relative and **confidence scales with how consistent the
|
|
460
520
|
cohort is** — a lone deviation among many agreeing siblings is likelier a mistake
|
|
@@ -603,6 +663,24 @@ to confirm, not detections.** The same grounding contract as the review applies
|
|
|
603
663
|
assignment, no exploit code, confidence-tiered, and each lead lists exactly what
|
|
604
664
|
a human must verify. Uses the same auth/backend resolution as `--llm`.
|
|
605
665
|
|
|
666
|
+
With `--verify`, each hypothesis is adjudicated against the actual source at its
|
|
667
|
+
cited locations. **`--verify-votes N`** (default 3) turns that into a jury: **N
|
|
668
|
+
independent skeptics** each adjudicate the same fixed hypothesis list. A lead is
|
|
669
|
+
**CONFIRMED only on a strict majority** (NEEDS_REVIEW only when a majority flags
|
|
670
|
+
the evidence as insufficient); ties and uncertainty are **REFUTED** — so a lead
|
|
671
|
+
only one skeptic would confirm is dropped. **`--hunt-lenses N`** additionally
|
|
672
|
+
fans generation out into N independent passes, each specialised in one failure
|
|
673
|
+
mode (auth-bypass, TOCTOU/race, IDOR, deserialization, SSRF-to-internal,
|
|
674
|
+
secret-misuse), and dedupes the leads across passes before verifying — more
|
|
675
|
+
unique novel leads than a single generalist pass. **`--hunt-rounds N`** loops
|
|
676
|
+
generation for up to N rounds, accumulating new (deduped) leads while a
|
|
677
|
+
completeness critic seeds each next round with untried angles, and stops early
|
|
678
|
+
once a round finds nothing new; **`--hunt-budget T`** caps the multi-round output
|
|
679
|
+
tokens.
|
|
680
|
+
This is the verifier the unknown-bug initiative
|
|
681
|
+
([`docs/unknown-bug-epic.md`](docs/unknown-bug-epic.md)) is built on;
|
|
682
|
+
`--verify-votes 1` is the classic single pass.
|
|
683
|
+
|
|
606
684
|
Layered on top: **MITRE ATT&CK technique mappings** on every insight and
|
|
607
685
|
**detection opportunities** (Sigma/KQL/Splunk-style hints) for each weakness.
|
|
608
686
|
|
|
@@ -661,6 +739,9 @@ attackmap analyze <path> --llm --llm-provider openai # use OpenAI/Codex (OPE
|
|
|
661
739
|
attackmap analyze <path> --llm --llm-provider openai --llm-model gpt-5.5 # any OpenAI model ID, passed through
|
|
662
740
|
attackmap analyze <path> --hunt # LLM vulnerability-hypothesis hunt (leads to confirm)
|
|
663
741
|
attackmap analyze <path> --hunt --verify # adjudicate each lead vs. source (confirmed/refuted/review)
|
|
742
|
+
attackmap analyze <path> --hunt --verify --verify-votes 3 # majority vote of N independent skeptics (default 3; 1 = single pass)
|
|
743
|
+
attackmap analyze <path> --hunt --verify --hunt-lenses 4 # N lens-specialised generation passes, deduped, then verified
|
|
744
|
+
attackmap analyze <path> --hunt --verify --hunt-rounds 4 # loop-until-dry generation (completeness critic seeds each round)
|
|
664
745
|
attackmap analyze <path> --remediate # LLM review-first fix suggestions (remediation.md)
|
|
665
746
|
attackmap analyze <path> --triage # cluster/dedupe/rank existing findings (triage.md; deterministic fallback)
|
|
666
747
|
attackmap analyze <path> --pr-comment pr.md # Markdown PR summary comment for CI
|
|
@@ -707,7 +788,7 @@ introduces a new HIGH finding.
|
|
|
707
788
|
|
|
708
789
|
## Project status
|
|
709
790
|
|
|
710
|
-
AttackMap is **beta** (v0.4.
|
|
791
|
+
AttackMap is **beta** (v0.4.18) — published and validated on real codebases, but
|
|
711
792
|
pre-1.0 and heuristic.
|
|
712
793
|
|
|
713
794
|
**Solid today:**
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "attackmap"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.25"
|
|
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"
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.4.
|
|
2
|
+
__version__ = "0.4.25"
|
|
@@ -47,6 +47,7 @@ from .scanner import (
|
|
|
47
47
|
DB_KEYWORDS,
|
|
48
48
|
DB_PATTERNS,
|
|
49
49
|
EXTERNAL_CALL_PATTERNS,
|
|
50
|
+
_external_call_fields,
|
|
50
51
|
SECRET_PATTERNS,
|
|
51
52
|
extract_routes,
|
|
52
53
|
scan_repo,
|
|
@@ -104,7 +105,10 @@ class ExternalCallAnalyzer:
|
|
|
104
105
|
calls: list[ExternalCall] = []
|
|
105
106
|
for pattern in EXTERNAL_CALL_PATTERNS:
|
|
106
107
|
for match in pattern.finditer(context.content):
|
|
107
|
-
|
|
108
|
+
target, method = _external_call_fields(match)
|
|
109
|
+
calls.append(
|
|
110
|
+
ExternalCall(target=target, method=method, file=context.relative_path)
|
|
111
|
+
)
|
|
108
112
|
return AnalyzerSignals(external_calls=calls)
|
|
109
113
|
|
|
110
114
|
|
|
@@ -225,9 +229,14 @@ class DefaultAnalyzer:
|
|
|
225
229
|
def name(self) -> str:
|
|
226
230
|
return self.metadata.name
|
|
227
231
|
|
|
228
|
-
def analyze(
|
|
232
|
+
def analyze(
|
|
233
|
+
self, root: str | Path, progress: "ScanProgress | None" = None, recall: bool = False
|
|
234
|
+
) -> AnalyzerResult:
|
|
229
235
|
return scan_repo(
|
|
230
|
-
root,
|
|
236
|
+
root,
|
|
237
|
+
suffixes=set(CODE_EXTENSIONS) - self._CLAIMED_SUFFIXES,
|
|
238
|
+
progress=progress,
|
|
239
|
+
recall=recall,
|
|
231
240
|
)
|
|
232
241
|
|
|
233
242
|
|
|
@@ -249,8 +258,10 @@ class BuiltinPythonWebAnalyzer:
|
|
|
249
258
|
def name(self) -> str:
|
|
250
259
|
return self.metadata.name
|
|
251
260
|
|
|
252
|
-
def analyze(
|
|
253
|
-
|
|
261
|
+
def analyze(
|
|
262
|
+
self, root: str | Path, progress: "ScanProgress | None" = None, recall: bool = False
|
|
263
|
+
) -> AnalyzerResult:
|
|
264
|
+
return scan_repo(root, suffixes={".py"}, progress=progress, recall=recall)
|
|
254
265
|
|
|
255
266
|
|
|
256
267
|
class BuiltinJavaScriptWebAnalyzer:
|
|
@@ -323,8 +334,10 @@ class BuiltinJavaScriptWebAnalyzer:
|
|
|
323
334
|
return False
|
|
324
335
|
return False
|
|
325
336
|
|
|
326
|
-
def analyze(
|
|
327
|
-
|
|
337
|
+
def analyze(
|
|
338
|
+
self, root: str | Path, progress: "ScanProgress | None" = None, recall: bool = False
|
|
339
|
+
) -> AnalyzerResult:
|
|
340
|
+
return scan_repo(root, suffixes=self._JS_SUFFIXES, progress=progress, recall=recall)
|
|
328
341
|
|
|
329
342
|
|
|
330
343
|
class BuiltinConfigAnalyzer:
|
|
@@ -604,12 +617,13 @@ def analyze_repository(
|
|
|
604
617
|
root: str | Path,
|
|
605
618
|
analyzers: Iterable[Analyzer] | None = None,
|
|
606
619
|
progress: "ScanProgress | None" = None,
|
|
620
|
+
recall: bool = False,
|
|
607
621
|
) -> AnalyzerResult:
|
|
608
622
|
repo_root = Path(root).resolve()
|
|
609
623
|
active_analyzers = resolve_run_analyzers(repo_root, analyzers=analyzers)
|
|
610
624
|
results: list[AnalyzerResult] = []
|
|
611
625
|
for analyzer in active_analyzers:
|
|
612
|
-
result = _call_analyze(analyzer, repo_root, progress)
|
|
626
|
+
result = _call_analyze(analyzer, repo_root, progress, recall)
|
|
613
627
|
_stamp_provenance(result, analyzer.name)
|
|
614
628
|
results.append(result)
|
|
615
629
|
if not results:
|
|
@@ -617,17 +631,26 @@ def analyze_repository(
|
|
|
617
631
|
return merge_analyzer_results(results, root=repo_root)
|
|
618
632
|
|
|
619
633
|
|
|
620
|
-
def _call_analyze(
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
634
|
+
def _call_analyze(
|
|
635
|
+
analyzer: Analyzer,
|
|
636
|
+
root: Path,
|
|
637
|
+
progress: "ScanProgress | None",
|
|
638
|
+
recall: bool = False,
|
|
639
|
+
) -> AnalyzerResult:
|
|
640
|
+
"""Invoke ``analyzer.analyze``, forwarding ``progress``/``recall`` only to
|
|
641
|
+
analyzers that accept them. Keeps the plugin ``analyze(root)`` contract
|
|
642
|
+
intact — old plugins that don't know about these kwargs are called
|
|
643
|
+
unchanged, and ``recall`` is only ever passed when it's on."""
|
|
644
|
+
kwargs: dict[str, object] = {}
|
|
645
|
+
try:
|
|
646
|
+
params = inspect.signature(analyzer.analyze).parameters
|
|
647
|
+
if progress is not None and "progress" in params:
|
|
648
|
+
kwargs["progress"] = progress
|
|
649
|
+
if recall and "recall" in params:
|
|
650
|
+
kwargs["recall"] = recall
|
|
651
|
+
except (TypeError, ValueError):
|
|
652
|
+
pass
|
|
653
|
+
return analyzer.analyze(root, **kwargs) # type: ignore[call-arg]
|
|
631
654
|
|
|
632
655
|
|
|
633
656
|
def _stamp_provenance(result: AnalyzerResult, analyzer_name: str) -> None:
|