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.

Files changed (120) hide show
  1. {attackmap-0.4.15/src/attackmap.egg-info → attackmap-0.4.25}/PKG-INFO +84 -3
  2. {attackmap-0.4.15 → attackmap-0.4.25}/README.md +83 -2
  3. {attackmap-0.4.15 → attackmap-0.4.25}/pyproject.toml +1 -1
  4. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/__init__.py +1 -1
  5. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/analyzers.py +42 -19
  6. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/anomalies.py +241 -9
  7. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/authz.py +4 -1
  8. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/cli.py +327 -45
  9. attackmap-0.4.25/src/attackmap/contracts.py +201 -0
  10. attackmap-0.4.25/src/attackmap/crossrepo.py +351 -0
  11. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/exploitability.py +3 -1
  12. attackmap-0.4.25/src/attackmap/fleet.py +358 -0
  13. attackmap-0.4.25/src/attackmap/hunt_harness.py +451 -0
  14. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/llm_review.py +35 -8
  15. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/merge.py +4 -1
  16. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/models.py +28 -1
  17. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/review_prompts.py +172 -0
  18. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/scanner.py +36 -6
  19. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/taint.py +191 -20
  20. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/threat_model.py +63 -2
  21. {attackmap-0.4.15 → attackmap-0.4.25/src/attackmap.egg-info}/PKG-INFO +84 -3
  22. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap.egg-info/SOURCES.txt +9 -0
  23. attackmap-0.4.25/tests/test_anomalies.py +663 -0
  24. attackmap-0.4.25/tests/test_contracts.py +194 -0
  25. attackmap-0.4.25/tests/test_crossrepo.py +328 -0
  26. attackmap-0.4.25/tests/test_fleet.py +319 -0
  27. attackmap-0.4.25/tests/test_hunt_harness.py +558 -0
  28. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_merge.py +11 -5
  29. attackmap-0.4.25/tests/test_recall.py +310 -0
  30. attackmap-0.4.15/tests/test_anomalies.py +0 -397
  31. {attackmap-0.4.15 → attackmap-0.4.25}/LICENSE +0 -0
  32. {attackmap-0.4.15 → attackmap-0.4.25}/setup.cfg +0 -0
  33. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/analyzer.py +0 -0
  34. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/analyzer_contracts.py +0 -0
  35. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/asset_model.py +0 -0
  36. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/attack_taxonomy.py +0 -0
  37. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/config_scanner.py +0 -0
  38. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/context_pack.py +0 -0
  39. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/control_model.py +0 -0
  40. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/crypto.py +0 -0
  41. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/cve.py +0 -0
  42. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/defensive_review.py +0 -0
  43. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/detection_opportunities.py +0 -0
  44. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/diagrams.py +0 -0
  45. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/diff.py +0 -0
  46. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/graph.py +0 -0
  47. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/insights.py +0 -0
  48. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/lockfiles.py +0 -0
  49. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/progress.py +0 -0
  50. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/recon_models.py +0 -0
  51. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/recon_to_analysis.py +0 -0
  52. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/report.py +0 -0
  53. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/review_eval.py +0 -0
  54. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/review_json.py +0 -0
  55. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/route_auth_fusion.py +0 -0
  56. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/sarif.py +0 -0
  57. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/sbom.py +0 -0
  58. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/sdk/__init__.py +0 -0
  59. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/sdk/contracts.py +0 -0
  60. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/sdk/models.py +0 -0
  61. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/security_overlay.py +0 -0
  62. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/srcpaths.py +0 -0
  63. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/suggest.py +0 -0
  64. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/suppress.py +0 -0
  65. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/topology.py +0 -0
  66. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/triage.py +0 -0
  67. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/weaknesses.py +0 -0
  68. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/webhardening.py +0 -0
  69. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap/workflow_scanner.py +0 -0
  70. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap.egg-info/dependency_links.txt +0 -0
  71. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap.egg-info/entry_points.txt +0 -0
  72. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap.egg-info/requires.txt +0 -0
  73. {attackmap-0.4.15 → attackmap-0.4.25}/src/attackmap.egg-info/top_level.txt +0 -0
  74. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_analyzer.py +0 -0
  75. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_analyzer_metadata_schema.py +0 -0
  76. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_analyzers.py +0 -0
  77. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_artifact_schemas.py +0 -0
  78. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_authz.py +0 -0
  79. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_config_scanner.py +0 -0
  80. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_context_pack.py +0 -0
  81. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_crypto.py +0 -0
  82. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_cve.py +0 -0
  83. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_defensive_review.py +0 -0
  84. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_diagrams.py +0 -0
  85. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_diff.py +0 -0
  86. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_exploitability.py +0 -0
  87. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_graph.py +0 -0
  88. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_hunt.py +0 -0
  89. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_injection_sinks.py +0 -0
  90. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_llm_review.py +0 -0
  91. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_lockfiles.py +0 -0
  92. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_modules_cli.py +0 -0
  93. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_pr_comment.py +0 -0
  94. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_progress.py +0 -0
  95. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_provenance.py +0 -0
  96. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_recon_to_analysis.py +0 -0
  97. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_remediate.py +0 -0
  98. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_report.py +0 -0
  99. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_review_eval.py +0 -0
  100. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_review_json.py +0 -0
  101. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_review_prompts.py +0 -0
  102. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_route_auth_fusion.py +0 -0
  103. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_sarif.py +0 -0
  104. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_sbom.py +0 -0
  105. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_scanner.py +0 -0
  106. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_security_overlay.py +0 -0
  107. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_shared_contract_imports.py +0 -0
  108. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_signal_v2.py +0 -0
  109. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_srcpaths.py +0 -0
  110. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_suggest.py +0 -0
  111. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_suppress.py +0 -0
  112. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_taint.py +0 -0
  113. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_taint_sanitizer.py +0 -0
  114. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_threat_intel.py +0 -0
  115. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_threat_model.py +0 -0
  116. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_topology.py +0 -0
  117. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_triage.py +0 -0
  118. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_weaknesses.py +0 -0
  119. {attackmap-0.4.15 → attackmap-0.4.25}/tests/test_webhardening.py +0 -0
  120. {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.15
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
  [![Python: 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://www.python.org/)
78
78
  [![PyPI](https://img.shields.io/pypi/v/attackmap.svg)](https://pypi.org/project/attackmap/)
79
79
 
80
- > **Status: beta (v0.4.15).** Core engine and 14 analyzer plugins are published
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.15) — published and validated on real codebases, but
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
  [![Python: 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://www.python.org/)
17
17
  [![PyPI](https://img.shields.io/pypi/v/attackmap.svg)](https://pypi.org/project/attackmap/)
18
18
 
19
- > **Status: beta (v0.4.15).** Core engine and 14 analyzer plugins are published
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.15) — published and validated on real codebases, but
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.15"
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.15"
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
 
@@ -225,9 +229,14 @@ class DefaultAnalyzer:
225
229
  def name(self) -> str:
226
230
  return self.metadata.name
227
231
 
228
- def analyze(self, root: str | Path, progress: "ScanProgress | None" = None) -> AnalyzerResult:
232
+ def analyze(
233
+ self, root: str | Path, progress: "ScanProgress | None" = None, recall: bool = False
234
+ ) -> AnalyzerResult:
229
235
  return scan_repo(
230
- root, suffixes=set(CODE_EXTENSIONS) - self._CLAIMED_SUFFIXES, progress=progress
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(self, root: str | Path, progress: "ScanProgress | None" = None) -> AnalyzerResult:
253
- return scan_repo(root, suffixes={".py"}, progress=progress)
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(self, root: str | Path, progress: "ScanProgress | None" = None) -> AnalyzerResult:
327
- return scan_repo(root, suffixes=self._JS_SUFFIXES, progress=progress)
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(analyzer: Analyzer, root: Path, progress: "ScanProgress | None") -> AnalyzerResult:
621
- """Invoke ``analyzer.analyze``, forwarding ``progress`` only to analyzers
622
- that accept it. Keeps the plugin ``analyze(root)`` contract intact — old
623
- plugins that don't know about progress are called unchanged."""
624
- if progress is not None:
625
- try:
626
- if "progress" in inspect.signature(analyzer.analyze).parameters:
627
- return analyzer.analyze(root, progress=progress) # type: ignore[call-arg]
628
- except (TypeError, ValueError):
629
- pass
630
- return analyzer.analyze(root)
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: