attackmap 0.4.21__tar.gz → 0.4.26__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.21/src/attackmap.egg-info → attackmap-0.4.26}/PKG-INFO +52 -9
- {attackmap-0.4.21 → attackmap-0.4.26}/README.md +51 -8
- {attackmap-0.4.21 → attackmap-0.4.26}/pyproject.toml +1 -1
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/__init__.py +1 -1
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/analyzers.py +5 -1
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/anomalies.py +27 -1
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/cli.py +198 -6
- attackmap-0.4.26/src/attackmap/contracts.py +201 -0
- attackmap-0.4.26/src/attackmap/crossrepo.py +351 -0
- attackmap-0.4.26/src/attackmap/fleet.py +358 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/lockfiles.py +21 -4
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/merge.py +4 -1
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/models.py +5 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/scanner.py +31 -4
- {attackmap-0.4.21 → attackmap-0.4.26/src/attackmap.egg-info}/PKG-INFO +52 -9
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap.egg-info/SOURCES.txt +6 -0
- attackmap-0.4.26/tests/test_contracts.py +194 -0
- attackmap-0.4.26/tests/test_crossrepo.py +328 -0
- attackmap-0.4.26/tests/test_fleet.py +319 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_lockfiles.py +54 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_merge.py +11 -5
- {attackmap-0.4.21 → attackmap-0.4.26}/LICENSE +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/setup.cfg +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/analyzer.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/analyzer_contracts.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/asset_model.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/attack_taxonomy.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/authz.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/config_scanner.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/context_pack.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/control_model.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/crypto.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/cve.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/defensive_review.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/detection_opportunities.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/diagrams.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/diff.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/exploitability.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/graph.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/hunt_harness.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/insights.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/llm_review.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/progress.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/recon_models.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/recon_to_analysis.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/report.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/review_eval.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/review_json.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/review_prompts.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/route_auth_fusion.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/sarif.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/sbom.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/sdk/__init__.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/sdk/contracts.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/sdk/models.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/security_overlay.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/srcpaths.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/suggest.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/suppress.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/taint.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/threat_model.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/topology.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/triage.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/weaknesses.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/webhardening.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/workflow_scanner.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap.egg-info/dependency_links.txt +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap.egg-info/entry_points.txt +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap.egg-info/requires.txt +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap.egg-info/top_level.txt +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_analyzer.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_analyzer_metadata_schema.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_analyzers.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_anomalies.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_artifact_schemas.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_authz.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_config_scanner.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_context_pack.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_crypto.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_cve.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_defensive_review.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_diagrams.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_diff.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_exploitability.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_graph.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_hunt.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_hunt_harness.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_injection_sinks.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_llm_review.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_modules_cli.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_pr_comment.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_progress.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_provenance.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_recall.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_recon_to_analysis.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_remediate.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_report.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_review_eval.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_review_json.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_review_prompts.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_route_auth_fusion.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_sarif.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_sbom.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_scanner.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_security_overlay.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_shared_contract_imports.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_signal_v2.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_srcpaths.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_suggest.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_suppress.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_taint.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_taint_sanitizer.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_threat_intel.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_threat_model.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_topology.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_triage.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_weaknesses.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_webhardening.py +0 -0
- {attackmap-0.4.21 → attackmap-0.4.26}/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.26
|
|
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>
|
|
@@ -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
|
|
@@ -765,10 +790,12 @@ scaffolding, testing, and publishing instructions is in
|
|
|
765
790
|
|
|
766
791
|
```bash
|
|
767
792
|
attackmap analyze <path> # run a review on a repository
|
|
793
|
+
attackmap analyze repoA repoB gateway # multi-repo fleet scan (per-repo reports + fleet summary)
|
|
768
794
|
attackmap analyze <path> --output dir # write outputs to `dir/`
|
|
769
795
|
attackmap analyze <path> --format json # json | markdown | all (default)
|
|
770
796
|
attackmap analyze <path> --module python --module rust # only these analyzers
|
|
771
797
|
attackmap analyze <path> --cve # cross-reference SBOM against OSV.dev
|
|
798
|
+
attackmap analyze <path> --recall # widen taint discovery (deeper hops, capability-reach); extra reach marked SPECULATIVE
|
|
772
799
|
attackmap analyze <path> --llm # add LLM narrative (auto-resolve auth)
|
|
773
800
|
attackmap analyze <path> --llm --llm-backend cli # force Claude CLI
|
|
774
801
|
attackmap analyze <path> --llm --llm-provider openai # use OpenAI/Codex (OPENAI_API_KEY or `codex` CLI)
|
|
@@ -778,6 +805,8 @@ attackmap analyze <path> --hunt --verify # adjudicate each lead vs. source (conf
|
|
|
778
805
|
attackmap analyze <path> --hunt --verify --verify-votes 3 # majority vote of N independent skeptics (default 3; 1 = single pass)
|
|
779
806
|
attackmap analyze <path> --hunt --verify --hunt-lenses 4 # N lens-specialised generation passes, deduped, then verified
|
|
780
807
|
attackmap analyze <path> --hunt --verify --hunt-rounds 4 # loop-until-dry generation (completeness critic seeds each round)
|
|
808
|
+
attackmap analyze <path> --hunt --verify --hunt-budget 500000 # cap total hunt output tokens across rounds
|
|
809
|
+
attackmap analyze <path> --recall --hunt --verify # widen the net, then adjudicate the speculative reach
|
|
781
810
|
attackmap analyze <path> --remediate # LLM review-first fix suggestions (remediation.md)
|
|
782
811
|
attackmap analyze <path> --triage # cluster/dedupe/rank existing findings (triage.md; deterministic fallback)
|
|
783
812
|
attackmap analyze <path> --pr-comment pr.md # Markdown PR summary comment for CI
|
|
@@ -824,7 +853,7 @@ introduces a new HIGH finding.
|
|
|
824
853
|
|
|
825
854
|
## Project status
|
|
826
855
|
|
|
827
|
-
AttackMap is **beta** (v0.4.
|
|
856
|
+
AttackMap is **beta** (v0.4.25) — published and validated on real codebases, but
|
|
828
857
|
pre-1.0 and heuristic.
|
|
829
858
|
|
|
830
859
|
**Solid today:**
|
|
@@ -842,13 +871,27 @@ pre-1.0 and heuristic.
|
|
|
842
871
|
- BOLA/IDOR authorization detection (with custom-middleware / guard-arg awareness).
|
|
843
872
|
- Insecure-crypto / weak-randomness (Python/JS/Go/PHP) and web-hardening
|
|
844
873
|
(CORS/CSRF/cookies/CSP/debug) detection.
|
|
845
|
-
- Anomaly / outlier detection
|
|
846
|
-
|
|
847
|
-
and
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
874
|
+
- Anomaly / outlier detection — including **signature-free invariant mining**
|
|
875
|
+
(flag the handler that reaches a dangerous sink without the guard its peers
|
|
876
|
+
apply) — and **exploitability fusion**, a deterministic, explainable 0–100
|
|
877
|
+
"exploitable now" score that ranks route→sink combinations and folds in
|
|
878
|
+
known-vulnerable dependencies on the path.
|
|
879
|
+
- **`--hunt`** (LLM exploit-chain hypotheses) with a **verify jury** —
|
|
880
|
+
**`--verify`** adjudicates each lead CONFIRMED/REFUTED/NEEDS-REVIEW against the
|
|
881
|
+
actual source; **`--verify-votes`** runs N independent skeptics, **`--hunt-lenses`**
|
|
882
|
+
fans generation across failure-mode specialists, **`--hunt-rounds`** loops
|
|
883
|
+
until dry with a completeness critic, **`--hunt-budget`** caps token spend —
|
|
884
|
+
plus **`--remediate`** (review-first fixes) and **`--triage`**
|
|
885
|
+
(cluster/dedupe/rank existing findings, deterministic fallback).
|
|
886
|
+
- **`--recall`** — verifier-gated aggressive discovery: widens the taint walk
|
|
887
|
+
(deeper hops, capability-reach enumeration of every reach to exec/fs/net/
|
|
888
|
+
deserialize/template/SQL) and marks the extra reach SPECULATIVE, kept out of
|
|
889
|
+
`--fail-on-new-high` and left for `--hunt --verify` to adjudicate.
|
|
890
|
+
- **Cross-repo / fleet analysis** — `attackmap analyze repoA repoB …` scans a
|
|
891
|
+
fleet, links one repo's outbound HTTP calls to another's routes, and surfaces
|
|
892
|
+
bugs that live in the seams: cross-boundary (confused-deputy) flows,
|
|
893
|
+
trust-assumption gaps, and the sibling service that omits a control its peers
|
|
894
|
+
enforce. All cross-repo findings are speculative until verified.
|
|
852
895
|
- SBOM inventory (5 ecosystems) + OSV.dev CVE cross-reference (`--cve`).
|
|
853
896
|
- Output: Markdown + JSON + **SARIF 2.1.0** + **Mermaid / Graphviz** diagrams;
|
|
854
897
|
**diff/baseline** PR gating; a reusable **GitHub Action + PR bot**; optional
|
|
@@ -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
|
|
@@ -704,10 +729,12 @@ scaffolding, testing, and publishing instructions is in
|
|
|
704
729
|
|
|
705
730
|
```bash
|
|
706
731
|
attackmap analyze <path> # run a review on a repository
|
|
732
|
+
attackmap analyze repoA repoB gateway # multi-repo fleet scan (per-repo reports + fleet summary)
|
|
707
733
|
attackmap analyze <path> --output dir # write outputs to `dir/`
|
|
708
734
|
attackmap analyze <path> --format json # json | markdown | all (default)
|
|
709
735
|
attackmap analyze <path> --module python --module rust # only these analyzers
|
|
710
736
|
attackmap analyze <path> --cve # cross-reference SBOM against OSV.dev
|
|
737
|
+
attackmap analyze <path> --recall # widen taint discovery (deeper hops, capability-reach); extra reach marked SPECULATIVE
|
|
711
738
|
attackmap analyze <path> --llm # add LLM narrative (auto-resolve auth)
|
|
712
739
|
attackmap analyze <path> --llm --llm-backend cli # force Claude CLI
|
|
713
740
|
attackmap analyze <path> --llm --llm-provider openai # use OpenAI/Codex (OPENAI_API_KEY or `codex` CLI)
|
|
@@ -717,6 +744,8 @@ attackmap analyze <path> --hunt --verify # adjudicate each lead vs. source (conf
|
|
|
717
744
|
attackmap analyze <path> --hunt --verify --verify-votes 3 # majority vote of N independent skeptics (default 3; 1 = single pass)
|
|
718
745
|
attackmap analyze <path> --hunt --verify --hunt-lenses 4 # N lens-specialised generation passes, deduped, then verified
|
|
719
746
|
attackmap analyze <path> --hunt --verify --hunt-rounds 4 # loop-until-dry generation (completeness critic seeds each round)
|
|
747
|
+
attackmap analyze <path> --hunt --verify --hunt-budget 500000 # cap total hunt output tokens across rounds
|
|
748
|
+
attackmap analyze <path> --recall --hunt --verify # widen the net, then adjudicate the speculative reach
|
|
720
749
|
attackmap analyze <path> --remediate # LLM review-first fix suggestions (remediation.md)
|
|
721
750
|
attackmap analyze <path> --triage # cluster/dedupe/rank existing findings (triage.md; deterministic fallback)
|
|
722
751
|
attackmap analyze <path> --pr-comment pr.md # Markdown PR summary comment for CI
|
|
@@ -763,7 +792,7 @@ introduces a new HIGH finding.
|
|
|
763
792
|
|
|
764
793
|
## Project status
|
|
765
794
|
|
|
766
|
-
AttackMap is **beta** (v0.4.
|
|
795
|
+
AttackMap is **beta** (v0.4.25) — published and validated on real codebases, but
|
|
767
796
|
pre-1.0 and heuristic.
|
|
768
797
|
|
|
769
798
|
**Solid today:**
|
|
@@ -781,13 +810,27 @@ pre-1.0 and heuristic.
|
|
|
781
810
|
- BOLA/IDOR authorization detection (with custom-middleware / guard-arg awareness).
|
|
782
811
|
- Insecure-crypto / weak-randomness (Python/JS/Go/PHP) and web-hardening
|
|
783
812
|
(CORS/CSRF/cookies/CSP/debug) detection.
|
|
784
|
-
- Anomaly / outlier detection
|
|
785
|
-
|
|
786
|
-
and
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
813
|
+
- Anomaly / outlier detection — including **signature-free invariant mining**
|
|
814
|
+
(flag the handler that reaches a dangerous sink without the guard its peers
|
|
815
|
+
apply) — and **exploitability fusion**, a deterministic, explainable 0–100
|
|
816
|
+
"exploitable now" score that ranks route→sink combinations and folds in
|
|
817
|
+
known-vulnerable dependencies on the path.
|
|
818
|
+
- **`--hunt`** (LLM exploit-chain hypotheses) with a **verify jury** —
|
|
819
|
+
**`--verify`** adjudicates each lead CONFIRMED/REFUTED/NEEDS-REVIEW against the
|
|
820
|
+
actual source; **`--verify-votes`** runs N independent skeptics, **`--hunt-lenses`**
|
|
821
|
+
fans generation across failure-mode specialists, **`--hunt-rounds`** loops
|
|
822
|
+
until dry with a completeness critic, **`--hunt-budget`** caps token spend —
|
|
823
|
+
plus **`--remediate`** (review-first fixes) and **`--triage`**
|
|
824
|
+
(cluster/dedupe/rank existing findings, deterministic fallback).
|
|
825
|
+
- **`--recall`** — verifier-gated aggressive discovery: widens the taint walk
|
|
826
|
+
(deeper hops, capability-reach enumeration of every reach to exec/fs/net/
|
|
827
|
+
deserialize/template/SQL) and marks the extra reach SPECULATIVE, kept out of
|
|
828
|
+
`--fail-on-new-high` and left for `--hunt --verify` to adjudicate.
|
|
829
|
+
- **Cross-repo / fleet analysis** — `attackmap analyze repoA repoB …` scans a
|
|
830
|
+
fleet, links one repo's outbound HTTP calls to another's routes, and surfaces
|
|
831
|
+
bugs that live in the seams: cross-boundary (confused-deputy) flows,
|
|
832
|
+
trust-assumption gaps, and the sibling service that omits a control its peers
|
|
833
|
+
enforce. All cross-repo findings are speculative until verified.
|
|
791
834
|
- SBOM inventory (5 ecosystems) + OSV.dev CVE cross-reference (`--cve`).
|
|
792
835
|
- Output: Markdown + JSON + **SARIF 2.1.0** + **Mermaid / Graphviz** diagrams;
|
|
793
836
|
**diff/baseline** PR gating; a reusable **GitHub Action + PR bot**; optional
|
|
@@ -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.26"
|
|
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
|
|
|
@@ -195,6 +195,32 @@ def find_anomalies(
|
|
|
195
195
|
return out
|
|
196
196
|
|
|
197
197
|
|
|
198
|
+
def route_auth_signals(
|
|
199
|
+
scan: ScanResult, root: str | Path | None = None
|
|
200
|
+
) -> dict[tuple[str, str, str], bool]:
|
|
201
|
+
"""Per-route authentication/authorization presence for one repo.
|
|
202
|
+
|
|
203
|
+
Maps ``(file, method, path)`` → whether an auth/authorization marker appears
|
|
204
|
+
within that route handler's own span (the same span-bounded detection the
|
|
205
|
+
within-repo `auth_outlier` pass uses). Exposed for cross-repo trust analysis
|
|
206
|
+
(#146d): the fleet layer runs it per repo — each with its own root — to spot
|
|
207
|
+
an unauthenticated route reached across a service boundary, and the sibling
|
|
208
|
+
service that omits an auth control its peers enforce."""
|
|
209
|
+
root_path = Path(root or scan.root).resolve()
|
|
210
|
+
routes = [r for r in scan.routes if not is_test_file(r.file)]
|
|
211
|
+
line_cache: dict[str, list[str]] = {}
|
|
212
|
+
file_route_lines: dict[str, list[int]] = {}
|
|
213
|
+
for route in routes:
|
|
214
|
+
if route.line is not None:
|
|
215
|
+
file_route_lines.setdefault(route.file, []).append(route.line)
|
|
216
|
+
for lines in file_route_lines.values():
|
|
217
|
+
lines.sort()
|
|
218
|
+
ctx = _SignalCtx(root_path, line_cache, file_route_lines)
|
|
219
|
+
return {
|
|
220
|
+
(r.file, r.method, r.path): _route_has_signal(r, ctx, _AUTH_MARKERS) for r in routes
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
198
224
|
# --- Heuristics ------------------------------------------------------------
|
|
199
225
|
|
|
200
226
|
|
|
@@ -608,4 +634,4 @@ def _file_lines(rel_file: str, ctx: _SignalCtx) -> list[str]:
|
|
|
608
634
|
return lines
|
|
609
635
|
|
|
610
636
|
|
|
611
|
-
__all__ = ["find_anomalies"]
|
|
637
|
+
__all__ = ["find_anomalies", "route_auth_signals"]
|
|
@@ -17,6 +17,21 @@ from .analyzers import (
|
|
|
17
17
|
select_requested_analyzers,
|
|
18
18
|
)
|
|
19
19
|
from .cve import query_vulnerabilities
|
|
20
|
+
from .anomalies import route_auth_signals
|
|
21
|
+
from .contracts import link_contracts
|
|
22
|
+
from .crossrepo import (
|
|
23
|
+
find_cross_boundary_flows,
|
|
24
|
+
find_cross_repo_anomalies,
|
|
25
|
+
find_trust_gaps,
|
|
26
|
+
)
|
|
27
|
+
from .fleet import (
|
|
28
|
+
FleetRepoResult,
|
|
29
|
+
FleetScan,
|
|
30
|
+
fleet_repo_ids,
|
|
31
|
+
fleet_summary_json,
|
|
32
|
+
render_fleet_graph_mermaid,
|
|
33
|
+
render_fleet_summary,
|
|
34
|
+
)
|
|
20
35
|
from .diff import (
|
|
21
36
|
FindingSnapshot,
|
|
22
37
|
diff_findings,
|
|
@@ -66,9 +81,151 @@ TRIAGE_BANNER = (
|
|
|
66
81
|
)
|
|
67
82
|
|
|
68
83
|
|
|
84
|
+
def _run_fleet(
|
|
85
|
+
repo_paths: list[Path],
|
|
86
|
+
*,
|
|
87
|
+
output: str,
|
|
88
|
+
module: list[str] | None,
|
|
89
|
+
recall: bool,
|
|
90
|
+
cve: bool,
|
|
91
|
+
no_suppress: bool,
|
|
92
|
+
suppress_file: str | None,
|
|
93
|
+
progress_format: str,
|
|
94
|
+
no_progress: bool,
|
|
95
|
+
fleet_incompatible: dict[str, bool],
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Multi-repo fleet scan (#146a). Scans each repo independently — reusing the
|
|
98
|
+
same building blocks a single-repo run uses — writes per-repo reports into
|
|
99
|
+
``output/<repo_id>/``, and assembles a fleet summary. No cross-repo detection
|
|
100
|
+
yet (that is #146b–#146d); this is the foundation."""
|
|
101
|
+
active_flags = [flag for flag, on in fleet_incompatible.items() if on]
|
|
102
|
+
if active_flags:
|
|
103
|
+
raise typer.BadParameter(
|
|
104
|
+
f"{', '.join(sorted(active_flags))} not yet supported in multi-repo mode — "
|
|
105
|
+
"run one repository at a time for these (fleet-aware wiring lands in #146b+)."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
selected_analyzers = None
|
|
109
|
+
if module:
|
|
110
|
+
try:
|
|
111
|
+
selected_analyzers = select_requested_analyzers(module, auto_install=True)
|
|
112
|
+
except ValueError as exc:
|
|
113
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
114
|
+
|
|
115
|
+
output_root = Path(output)
|
|
116
|
+
repo_ids = fleet_repo_ids(repo_paths)
|
|
117
|
+
fleet = FleetScan()
|
|
118
|
+
|
|
119
|
+
for repo_id, repo_path in zip(repo_ids, repo_paths):
|
|
120
|
+
typer.echo("")
|
|
121
|
+
typer.echo(f"── {repo_id} ({repo_path}) ──")
|
|
122
|
+
active_analyzers = resolve_run_analyzers(repo_path, analyzers=selected_analyzers)
|
|
123
|
+
scan_progress = create_progress(progress_format, no_progress=no_progress)
|
|
124
|
+
scan = analyze_repository(
|
|
125
|
+
repo_path, analyzers=active_analyzers, progress=scan_progress, recall=recall
|
|
126
|
+
)
|
|
127
|
+
if cve and scan.dependencies:
|
|
128
|
+
try:
|
|
129
|
+
vulns, _cve_summary = query_vulnerabilities(scan.dependencies)
|
|
130
|
+
finally:
|
|
131
|
+
scan_progress.done()
|
|
132
|
+
scan.vulnerabilities = vulns
|
|
133
|
+
|
|
134
|
+
graph = build_graph(scan)
|
|
135
|
+
analysis = translate_recon(scan)
|
|
136
|
+
attack_surfaces = analysis.attack_surfaces
|
|
137
|
+
findings = analysis.findings
|
|
138
|
+
attack_paths = analysis.attack_paths
|
|
139
|
+
architecture_md = summarize_architecture(scan, graph)
|
|
140
|
+
attack_surface_md = summarize_attack_surface(scan, attack_surfaces)
|
|
141
|
+
|
|
142
|
+
suppressed_findings: list = []
|
|
143
|
+
if not no_suppress:
|
|
144
|
+
explicit = Path(suppress_file) if suppress_file else None
|
|
145
|
+
if explicit is not None and not explicit.exists():
|
|
146
|
+
raise typer.BadParameter(f"Suppress file not found: {explicit}")
|
|
147
|
+
suppset, sup_warnings = collect_suppressions(repo_path, findings, explicit_file=explicit)
|
|
148
|
+
for warning in sup_warnings:
|
|
149
|
+
typer.echo(f"Suppression warning: {warning}", err=True)
|
|
150
|
+
outcome = apply_suppressions(findings, suppset)
|
|
151
|
+
findings = outcome.active
|
|
152
|
+
suppressed_findings = outcome.suppressed
|
|
153
|
+
|
|
154
|
+
defensive_review_md = render_defensive_review(scan, attack_surfaces, findings, attack_paths)
|
|
155
|
+
repo_out = output_root / repo_id
|
|
156
|
+
write_reports(
|
|
157
|
+
repo_out,
|
|
158
|
+
scan,
|
|
159
|
+
architecture_md,
|
|
160
|
+
attack_surface_md,
|
|
161
|
+
defensive_review_md,
|
|
162
|
+
attack_surfaces,
|
|
163
|
+
findings,
|
|
164
|
+
attack_paths,
|
|
165
|
+
analyzer_metadata=[
|
|
166
|
+
{
|
|
167
|
+
"name": metadata.name,
|
|
168
|
+
"description": metadata.description,
|
|
169
|
+
"scope": metadata.scope,
|
|
170
|
+
"ecosystems": list(metadata.ecosystems),
|
|
171
|
+
}
|
|
172
|
+
for metadata in (get_analyzer_metadata(a) for a in active_analyzers)
|
|
173
|
+
],
|
|
174
|
+
suppressed=suppressed_findings,
|
|
175
|
+
)
|
|
176
|
+
typer.echo(render_console_summary(scan, findings, attack_paths))
|
|
177
|
+
fleet.results.append(
|
|
178
|
+
FleetRepoResult(
|
|
179
|
+
repo_id=repo_id,
|
|
180
|
+
root=str(repo_path),
|
|
181
|
+
report_dir=str(repo_out.resolve()),
|
|
182
|
+
scan=scan,
|
|
183
|
+
findings=findings,
|
|
184
|
+
attack_paths=attack_paths,
|
|
185
|
+
suppressed_count=len(suppressed_findings),
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Cross-repo contract linking (#146b): match one repo's outbound calls to
|
|
190
|
+
# another repo's routes, over the assembled fleet.
|
|
191
|
+
repo_scans = [(r.repo_id, r.scan) for r in fleet.results]
|
|
192
|
+
fleet.links = link_contracts(repo_scans)
|
|
193
|
+
# Cross-boundary trust flows (#146c): confused-deputy leads over the links.
|
|
194
|
+
fleet.cross_boundary = find_cross_boundary_flows(fleet.links, repo_scans)
|
|
195
|
+
# Trust-assumption gaps + cross-repo anomalies (#146d / #149b). Per-route auth
|
|
196
|
+
# signals are resolved per repo (each with its own root).
|
|
197
|
+
auth_by_repo = {r.repo_id: route_auth_signals(r.scan, r.root) for r in fleet.results}
|
|
198
|
+
fleet.trust_gaps = find_trust_gaps(fleet.links, repo_scans, auth_by_repo)
|
|
199
|
+
fleet.cross_repo_anomalies = find_cross_repo_anomalies(repo_scans, auth_by_repo)
|
|
200
|
+
|
|
201
|
+
output_root.mkdir(parents=True, exist_ok=True)
|
|
202
|
+
(output_root / "fleet-summary.md").write_text(
|
|
203
|
+
render_fleet_summary(fleet) + "\n", encoding="utf-8"
|
|
204
|
+
)
|
|
205
|
+
(output_root / "fleet-summary.json").write_text(
|
|
206
|
+
json.dumps(fleet_summary_json(fleet), indent=2) + "\n", encoding="utf-8"
|
|
207
|
+
)
|
|
208
|
+
(output_root / "fleet-graph.md").write_text(
|
|
209
|
+
render_fleet_graph_mermaid(fleet) + "\n", encoding="utf-8"
|
|
210
|
+
)
|
|
211
|
+
typer.echo("")
|
|
212
|
+
typer.echo(
|
|
213
|
+
f"Fleet: {fleet.repo_count} repositories, {fleet.total_findings()} finding(s), "
|
|
214
|
+
f"{len(fleet.links)} cross-repo link(s), "
|
|
215
|
+
f"{len(fleet.cross_boundary)} cross-boundary + {len(fleet.trust_gaps)} trust-gap + "
|
|
216
|
+
f"{len(fleet.cross_repo_anomalies)} anomaly (all speculative). "
|
|
217
|
+
f"Summary written to: {(output_root / 'fleet-summary.md').resolve()}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
69
221
|
@app.command()
|
|
70
222
|
def analyze(
|
|
71
|
-
|
|
223
|
+
paths: list[str] | None = typer.Argument(
|
|
224
|
+
None,
|
|
225
|
+
help="Path(s) to the repository(ies) to analyze. Pass two or more for a "
|
|
226
|
+
"cross-repo fleet scan (#146a): each is scanned into its own report "
|
|
227
|
+
"subdirectory and a fleet summary is written alongside. Defaults to '.'.",
|
|
228
|
+
),
|
|
72
229
|
output: str = typer.Option("reports", "--output", "-o", help="Directory for generated reports."),
|
|
73
230
|
format: str = typer.Option("all", "--format", help="Output format: all, markdown, or json."),
|
|
74
231
|
module: list[str] | None = typer.Option(
|
|
@@ -198,9 +355,45 @@ def analyze(
|
|
|
198
355
|
help="Path to a suppression baseline, overriding auto-discovery of .attackmap-suppress.yaml at the repo root (#144).",
|
|
199
356
|
),
|
|
200
357
|
) -> None:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
358
|
+
repo_paths = [Path(p).resolve() for p in (paths or ["."])]
|
|
359
|
+
for rp in repo_paths:
|
|
360
|
+
if not rp.exists():
|
|
361
|
+
raise typer.BadParameter(f"Path does not exist: {rp}")
|
|
362
|
+
|
|
363
|
+
# Validate progress format before either branch, so the fleet path enforces
|
|
364
|
+
# the same option semantics as a single-repo run (#146a).
|
|
365
|
+
if progress_format not in {"auto", "tty", "json", "none"}:
|
|
366
|
+
raise typer.BadParameter("--progress-format must be one of: auto, json, none.")
|
|
367
|
+
|
|
368
|
+
# Multi-repo fleet mode (#146a): scan each repo independently and assemble a
|
|
369
|
+
# fleet view. Dispatched here so the single-repo path below is untouched.
|
|
370
|
+
if len(repo_paths) > 1:
|
|
371
|
+
_run_fleet(
|
|
372
|
+
repo_paths,
|
|
373
|
+
output=output,
|
|
374
|
+
module=module,
|
|
375
|
+
recall=recall,
|
|
376
|
+
cve=cve,
|
|
377
|
+
no_suppress=no_suppress,
|
|
378
|
+
suppress_file=suppress_file,
|
|
379
|
+
progress_format=progress_format,
|
|
380
|
+
no_progress=no_progress,
|
|
381
|
+
# Single-repo-only features aren't fleet-aware yet — reject rather
|
|
382
|
+
# than silently ignore, so the user isn't surprised (#146b+ wire them).
|
|
383
|
+
fleet_incompatible={
|
|
384
|
+
"--baseline": baseline is not None,
|
|
385
|
+
"--diff-output": diff_output is not None,
|
|
386
|
+
"--fail-on-new-high": fail_on_new_high,
|
|
387
|
+
"--pr-comment": pr_comment is not None,
|
|
388
|
+
"--llm": llm,
|
|
389
|
+
"--hunt": hunt,
|
|
390
|
+
"--remediate": remediate,
|
|
391
|
+
"--triage": triage,
|
|
392
|
+
},
|
|
393
|
+
)
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
repo_path = repo_paths[0]
|
|
204
397
|
# Validate diff-mode flag combinations before doing any real work.
|
|
205
398
|
if fail_on_new_high and baseline is None:
|
|
206
399
|
raise typer.BadParameter("--fail-on-new-high requires --baseline to be set.")
|
|
@@ -216,8 +409,7 @@ def analyze(
|
|
|
216
409
|
except ValueError as exc:
|
|
217
410
|
raise typer.BadParameter(str(exc)) from exc
|
|
218
411
|
|
|
219
|
-
|
|
220
|
-
raise typer.BadParameter("--progress-format must be one of: auto, json, none.")
|
|
412
|
+
# (--progress-format is validated up-front, before the fleet dispatch.)
|
|
221
413
|
if llm_speed not in {"standard", "fast"}:
|
|
222
414
|
raise typer.BadParameter("--llm-speed must be one of: standard, fast.")
|
|
223
415
|
if llm_provider not in {"claude", "openai"}:
|