attackmap 0.4.25__tar.gz → 0.4.27__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.25/src/attackmap.egg-info → attackmap-0.4.27}/PKG-INFO +27 -9
- {attackmap-0.4.25 → attackmap-0.4.27}/README.md +26 -8
- {attackmap-0.4.25 → attackmap-0.4.27}/pyproject.toml +1 -1
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/cli.py +9 -3
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/cve.py +226 -33
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/lockfiles.py +21 -4
- {attackmap-0.4.25 → attackmap-0.4.27/src/attackmap.egg-info}/PKG-INFO +27 -9
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_cve.py +188 -17
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_lockfiles.py +54 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/LICENSE +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/setup.cfg +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/__init__.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/analyzer.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/analyzer_contracts.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/analyzers.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/anomalies.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/asset_model.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/attack_taxonomy.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/authz.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/config_scanner.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/context_pack.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/contracts.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/control_model.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/crossrepo.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/crypto.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/defensive_review.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/detection_opportunities.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/diagrams.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/diff.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/exploitability.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/fleet.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/graph.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/hunt_harness.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/insights.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/llm_review.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/merge.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/models.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/progress.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/recon_models.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/recon_to_analysis.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/report.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/review_eval.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/review_json.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/review_prompts.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/route_auth_fusion.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/sarif.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/sbom.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/scanner.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/sdk/__init__.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/sdk/contracts.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/sdk/models.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/security_overlay.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/srcpaths.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/suggest.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/suppress.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/taint.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/threat_model.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/topology.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/triage.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/weaknesses.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/webhardening.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap/workflow_scanner.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap.egg-info/SOURCES.txt +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap.egg-info/dependency_links.txt +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap.egg-info/entry_points.txt +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap.egg-info/requires.txt +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/src/attackmap.egg-info/top_level.txt +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_analyzer.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_analyzer_metadata_schema.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_analyzers.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_anomalies.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_artifact_schemas.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_authz.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_config_scanner.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_context_pack.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_contracts.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_crossrepo.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_crypto.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_defensive_review.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_diagrams.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_diff.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_exploitability.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_fleet.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_graph.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_hunt.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_hunt_harness.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_injection_sinks.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_llm_review.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_merge.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_modules_cli.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_pr_comment.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_progress.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_provenance.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_recall.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_recon_to_analysis.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_remediate.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_report.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_review_eval.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_review_json.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_review_prompts.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_route_auth_fusion.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_sarif.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_sbom.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_scanner.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_security_overlay.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_shared_contract_imports.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_signal_v2.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_srcpaths.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_suggest.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_suppress.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_taint.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_taint_sanitizer.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_threat_intel.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_threat_model.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_topology.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_triage.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_weaknesses.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/tests/test_webhardening.py +0 -0
- {attackmap-0.4.25 → attackmap-0.4.27}/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.27
|
|
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>
|
|
@@ -790,10 +790,12 @@ scaffolding, testing, and publishing instructions is in
|
|
|
790
790
|
|
|
791
791
|
```bash
|
|
792
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)
|
|
793
794
|
attackmap analyze <path> --output dir # write outputs to `dir/`
|
|
794
795
|
attackmap analyze <path> --format json # json | markdown | all (default)
|
|
795
796
|
attackmap analyze <path> --module python --module rust # only these analyzers
|
|
796
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
|
|
797
799
|
attackmap analyze <path> --llm # add LLM narrative (auto-resolve auth)
|
|
798
800
|
attackmap analyze <path> --llm --llm-backend cli # force Claude CLI
|
|
799
801
|
attackmap analyze <path> --llm --llm-provider openai # use OpenAI/Codex (OPENAI_API_KEY or `codex` CLI)
|
|
@@ -803,6 +805,8 @@ attackmap analyze <path> --hunt --verify # adjudicate each lead vs. source (conf
|
|
|
803
805
|
attackmap analyze <path> --hunt --verify --verify-votes 3 # majority vote of N independent skeptics (default 3; 1 = single pass)
|
|
804
806
|
attackmap analyze <path> --hunt --verify --hunt-lenses 4 # N lens-specialised generation passes, deduped, then verified
|
|
805
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
|
|
806
810
|
attackmap analyze <path> --remediate # LLM review-first fix suggestions (remediation.md)
|
|
807
811
|
attackmap analyze <path> --triage # cluster/dedupe/rank existing findings (triage.md; deterministic fallback)
|
|
808
812
|
attackmap analyze <path> --pr-comment pr.md # Markdown PR summary comment for CI
|
|
@@ -849,7 +853,7 @@ introduces a new HIGH finding.
|
|
|
849
853
|
|
|
850
854
|
## Project status
|
|
851
855
|
|
|
852
|
-
AttackMap is **beta** (v0.4.
|
|
856
|
+
AttackMap is **beta** (v0.4.25) — published and validated on real codebases, but
|
|
853
857
|
pre-1.0 and heuristic.
|
|
854
858
|
|
|
855
859
|
**Solid today:**
|
|
@@ -867,13 +871,27 @@ pre-1.0 and heuristic.
|
|
|
867
871
|
- BOLA/IDOR authorization detection (with custom-middleware / guard-arg awareness).
|
|
868
872
|
- Insecure-crypto / weak-randomness (Python/JS/Go/PHP) and web-hardening
|
|
869
873
|
(CORS/CSRF/cookies/CSP/debug) detection.
|
|
870
|
-
- Anomaly / outlier detection
|
|
871
|
-
|
|
872
|
-
and
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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.
|
|
877
895
|
- SBOM inventory (5 ecosystems) + OSV.dev CVE cross-reference (`--cve`).
|
|
878
896
|
- Output: Markdown + JSON + **SARIF 2.1.0** + **Mermaid / Graphviz** diagrams;
|
|
879
897
|
**diff/baseline** PR gating; a reusable **GitHub Action + PR bot**; optional
|
|
@@ -729,10 +729,12 @@ scaffolding, testing, and publishing instructions is in
|
|
|
729
729
|
|
|
730
730
|
```bash
|
|
731
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)
|
|
732
733
|
attackmap analyze <path> --output dir # write outputs to `dir/`
|
|
733
734
|
attackmap analyze <path> --format json # json | markdown | all (default)
|
|
734
735
|
attackmap analyze <path> --module python --module rust # only these analyzers
|
|
735
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
|
|
736
738
|
attackmap analyze <path> --llm # add LLM narrative (auto-resolve auth)
|
|
737
739
|
attackmap analyze <path> --llm --llm-backend cli # force Claude CLI
|
|
738
740
|
attackmap analyze <path> --llm --llm-provider openai # use OpenAI/Codex (OPENAI_API_KEY or `codex` CLI)
|
|
@@ -742,6 +744,8 @@ attackmap analyze <path> --hunt --verify # adjudicate each lead vs. source (conf
|
|
|
742
744
|
attackmap analyze <path> --hunt --verify --verify-votes 3 # majority vote of N independent skeptics (default 3; 1 = single pass)
|
|
743
745
|
attackmap analyze <path> --hunt --verify --hunt-lenses 4 # N lens-specialised generation passes, deduped, then verified
|
|
744
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
|
|
745
749
|
attackmap analyze <path> --remediate # LLM review-first fix suggestions (remediation.md)
|
|
746
750
|
attackmap analyze <path> --triage # cluster/dedupe/rank existing findings (triage.md; deterministic fallback)
|
|
747
751
|
attackmap analyze <path> --pr-comment pr.md # Markdown PR summary comment for CI
|
|
@@ -788,7 +792,7 @@ introduces a new HIGH finding.
|
|
|
788
792
|
|
|
789
793
|
## Project status
|
|
790
794
|
|
|
791
|
-
AttackMap is **beta** (v0.4.
|
|
795
|
+
AttackMap is **beta** (v0.4.25) — published and validated on real codebases, but
|
|
792
796
|
pre-1.0 and heuristic.
|
|
793
797
|
|
|
794
798
|
**Solid today:**
|
|
@@ -806,13 +810,27 @@ pre-1.0 and heuristic.
|
|
|
806
810
|
- BOLA/IDOR authorization detection (with custom-middleware / guard-arg awareness).
|
|
807
811
|
- Insecure-crypto / weak-randomness (Python/JS/Go/PHP) and web-hardening
|
|
808
812
|
(CORS/CSRF/cookies/CSP/debug) detection.
|
|
809
|
-
- Anomaly / outlier detection
|
|
810
|
-
|
|
811
|
-
and
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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.
|
|
816
834
|
- SBOM inventory (5 ecosystems) + OSV.dev CVE cross-reference (`--cve`).
|
|
817
835
|
- Output: Markdown + JSON + **SARIF 2.1.0** + **Mermaid / Graphviz** diagrams;
|
|
818
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.27"
|
|
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"
|
|
@@ -126,7 +126,9 @@ def _run_fleet(
|
|
|
126
126
|
)
|
|
127
127
|
if cve and scan.dependencies:
|
|
128
128
|
try:
|
|
129
|
-
vulns, _cve_summary = query_vulnerabilities(
|
|
129
|
+
vulns, _cve_summary = query_vulnerabilities(
|
|
130
|
+
scan.dependencies, progress=scan_progress
|
|
131
|
+
)
|
|
130
132
|
finally:
|
|
131
133
|
scan_progress.done()
|
|
132
134
|
scan.vulnerabilities = vulns
|
|
@@ -434,9 +436,13 @@ def analyze(
|
|
|
434
436
|
)
|
|
435
437
|
if cve and scan.dependencies:
|
|
436
438
|
typer.echo(f"Checking {len(scan.dependencies)} dependencies against OSV.dev…")
|
|
437
|
-
|
|
439
|
+
# query_vulnerabilities drives a determinate begin/advance bar itself
|
|
440
|
+
# (per unique package), so big lockfile trees show real progress + ETA
|
|
441
|
+
# instead of an indeterminate spinner.
|
|
438
442
|
try:
|
|
439
|
-
vulns, cve_summary = query_vulnerabilities(
|
|
443
|
+
vulns, cve_summary = query_vulnerabilities(
|
|
444
|
+
scan.dependencies, progress=scan_progress
|
|
445
|
+
)
|
|
440
446
|
finally:
|
|
441
447
|
scan_progress.done()
|
|
442
448
|
scan.vulnerabilities = vulns
|
|
@@ -7,9 +7,16 @@ and doesn't belong in an offline-only scan.
|
|
|
7
7
|
|
|
8
8
|
## Design summary
|
|
9
9
|
|
|
10
|
-
- **
|
|
11
|
-
name, version)
|
|
12
|
-
|
|
10
|
+
- **Endpoints**: ``POST https://api.osv.dev/v1/querybatch`` in chunks of
|
|
11
|
+
up to ``_BATCH_SIZE`` (ecosystem, name, version) tuples — one request
|
|
12
|
+
covers hundreds of packages instead of one request per package (a
|
|
13
|
+
lockfile-resolved monorepo can carry thousands of pinned deps; per-
|
|
14
|
+
package queries took ~15 minutes on such repos). The batch response
|
|
15
|
+
carries vulnerability IDs only, so full records are then fetched via
|
|
16
|
+
``GET https://api.osv.dev/v1/vulns/{id}`` — but only for the handful
|
|
17
|
+
of packages that actually have advisories, memoized per run. A batch
|
|
18
|
+
result that paginates (``next_page_token``, rare) falls back to the
|
|
19
|
+
classic ``POST /v1/query`` for that one package.
|
|
13
20
|
- **Concrete version resolution**: OSV needs a concrete version to
|
|
14
21
|
match against affected ranges. Manifests carry range specs
|
|
15
22
|
(``^4.16.0``, ``>=2.28,<3``, ``latest``); we extract a best-effort
|
|
@@ -48,6 +55,10 @@ logger = logging.getLogger(__name__)
|
|
|
48
55
|
|
|
49
56
|
|
|
50
57
|
_OSV_QUERY_URL = "https://api.osv.dev/v1/query"
|
|
58
|
+
_OSV_BATCH_URL = "https://api.osv.dev/v1/querybatch"
|
|
59
|
+
_OSV_VULNS_URL = "https://api.osv.dev/v1/vulns/"
|
|
60
|
+
# OSV accepts up to 1000 queries per batch call; stay comfortably under.
|
|
61
|
+
_BATCH_SIZE = 500
|
|
51
62
|
_OSV_ECOSYSTEM: dict[str, str] = {
|
|
52
63
|
"pypi": "PyPI",
|
|
53
64
|
"npm": "npm",
|
|
@@ -85,6 +96,7 @@ def query_vulnerabilities(
|
|
|
85
96
|
cache_dir: Path | None = None,
|
|
86
97
|
transport: Callable[[str, bytes], bytes] | None = None,
|
|
87
98
|
clock: Callable[[], float] | None = None,
|
|
99
|
+
progress=None,
|
|
88
100
|
) -> tuple[list[Vulnerability], LookupSummary]:
|
|
89
101
|
"""Look up CVEs for each ``DependencyHint`` and return records.
|
|
90
102
|
|
|
@@ -98,9 +110,15 @@ def query_vulnerabilities(
|
|
|
98
110
|
supply a per-test tmpdir here.
|
|
99
111
|
transport
|
|
100
112
|
Injectable network transport ``(url, body) -> response_bytes``.
|
|
101
|
-
Tests pass a stub; default is urllib.
|
|
113
|
+
Tests pass a stub; default is urllib. Detail lookups pass an
|
|
114
|
+
empty body (a GET in the default transport).
|
|
102
115
|
clock
|
|
103
116
|
Injectable ``time.time`` for deterministic cache-TTL tests.
|
|
117
|
+
progress
|
|
118
|
+
Optional duck-typed progress sink with ``begin(total, label)``
|
|
119
|
+
and ``advance(current)`` — the CLI passes its ``ScanProgress``
|
|
120
|
+
so a long lookup renders as a determinate bar, not a stalled
|
|
121
|
+
spinner.
|
|
104
122
|
"""
|
|
105
123
|
cache = _resolve_cache_dir(cache_dir)
|
|
106
124
|
cache.mkdir(parents=True, exist_ok=True)
|
|
@@ -111,6 +129,12 @@ def query_vulnerabilities(
|
|
|
111
129
|
summary = LookupSummary()
|
|
112
130
|
seen_keys: set[tuple[str, str, str, str]] = set()
|
|
113
131
|
|
|
132
|
+
# Pass 1 — resolve each hint to a concrete queryable version and dedupe
|
|
133
|
+
# to unique (ecosystem, name, version) packages. A lockfile monorepo can
|
|
134
|
+
# carry thousands of hints; querying per unique package (batched below)
|
|
135
|
+
# instead of per hint is what keeps --cve tractable.
|
|
136
|
+
entries: list[tuple[DependencyHint, tuple[str, str, str]]] = []
|
|
137
|
+
unique: dict[tuple[str, str, str], tuple[str, str, str]] = {} # key -> (osv_eco, name, concrete)
|
|
114
138
|
for dep in dependencies:
|
|
115
139
|
osv_eco = _OSV_ECOSYSTEM.get(dep.ecosystem)
|
|
116
140
|
if osv_eco is None:
|
|
@@ -125,21 +149,76 @@ def query_vulnerabilities(
|
|
|
125
149
|
if concrete is None:
|
|
126
150
|
summary.skipped_no_version += 1
|
|
127
151
|
continue
|
|
128
|
-
|
|
129
152
|
key = (dep.ecosystem, dep.name, concrete)
|
|
130
|
-
|
|
153
|
+
entries.append((dep, key))
|
|
154
|
+
unique.setdefault(key, (osv_eco, dep.name, concrete))
|
|
131
155
|
|
|
132
|
-
|
|
156
|
+
if progress is not None:
|
|
157
|
+
progress.begin(len(unique), label="Checking dependencies against OSV.dev")
|
|
158
|
+
|
|
159
|
+
# Pass 2 — serve unique packages from the cache where fresh.
|
|
160
|
+
payload_by_key: dict[tuple[str, str, str], dict] = {}
|
|
161
|
+
for key in unique:
|
|
162
|
+
cache_path = cache / (_cache_key(*key) + ".json")
|
|
163
|
+
payload = _read_cache(cache_path, now, ttl_seconds)
|
|
133
164
|
if payload is not None:
|
|
165
|
+
payload_by_key[key] = payload
|
|
134
166
|
summary.cached += 1
|
|
135
|
-
|
|
136
|
-
|
|
167
|
+
if progress is not None:
|
|
168
|
+
progress.advance(key[1])
|
|
169
|
+
|
|
170
|
+
# Pass 3 — batch-query the misses (one POST per _BATCH_SIZE packages),
|
|
171
|
+
# then fetch full advisory records only for the IDs that actually hit,
|
|
172
|
+
# memoized so a CVE shared by many packages is fetched once.
|
|
173
|
+
misses = [key for key in unique if key not in payload_by_key]
|
|
174
|
+
details_memo: dict[str, dict] = {}
|
|
175
|
+
for start in range(0, len(misses), _BATCH_SIZE):
|
|
176
|
+
chunk = misses[start : start + _BATCH_SIZE]
|
|
177
|
+
results = _fetch_osv_batch([unique[key] for key in chunk], transport, summary)
|
|
178
|
+
if results is None:
|
|
179
|
+
# Whole batch unreachable — same offline semantics as before,
|
|
180
|
+
# tallied per package so the summary line stays meaningful.
|
|
181
|
+
summary.skipped_offline += len(chunk)
|
|
182
|
+
if progress is not None:
|
|
183
|
+
for key in chunk:
|
|
184
|
+
progress.advance(key[1])
|
|
185
|
+
continue
|
|
186
|
+
for key, result in zip(chunk, results):
|
|
187
|
+
osv_eco, name, concrete = unique[key]
|
|
188
|
+
payload: dict | None
|
|
189
|
+
if not isinstance(result, dict):
|
|
190
|
+
result = {}
|
|
191
|
+
if result.get("next_page_token"):
|
|
192
|
+
# A package with >1 page of advisories (rare): fall back to
|
|
193
|
+
# the classic single query, following ITS pagination too —
|
|
194
|
+
# exactly these high-advisory packages paginate there as well.
|
|
195
|
+
payload = _fetch_osv_paged(osv_eco, name, concrete, transport, summary)
|
|
196
|
+
else:
|
|
197
|
+
# Assemble the full advisory records for this package. If any
|
|
198
|
+
# detail fetch fails, the package must NOT be cached — a
|
|
199
|
+
# partial payload would suppress a known vulnerability for the
|
|
200
|
+
# whole cache TTL. Treat it as offline for this run instead.
|
|
201
|
+
payload = _assemble_details(result, details_memo, transport, summary)
|
|
137
202
|
if payload is None:
|
|
138
|
-
|
|
139
|
-
|
|
203
|
+
summary.skipped_offline += 1
|
|
204
|
+
if progress is not None:
|
|
205
|
+
progress.advance(name)
|
|
206
|
+
continue
|
|
207
|
+
payload_by_key[key] = payload
|
|
208
|
+
_write_cache(cache / (_cache_key(*key) + ".json"), payload, now)
|
|
140
209
|
summary.queried += 1
|
|
141
|
-
if
|
|
142
|
-
|
|
210
|
+
if progress is not None:
|
|
211
|
+
progress.advance(name)
|
|
212
|
+
if transport is None and start + _BATCH_SIZE < len(misses):
|
|
213
|
+
time.sleep(_INTER_REQUEST_SLEEP_S)
|
|
214
|
+
|
|
215
|
+
# Pass 4 — fan the per-package payloads back out to every hint (dedup by
|
|
216
|
+
# (eco, name, version, id) keeps multi-lockfile repeats from double-emitting).
|
|
217
|
+
for dep, key in entries:
|
|
218
|
+
payload = payload_by_key.get(key)
|
|
219
|
+
if payload is None:
|
|
220
|
+
continue
|
|
221
|
+
concrete = key[2]
|
|
143
222
|
|
|
144
223
|
for entry in payload.get("vulns") or []:
|
|
145
224
|
vuln = _entry_to_vulnerability(entry, dep, concrete)
|
|
@@ -404,37 +483,151 @@ def _write_cache(path: Path, payload: dict, now: float) -> None:
|
|
|
404
483
|
pass
|
|
405
484
|
|
|
406
485
|
|
|
407
|
-
def
|
|
486
|
+
def _fetch_osv_batch(
|
|
487
|
+
packages: list[tuple[str, str, str]],
|
|
488
|
+
transport: Callable[[str, bytes], bytes] | None,
|
|
489
|
+
summary: LookupSummary,
|
|
490
|
+
) -> list[dict] | None:
|
|
491
|
+
"""One ``POST /v1/querybatch`` for up to ``_BATCH_SIZE`` packages.
|
|
492
|
+
|
|
493
|
+
Returns the ``results`` list (aligned with ``packages`` by index — OSV
|
|
494
|
+
guarantees request order) or ``None`` on a network/shape failure, in
|
|
495
|
+
which case the whole chunk is treated as offline.
|
|
496
|
+
"""
|
|
497
|
+
body = json.dumps(
|
|
498
|
+
{
|
|
499
|
+
"queries": [
|
|
500
|
+
{"package": {"name": name, "ecosystem": osv_eco}, "version": version}
|
|
501
|
+
for osv_eco, name, version in packages
|
|
502
|
+
]
|
|
503
|
+
}
|
|
504
|
+
).encode("utf-8")
|
|
505
|
+
raw = _http(_OSV_BATCH_URL, body, transport, summary)
|
|
506
|
+
if raw is None:
|
|
507
|
+
return None
|
|
508
|
+
try:
|
|
509
|
+
payload = json.loads(raw.decode("utf-8") if isinstance(raw, bytes) else raw)
|
|
510
|
+
except json.JSONDecodeError:
|
|
511
|
+
summary.network_errors += 1
|
|
512
|
+
return None
|
|
513
|
+
results = payload.get("results") if isinstance(payload, dict) else None
|
|
514
|
+
if not isinstance(results, list) or len(results) != len(packages):
|
|
515
|
+
summary.network_errors += 1
|
|
516
|
+
return None
|
|
517
|
+
return results
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _assemble_details(
|
|
521
|
+
result: dict,
|
|
522
|
+
details_memo: dict[str, dict],
|
|
523
|
+
transport: Callable[[str, bytes], bytes] | None,
|
|
524
|
+
summary: LookupSummary,
|
|
525
|
+
) -> dict | None:
|
|
526
|
+
"""Turn one batch result's ID references into full advisory records.
|
|
527
|
+
|
|
528
|
+
Returns ``None`` if any advertised detail could not be fetched — the
|
|
529
|
+
caller must then skip caching the package, because caching a partial
|
|
530
|
+
payload would suppress a known vulnerability for the whole TTL (P1).
|
|
531
|
+
"""
|
|
532
|
+
full_entries = []
|
|
533
|
+
for ref in result.get("vulns") or []:
|
|
534
|
+
vuln_id = str(ref.get("id", "")) if isinstance(ref, dict) else ""
|
|
535
|
+
if not vuln_id:
|
|
536
|
+
continue # shape anomaly, not a fetch failure — nothing to fetch
|
|
537
|
+
entry = details_memo.get(vuln_id)
|
|
538
|
+
if entry is None:
|
|
539
|
+
entry = _fetch_osv_vuln(vuln_id, transport, summary)
|
|
540
|
+
if entry is None:
|
|
541
|
+
return None
|
|
542
|
+
details_memo[vuln_id] = entry
|
|
543
|
+
full_entries.append(entry)
|
|
544
|
+
return {"vulns": full_entries} if full_entries else {}
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _fetch_osv_paged(
|
|
408
548
|
osv_ecosystem: str,
|
|
409
549
|
name: str,
|
|
410
550
|
version: str,
|
|
411
551
|
transport: Callable[[str, bytes], bytes] | None,
|
|
412
552
|
summary: LookupSummary,
|
|
413
553
|
) -> dict | None:
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
554
|
+
"""Classic ``POST /v1/query``, following ``next_page_token`` to the end.
|
|
555
|
+
|
|
556
|
+
Returns the union of all pages' ``vulns``, or ``None`` if any page fails
|
|
557
|
+
— never a partial result, for the same don't-cache-partials reason as
|
|
558
|
+
:func:`_assemble_details` (P2: single-query responses paginate too).
|
|
559
|
+
"""
|
|
560
|
+
all_vulns: list[dict] = []
|
|
561
|
+
page_token: str | None = None
|
|
562
|
+
while True:
|
|
563
|
+
query: dict = {
|
|
564
|
+
"package": {"name": name, "ecosystem": osv_ecosystem},
|
|
565
|
+
"version": version,
|
|
566
|
+
}
|
|
567
|
+
if page_token:
|
|
568
|
+
query["page_token"] = page_token
|
|
569
|
+
raw = _http(_OSV_QUERY_URL, json.dumps(query).encode("utf-8"), transport, summary)
|
|
570
|
+
if raw is None:
|
|
571
|
+
return None
|
|
572
|
+
try:
|
|
573
|
+
payload = json.loads(raw.decode("utf-8") if isinstance(raw, bytes) else raw)
|
|
574
|
+
except json.JSONDecodeError:
|
|
575
|
+
summary.network_errors += 1
|
|
576
|
+
return None
|
|
577
|
+
if not isinstance(payload, dict):
|
|
578
|
+
summary.network_errors += 1
|
|
579
|
+
return None
|
|
580
|
+
all_vulns.extend(payload.get("vulns") or [])
|
|
581
|
+
page_token = payload.get("next_page_token")
|
|
582
|
+
if not page_token:
|
|
583
|
+
break
|
|
584
|
+
if transport is None:
|
|
585
|
+
time.sleep(_INTER_REQUEST_SLEEP_S)
|
|
586
|
+
return {"vulns": all_vulns} if all_vulns else {}
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _fetch_osv_vuln(
|
|
590
|
+
vuln_id: str,
|
|
591
|
+
transport: Callable[[str, bytes], bytes] | None,
|
|
592
|
+
summary: LookupSummary,
|
|
593
|
+
) -> dict | None:
|
|
594
|
+
"""Fetch one full advisory record via ``GET /v1/vulns/{id}``."""
|
|
595
|
+
raw = _http(_OSV_VULNS_URL + vuln_id, b"", transport, summary)
|
|
596
|
+
if raw is None:
|
|
597
|
+
return None
|
|
417
598
|
try:
|
|
418
|
-
if
|
|
419
|
-
|
|
420
|
-
else:
|
|
421
|
-
request = urllib.request.Request(
|
|
422
|
-
_OSV_QUERY_URL,
|
|
423
|
-
data=body,
|
|
424
|
-
headers={"Content-Type": "application/json"},
|
|
425
|
-
method="POST",
|
|
426
|
-
)
|
|
427
|
-
with urllib.request.urlopen(request, timeout=_REQUEST_TIMEOUT_S) as response:
|
|
428
|
-
raw = response.read()
|
|
429
|
-
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
|
599
|
+
entry = json.loads(raw.decode("utf-8") if isinstance(raw, bytes) else raw)
|
|
600
|
+
except json.JSONDecodeError:
|
|
430
601
|
summary.network_errors += 1
|
|
431
|
-
summary.skipped_offline += 1
|
|
432
|
-
logger.debug("OSV network error for %s/%s@%s: %s", osv_ecosystem, name, version, exc)
|
|
433
602
|
return None
|
|
603
|
+
return entry if isinstance(entry, dict) else None
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _http(
|
|
607
|
+
url: str,
|
|
608
|
+
body: bytes,
|
|
609
|
+
transport: Callable[[str, bytes], bytes] | None,
|
|
610
|
+
summary: LookupSummary,
|
|
611
|
+
) -> bytes | None:
|
|
612
|
+
"""One HTTP exchange: POST when ``body`` is non-empty, GET otherwise.
|
|
613
|
+
|
|
614
|
+
The injectable ``transport`` sees ``(url, body)`` either way, so test
|
|
615
|
+
stubs dispatch on the URL.
|
|
616
|
+
"""
|
|
434
617
|
try:
|
|
435
|
-
|
|
436
|
-
|
|
618
|
+
if transport is not None:
|
|
619
|
+
return transport(url, body)
|
|
620
|
+
request = urllib.request.Request(
|
|
621
|
+
url,
|
|
622
|
+
data=body or None,
|
|
623
|
+
headers={"Content-Type": "application/json"} if body else {},
|
|
624
|
+
method="POST" if body else "GET",
|
|
625
|
+
)
|
|
626
|
+
with urllib.request.urlopen(request, timeout=_REQUEST_TIMEOUT_S) as response:
|
|
627
|
+
return response.read()
|
|
628
|
+
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
|
437
629
|
summary.network_errors += 1
|
|
630
|
+
logger.debug("OSV network error for %s: %s", url, exc)
|
|
438
631
|
return None
|
|
439
632
|
|
|
440
633
|
|
|
@@ -263,11 +263,29 @@ def _parse_pnpm_lock(path: Path) -> _Graph | None:
|
|
|
263
263
|
import yaml
|
|
264
264
|
except ImportError: # pragma: no cover - PyYAML is a declared dependency
|
|
265
265
|
return None
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
# pnpm-lock is normally a single YAML document, but some files concatenate
|
|
267
|
+
# several with `---` separators (e.g. a merge artifact, or a tool that
|
|
268
|
+
# appends per-workspace lockfiles). Parse every document and merge them, and
|
|
269
|
+
# never let a malformed-YAML error abort the whole scan — a lockfile we can't
|
|
270
|
+
# read just yields no dependency hints.
|
|
271
|
+
try:
|
|
272
|
+
documents = [
|
|
273
|
+
doc
|
|
274
|
+
for doc in yaml.safe_load_all(path.read_text(encoding="utf-8"))
|
|
275
|
+
if isinstance(doc, dict)
|
|
276
|
+
]
|
|
277
|
+
except yaml.YAMLError:
|
|
278
|
+
return None
|
|
279
|
+
if not documents:
|
|
268
280
|
return None
|
|
269
281
|
graph = _Graph(ecosystem="npm")
|
|
282
|
+
for data in documents:
|
|
283
|
+
_absorb_pnpm_document(data, graph)
|
|
284
|
+
return graph
|
|
270
285
|
|
|
286
|
+
|
|
287
|
+
def _absorb_pnpm_document(data: dict, graph: _Graph) -> None:
|
|
288
|
+
"""Fold one parsed pnpm-lock YAML document into ``graph`` (roots + packages)."""
|
|
271
289
|
# Roots: importers.'.'.{dependencies,devDependencies} (v6+) or top-level.
|
|
272
290
|
importers = data.get("importers")
|
|
273
291
|
root_sections = []
|
|
@@ -285,7 +303,7 @@ def _parse_pnpm_lock(path: Path) -> _Graph | None:
|
|
|
285
303
|
|
|
286
304
|
packages = data.get("packages")
|
|
287
305
|
if not isinstance(packages, dict):
|
|
288
|
-
return
|
|
306
|
+
return
|
|
289
307
|
for raw_key, meta in packages.items():
|
|
290
308
|
m = _PNPM_PKG_KEY_RE.match(str(raw_key))
|
|
291
309
|
if not m:
|
|
@@ -302,7 +320,6 @@ def _parse_pnpm_lock(path: Path) -> _Graph | None:
|
|
|
302
320
|
children.update(deps.keys())
|
|
303
321
|
if children:
|
|
304
322
|
graph.edges.setdefault(name, set()).update(children)
|
|
305
|
-
return graph
|
|
306
323
|
|
|
307
324
|
|
|
308
325
|
# --- pypi: poetry.lock / uv.lock; cargo: Cargo.lock (all TOML [[package]]) --
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: attackmap
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.27
|
|
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>
|
|
@@ -790,10 +790,12 @@ scaffolding, testing, and publishing instructions is in
|
|
|
790
790
|
|
|
791
791
|
```bash
|
|
792
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)
|
|
793
794
|
attackmap analyze <path> --output dir # write outputs to `dir/`
|
|
794
795
|
attackmap analyze <path> --format json # json | markdown | all (default)
|
|
795
796
|
attackmap analyze <path> --module python --module rust # only these analyzers
|
|
796
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
|
|
797
799
|
attackmap analyze <path> --llm # add LLM narrative (auto-resolve auth)
|
|
798
800
|
attackmap analyze <path> --llm --llm-backend cli # force Claude CLI
|
|
799
801
|
attackmap analyze <path> --llm --llm-provider openai # use OpenAI/Codex (OPENAI_API_KEY or `codex` CLI)
|
|
@@ -803,6 +805,8 @@ attackmap analyze <path> --hunt --verify # adjudicate each lead vs. source (conf
|
|
|
803
805
|
attackmap analyze <path> --hunt --verify --verify-votes 3 # majority vote of N independent skeptics (default 3; 1 = single pass)
|
|
804
806
|
attackmap analyze <path> --hunt --verify --hunt-lenses 4 # N lens-specialised generation passes, deduped, then verified
|
|
805
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
|
|
806
810
|
attackmap analyze <path> --remediate # LLM review-first fix suggestions (remediation.md)
|
|
807
811
|
attackmap analyze <path> --triage # cluster/dedupe/rank existing findings (triage.md; deterministic fallback)
|
|
808
812
|
attackmap analyze <path> --pr-comment pr.md # Markdown PR summary comment for CI
|
|
@@ -849,7 +853,7 @@ introduces a new HIGH finding.
|
|
|
849
853
|
|
|
850
854
|
## Project status
|
|
851
855
|
|
|
852
|
-
AttackMap is **beta** (v0.4.
|
|
856
|
+
AttackMap is **beta** (v0.4.25) — published and validated on real codebases, but
|
|
853
857
|
pre-1.0 and heuristic.
|
|
854
858
|
|
|
855
859
|
**Solid today:**
|
|
@@ -867,13 +871,27 @@ pre-1.0 and heuristic.
|
|
|
867
871
|
- BOLA/IDOR authorization detection (with custom-middleware / guard-arg awareness).
|
|
868
872
|
- Insecure-crypto / weak-randomness (Python/JS/Go/PHP) and web-hardening
|
|
869
873
|
(CORS/CSRF/cookies/CSP/debug) detection.
|
|
870
|
-
- Anomaly / outlier detection
|
|
871
|
-
|
|
872
|
-
and
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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.
|
|
877
895
|
- SBOM inventory (5 ecosystems) + OSV.dev CVE cross-reference (`--cve`).
|
|
878
896
|
- Output: Markdown + JSON + **SARIF 2.1.0** + **Mermaid / Graphviz** diagrams;
|
|
879
897
|
**diff/baseline** PR gating; a reusable **GitHub Action + PR bot**; optional
|