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.

Files changed (119) hide show
  1. {attackmap-0.4.21/src/attackmap.egg-info → attackmap-0.4.26}/PKG-INFO +52 -9
  2. {attackmap-0.4.21 → attackmap-0.4.26}/README.md +51 -8
  3. {attackmap-0.4.21 → attackmap-0.4.26}/pyproject.toml +1 -1
  4. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/__init__.py +1 -1
  5. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/analyzers.py +5 -1
  6. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/anomalies.py +27 -1
  7. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/cli.py +198 -6
  8. attackmap-0.4.26/src/attackmap/contracts.py +201 -0
  9. attackmap-0.4.26/src/attackmap/crossrepo.py +351 -0
  10. attackmap-0.4.26/src/attackmap/fleet.py +358 -0
  11. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/lockfiles.py +21 -4
  12. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/merge.py +4 -1
  13. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/models.py +5 -0
  14. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/scanner.py +31 -4
  15. {attackmap-0.4.21 → attackmap-0.4.26/src/attackmap.egg-info}/PKG-INFO +52 -9
  16. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap.egg-info/SOURCES.txt +6 -0
  17. attackmap-0.4.26/tests/test_contracts.py +194 -0
  18. attackmap-0.4.26/tests/test_crossrepo.py +328 -0
  19. attackmap-0.4.26/tests/test_fleet.py +319 -0
  20. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_lockfiles.py +54 -0
  21. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_merge.py +11 -5
  22. {attackmap-0.4.21 → attackmap-0.4.26}/LICENSE +0 -0
  23. {attackmap-0.4.21 → attackmap-0.4.26}/setup.cfg +0 -0
  24. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/analyzer.py +0 -0
  25. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/analyzer_contracts.py +0 -0
  26. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/asset_model.py +0 -0
  27. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/attack_taxonomy.py +0 -0
  28. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/authz.py +0 -0
  29. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/config_scanner.py +0 -0
  30. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/context_pack.py +0 -0
  31. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/control_model.py +0 -0
  32. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/crypto.py +0 -0
  33. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/cve.py +0 -0
  34. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/defensive_review.py +0 -0
  35. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/detection_opportunities.py +0 -0
  36. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/diagrams.py +0 -0
  37. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/diff.py +0 -0
  38. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/exploitability.py +0 -0
  39. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/graph.py +0 -0
  40. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/hunt_harness.py +0 -0
  41. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/insights.py +0 -0
  42. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/llm_review.py +0 -0
  43. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/progress.py +0 -0
  44. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/recon_models.py +0 -0
  45. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/recon_to_analysis.py +0 -0
  46. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/report.py +0 -0
  47. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/review_eval.py +0 -0
  48. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/review_json.py +0 -0
  49. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/review_prompts.py +0 -0
  50. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/route_auth_fusion.py +0 -0
  51. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/sarif.py +0 -0
  52. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/sbom.py +0 -0
  53. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/sdk/__init__.py +0 -0
  54. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/sdk/contracts.py +0 -0
  55. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/sdk/models.py +0 -0
  56. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/security_overlay.py +0 -0
  57. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/srcpaths.py +0 -0
  58. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/suggest.py +0 -0
  59. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/suppress.py +0 -0
  60. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/taint.py +0 -0
  61. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/threat_model.py +0 -0
  62. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/topology.py +0 -0
  63. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/triage.py +0 -0
  64. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/weaknesses.py +0 -0
  65. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/webhardening.py +0 -0
  66. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap/workflow_scanner.py +0 -0
  67. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap.egg-info/dependency_links.txt +0 -0
  68. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap.egg-info/entry_points.txt +0 -0
  69. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap.egg-info/requires.txt +0 -0
  70. {attackmap-0.4.21 → attackmap-0.4.26}/src/attackmap.egg-info/top_level.txt +0 -0
  71. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_analyzer.py +0 -0
  72. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_analyzer_metadata_schema.py +0 -0
  73. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_analyzers.py +0 -0
  74. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_anomalies.py +0 -0
  75. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_artifact_schemas.py +0 -0
  76. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_authz.py +0 -0
  77. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_config_scanner.py +0 -0
  78. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_context_pack.py +0 -0
  79. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_crypto.py +0 -0
  80. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_cve.py +0 -0
  81. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_defensive_review.py +0 -0
  82. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_diagrams.py +0 -0
  83. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_diff.py +0 -0
  84. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_exploitability.py +0 -0
  85. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_graph.py +0 -0
  86. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_hunt.py +0 -0
  87. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_hunt_harness.py +0 -0
  88. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_injection_sinks.py +0 -0
  89. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_llm_review.py +0 -0
  90. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_modules_cli.py +0 -0
  91. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_pr_comment.py +0 -0
  92. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_progress.py +0 -0
  93. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_provenance.py +0 -0
  94. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_recall.py +0 -0
  95. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_recon_to_analysis.py +0 -0
  96. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_remediate.py +0 -0
  97. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_report.py +0 -0
  98. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_review_eval.py +0 -0
  99. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_review_json.py +0 -0
  100. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_review_prompts.py +0 -0
  101. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_route_auth_fusion.py +0 -0
  102. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_sarif.py +0 -0
  103. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_sbom.py +0 -0
  104. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_scanner.py +0 -0
  105. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_security_overlay.py +0 -0
  106. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_shared_contract_imports.py +0 -0
  107. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_signal_v2.py +0 -0
  108. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_srcpaths.py +0 -0
  109. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_suggest.py +0 -0
  110. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_suppress.py +0 -0
  111. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_taint.py +0 -0
  112. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_taint_sanitizer.py +0 -0
  113. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_threat_intel.py +0 -0
  114. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_threat_model.py +0 -0
  115. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_topology.py +0 -0
  116. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_triage.py +0 -0
  117. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_weaknesses.py +0 -0
  118. {attackmap-0.4.21 → attackmap-0.4.26}/tests/test_webhardening.py +0 -0
  119. {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.21
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.18) — published and validated on real codebases, but
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 and **exploitability fusion** — a deterministic,
846
- explainable 0–100 "exploitable now" score that ranks route→sink combinations
847
- and folds in known-vulnerable dependencies on the path.
848
- - **`--hunt`** (LLM exploit-chain hypotheses) with **`--verify`** (adjudicate
849
- each lead CONFIRMED/REFUTED/NEEDS-REVIEW against the actual source),
850
- **`--remediate`** (review-first fix suggestions), and **`--triage`**
851
- (cluster/dedupe/rank existing findings, with a deterministic fallback).
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.18) — published and validated on real codebases, but
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 and **exploitability fusion** — a deterministic,
785
- explainable 0–100 "exploitable now" score that ranks route→sink combinations
786
- and folds in known-vulnerable dependencies on the path.
787
- - **`--hunt`** (LLM exploit-chain hypotheses) with **`--verify`** (adjudicate
788
- each lead CONFIRMED/REFUTED/NEEDS-REVIEW against the actual source),
789
- **`--remediate`** (review-first fix suggestions), and **`--triage`**
790
- (cluster/dedupe/rank existing findings, with a deterministic fallback).
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.21"
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.21"
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
- calls.append(ExternalCall(target=match.groups()[-1], file=context.relative_path))
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
- path: str = typer.Argument(".", help="Path to the repository to analyze."),
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
- repo_path = Path(path).resolve()
202
- if not repo_path.exists():
203
- raise typer.BadParameter(f"Path does not exist: {repo_path}")
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
- if progress_format not in {"auto", "tty", "json", "none"}:
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"}: