attackmap 0.4.2__tar.gz → 0.4.4__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.2/src/attackmap.egg-info → attackmap-0.4.4}/PKG-INFO +13 -2
- {attackmap-0.4.2 → attackmap-0.4.4}/README.md +11 -1
- {attackmap-0.4.2 → attackmap-0.4.4}/pyproject.toml +2 -1
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/__init__.py +1 -1
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/cli.py +65 -10
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/llm_review.py +285 -10
- {attackmap-0.4.2 → attackmap-0.4.4/src/attackmap.egg-info}/PKG-INFO +13 -2
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap.egg-info/SOURCES.txt +1 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap.egg-info/requires.txt +1 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_llm_review.py +284 -2
- attackmap-0.4.4/tests/test_modules_cli.py +44 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/LICENSE +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/setup.cfg +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/analyzer.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/analyzer_contracts.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/analyzers.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/anomalies.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/asset_model.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/attack_taxonomy.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/authz.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/config_scanner.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/context_pack.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/control_model.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/crypto.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/cve.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/defensive_review.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/detection_opportunities.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/diagrams.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/diff.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/exploitability.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/graph.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/insights.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/merge.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/models.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/progress.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/recon_models.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/recon_to_analysis.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/report.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/review_eval.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/review_json.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/review_prompts.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/sarif.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/sbom.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/scanner.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/sdk/__init__.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/sdk/contracts.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/sdk/models.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/security_overlay.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/srcpaths.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/suggest.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/taint.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/threat_model.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/topology.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/weaknesses.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/webhardening.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap.egg-info/dependency_links.txt +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap.egg-info/entry_points.txt +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap.egg-info/top_level.txt +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_analyzer.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_analyzer_metadata_schema.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_analyzers.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_anomalies.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_artifact_schemas.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_authz.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_config_scanner.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_context_pack.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_crypto.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_cve.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_defensive_review.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_diagrams.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_diff.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_exploitability.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_graph.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_hunt.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_injection_sinks.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_merge.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_pr_comment.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_progress.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_provenance.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_recon_to_analysis.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_remediate.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_report.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_review_eval.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_review_json.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_review_prompts.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_sarif.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_sbom.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_scanner.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_security_overlay.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_shared_contract_imports.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_signal_v2.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_srcpaths.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_suggest.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_taint.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_threat_intel.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_threat_model.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_topology.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_weaknesses.py +0 -0
- {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_webhardening.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.4
|
|
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>
|
|
@@ -36,6 +36,7 @@ Requires-Dist: pydantic>=2.7.0
|
|
|
36
36
|
Requires-Dist: networkx>=3.3
|
|
37
37
|
Provides-Extra: llm
|
|
38
38
|
Requires-Dist: anthropic>=0.40.0; extra == "llm"
|
|
39
|
+
Requires-Dist: openai>=1.60.0; extra == "llm"
|
|
39
40
|
Provides-Extra: all
|
|
40
41
|
Requires-Dist: attackmap[llm]; extra == "all"
|
|
41
42
|
Requires-Dist: attackmap-analyzer-python>=0.1.0; extra == "all"
|
|
@@ -105,6 +106,12 @@ to use your existing Pro/Max subscription:
|
|
|
105
106
|
attackmap analyze /path/to/repo --output reports --llm
|
|
106
107
|
```
|
|
107
108
|
|
|
109
|
+
Prefer OpenAI? Add `--llm-provider openai` to run the same review/hunt/remediate
|
|
110
|
+
phases on OpenAI. Set `OPENAI_API_KEY` for the Responses API, or log in once with
|
|
111
|
+
the [`codex` CLI](https://developers.openai.com/codex) to use your subscription
|
|
112
|
+
(`--llm-backend cli`). Default model is `gpt-5-codex`; `--llm-model` accepts any
|
|
113
|
+
model ID verbatim.
|
|
114
|
+
|
|
108
115
|
Read `reports/defensive-review.md` (heuristic) and `reports/defensive-review-llm.md`
|
|
109
116
|
(LLM-narrated) side by side.
|
|
110
117
|
|
|
@@ -482,7 +489,9 @@ trust-boundary violations, and more.
|
|
|
482
489
|
|
|
483
490
|
**4. LLM narrative review.** With `--llm`, Claude Opus generates a final review
|
|
484
491
|
from the structured evidence pack. The model is forced to cite real
|
|
485
|
-
surface/asset/control IDs, so it can't invent findings.
|
|
492
|
+
surface/asset/control IDs, so it can't invent findings. Add `--llm-provider
|
|
493
|
+
openai` to run the same phase on OpenAI/Codex instead (`gpt-5-codex` by default,
|
|
494
|
+
via `OPENAI_API_KEY` or the `codex` CLI) — the grounding contract is identical.
|
|
486
495
|
|
|
487
496
|
**5. Vulnerability-hypothesis hunting (`--hunt`).** The honest core of the
|
|
488
497
|
"find the unknown" ask. `attackmap analyze . --hunt` has Claude reason over the
|
|
@@ -542,6 +551,8 @@ attackmap analyze <path> --module python --module rust # only these analyzers
|
|
|
542
551
|
attackmap analyze <path> --cve # cross-reference SBOM against OSV.dev
|
|
543
552
|
attackmap analyze <path> --llm # add LLM narrative (auto-resolve auth)
|
|
544
553
|
attackmap analyze <path> --llm --llm-backend cli # force Claude CLI
|
|
554
|
+
attackmap analyze <path> --llm --llm-provider openai # use OpenAI/Codex (OPENAI_API_KEY or `codex` CLI)
|
|
555
|
+
attackmap analyze <path> --llm --llm-provider openai --llm-model gpt-5.5 # any OpenAI model ID, passed through
|
|
545
556
|
attackmap analyze <path> --hunt # LLM vulnerability-hypothesis hunt (leads to confirm)
|
|
546
557
|
attackmap analyze <path> --hunt --verify # adjudicate each lead vs. source (confirmed/refuted/review)
|
|
547
558
|
attackmap analyze <path> --remediate # LLM review-first fix suggestions (remediation.md)
|
|
@@ -46,6 +46,12 @@ to use your existing Pro/Max subscription:
|
|
|
46
46
|
attackmap analyze /path/to/repo --output reports --llm
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
Prefer OpenAI? Add `--llm-provider openai` to run the same review/hunt/remediate
|
|
50
|
+
phases on OpenAI. Set `OPENAI_API_KEY` for the Responses API, or log in once with
|
|
51
|
+
the [`codex` CLI](https://developers.openai.com/codex) to use your subscription
|
|
52
|
+
(`--llm-backend cli`). Default model is `gpt-5-codex`; `--llm-model` accepts any
|
|
53
|
+
model ID verbatim.
|
|
54
|
+
|
|
49
55
|
Read `reports/defensive-review.md` (heuristic) and `reports/defensive-review-llm.md`
|
|
50
56
|
(LLM-narrated) side by side.
|
|
51
57
|
|
|
@@ -423,7 +429,9 @@ trust-boundary violations, and more.
|
|
|
423
429
|
|
|
424
430
|
**4. LLM narrative review.** With `--llm`, Claude Opus generates a final review
|
|
425
431
|
from the structured evidence pack. The model is forced to cite real
|
|
426
|
-
surface/asset/control IDs, so it can't invent findings.
|
|
432
|
+
surface/asset/control IDs, so it can't invent findings. Add `--llm-provider
|
|
433
|
+
openai` to run the same phase on OpenAI/Codex instead (`gpt-5-codex` by default,
|
|
434
|
+
via `OPENAI_API_KEY` or the `codex` CLI) — the grounding contract is identical.
|
|
427
435
|
|
|
428
436
|
**5. Vulnerability-hypothesis hunting (`--hunt`).** The honest core of the
|
|
429
437
|
"find the unknown" ask. `attackmap analyze . --hunt` has Claude reason over the
|
|
@@ -483,6 +491,8 @@ attackmap analyze <path> --module python --module rust # only these analyzers
|
|
|
483
491
|
attackmap analyze <path> --cve # cross-reference SBOM against OSV.dev
|
|
484
492
|
attackmap analyze <path> --llm # add LLM narrative (auto-resolve auth)
|
|
485
493
|
attackmap analyze <path> --llm --llm-backend cli # force Claude CLI
|
|
494
|
+
attackmap analyze <path> --llm --llm-provider openai # use OpenAI/Codex (OPENAI_API_KEY or `codex` CLI)
|
|
495
|
+
attackmap analyze <path> --llm --llm-provider openai --llm-model gpt-5.5 # any OpenAI model ID, passed through
|
|
486
496
|
attackmap analyze <path> --hunt # LLM vulnerability-hypothesis hunt (leads to confirm)
|
|
487
497
|
attackmap analyze <path> --hunt --verify # adjudicate each lead vs. source (confirmed/refuted/review)
|
|
488
498
|
attackmap analyze <path> --remediate # LLM review-first fix suggestions (remediation.md)
|
|
@@ -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.4"
|
|
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"
|
|
@@ -56,6 +56,7 @@ dependencies = [
|
|
|
56
56
|
[project.optional-dependencies]
|
|
57
57
|
llm = [
|
|
58
58
|
"anthropic>=0.40.0",
|
|
59
|
+
"openai>=1.60.0",
|
|
59
60
|
]
|
|
60
61
|
all = [
|
|
61
62
|
"attackmap[llm]",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.4.
|
|
2
|
+
__version__ = "0.4.4"
|
|
@@ -69,20 +69,30 @@ def analyze(
|
|
|
69
69
|
"--llm",
|
|
70
70
|
help="Generate a narrative defensive review by calling Claude with the evidence pack. Auth resolves automatically: ANTHROPIC_API_KEY → ANTHROPIC_AUTH_TOKEN → `claude` CLI (subscription auth). Force a backend with --llm-backend.",
|
|
71
71
|
),
|
|
72
|
+
llm_provider: str = typer.Option(
|
|
73
|
+
"claude",
|
|
74
|
+
"--llm-provider",
|
|
75
|
+
help="LLM provider for --llm/--hunt/--remediate: 'claude' (default, Anthropic) or 'openai' (OpenAI/Codex).",
|
|
76
|
+
),
|
|
72
77
|
llm_model: str | None = typer.Option(
|
|
73
78
|
None,
|
|
74
79
|
"--llm-model",
|
|
75
|
-
help="
|
|
80
|
+
help="Model ID for --llm. Claude provider: e.g. claude-opus-4-8 (default), claude-fable-5, claude-sonnet-5, claude-opus-4-7. OpenAI provider: e.g. gpt-5-codex (default), gpt-5.5, gpt-5.4, gpt-5.4-mini — any model ID is passed through verbatim.",
|
|
76
81
|
),
|
|
77
82
|
llm_effort: str | None = typer.Option(
|
|
78
83
|
None,
|
|
79
84
|
"--llm-effort",
|
|
80
|
-
help="Effort for --llm: low|medium|high|xhigh|max. Defaults to high.",
|
|
85
|
+
help="Effort for --llm: low|medium|high|xhigh|max. Defaults to high. For OpenAI, xhigh/max clamp to high (Responses API reasoning effort tops out at high).",
|
|
81
86
|
),
|
|
82
87
|
llm_backend: str = typer.Option(
|
|
83
88
|
"auto",
|
|
84
89
|
"--llm-backend",
|
|
85
|
-
help="Which backend --llm uses: 'auto'
|
|
90
|
+
help="Which backend --llm uses. Claude: 'auto' tries ANTHROPIC_API_KEY → ANTHROPIC_AUTH_TOKEN → `claude` CLI; 'api' forces the SDK; 'cli' forces the `claude` CLI. OpenAI: 'auto' tries OPENAI_API_KEY then the `codex` CLI; 'api' forces the OpenAI SDK; 'cli' forces `codex exec` (your `codex login` subscription).",
|
|
91
|
+
),
|
|
92
|
+
llm_speed: str = typer.Option(
|
|
93
|
+
"standard",
|
|
94
|
+
"--llm-speed",
|
|
95
|
+
help="LLM output speed: 'standard' (default) or 'fast'. Fast mode (~2.5x output speed, premium price) applies only to Opus 4.8/4.7 via the API backend; other models/backends fall back to standard.",
|
|
86
96
|
),
|
|
87
97
|
hunt: bool = typer.Option(
|
|
88
98
|
False,
|
|
@@ -155,6 +165,17 @@ def analyze(
|
|
|
155
165
|
|
|
156
166
|
if progress_format not in {"auto", "tty", "json", "none"}:
|
|
157
167
|
raise typer.BadParameter("--progress-format must be one of: auto, json, none.")
|
|
168
|
+
if llm_speed not in {"standard", "fast"}:
|
|
169
|
+
raise typer.BadParameter("--llm-speed must be one of: standard, fast.")
|
|
170
|
+
if llm_provider not in {"claude", "openai"}:
|
|
171
|
+
raise typer.BadParameter("--llm-provider must be one of: claude, openai.")
|
|
172
|
+
# Provider-aware label for the "via X" progress/echo strings.
|
|
173
|
+
llm_display = "Codex" if llm_provider == "openai" else "Claude"
|
|
174
|
+
if llm_provider == "openai" and llm_speed == "fast" and (llm or hunt or remediate):
|
|
175
|
+
typer.echo(
|
|
176
|
+
"Note: --llm-speed fast is Claude-only; ignoring it for the OpenAI provider.",
|
|
177
|
+
err=True,
|
|
178
|
+
)
|
|
158
179
|
active_analyzers = resolve_run_analyzers(repo_path, analyzers=selected_analyzers)
|
|
159
180
|
scan_progress = create_progress(progress_format, no_progress=no_progress)
|
|
160
181
|
scan = analyze_repository(repo_path, analyzers=active_analyzers, progress=scan_progress)
|
|
@@ -259,9 +280,11 @@ def analyze(
|
|
|
259
280
|
|
|
260
281
|
typer.echo("")
|
|
261
282
|
typer.echo(
|
|
262
|
-
f"Generating narrative review via
|
|
283
|
+
f"Generating narrative review via {llm_display} (backend={llm_backend}, may take a minute)..."
|
|
284
|
+
)
|
|
285
|
+
scan_progress.stage(
|
|
286
|
+
f"{llm_display} is writing the defensive review (backend={llm_backend})"
|
|
263
287
|
)
|
|
264
|
-
scan_progress.stage(f"Claude is writing the defensive review (backend={llm_backend})")
|
|
265
288
|
try:
|
|
266
289
|
result = generate_llm_review(
|
|
267
290
|
scan,
|
|
@@ -271,6 +294,8 @@ def analyze(
|
|
|
271
294
|
model=llm_model,
|
|
272
295
|
effort=effort_value, # type: ignore[arg-type]
|
|
273
296
|
backend=llm_backend, # type: ignore[arg-type]
|
|
297
|
+
speed=llm_speed, # type: ignore[arg-type]
|
|
298
|
+
provider=llm_provider, # type: ignore[arg-type]
|
|
274
299
|
)
|
|
275
300
|
finally:
|
|
276
301
|
scan_progress.done()
|
|
@@ -312,11 +337,11 @@ def analyze(
|
|
|
312
337
|
typer.echo("")
|
|
313
338
|
hunt_mode = "hunt_verify" if verify else "hunt"
|
|
314
339
|
typer.echo(
|
|
315
|
-
f"Hunting for vulnerability hypotheses via
|
|
340
|
+
f"Hunting for vulnerability hypotheses via {llm_display} "
|
|
316
341
|
f"({'adjudicated against source, ' if verify else ''}backend={llm_backend}, may take a minute)..."
|
|
317
342
|
)
|
|
318
343
|
scan_progress.stage(
|
|
319
|
-
f"
|
|
344
|
+
f"{llm_display} is hunting exploit-chain hypotheses"
|
|
320
345
|
f"{' + verifying against source' if verify else ''} (backend={llm_backend})"
|
|
321
346
|
)
|
|
322
347
|
try:
|
|
@@ -329,6 +354,8 @@ def analyze(
|
|
|
329
354
|
effort=hunt_effort_value, # type: ignore[arg-type]
|
|
330
355
|
backend=llm_backend, # type: ignore[arg-type]
|
|
331
356
|
mode=hunt_mode, # type: ignore[arg-type]
|
|
357
|
+
speed=llm_speed, # type: ignore[arg-type]
|
|
358
|
+
provider=llm_provider, # type: ignore[arg-type]
|
|
332
359
|
)
|
|
333
360
|
finally:
|
|
334
361
|
scan_progress.done()
|
|
@@ -372,9 +399,11 @@ def analyze(
|
|
|
372
399
|
)
|
|
373
400
|
typer.echo("")
|
|
374
401
|
typer.echo(
|
|
375
|
-
f"Generating remediation suggestions via
|
|
402
|
+
f"Generating remediation suggestions via {llm_display} (backend={llm_backend}, may take a minute)..."
|
|
403
|
+
)
|
|
404
|
+
scan_progress.stage(
|
|
405
|
+
f"{llm_display} is drafting remediation suggestions (backend={llm_backend})"
|
|
376
406
|
)
|
|
377
|
-
scan_progress.stage(f"Claude is drafting remediation suggestions (backend={llm_backend})")
|
|
378
407
|
try:
|
|
379
408
|
rem_result = generate_llm_review(
|
|
380
409
|
scan,
|
|
@@ -385,6 +414,8 @@ def analyze(
|
|
|
385
414
|
effort=rem_effort_value, # type: ignore[arg-type]
|
|
386
415
|
backend=llm_backend, # type: ignore[arg-type]
|
|
387
416
|
mode="remediate",
|
|
417
|
+
speed=llm_speed, # type: ignore[arg-type]
|
|
418
|
+
provider=llm_provider, # type: ignore[arg-type]
|
|
388
419
|
)
|
|
389
420
|
finally:
|
|
390
421
|
scan_progress.done()
|
|
@@ -418,8 +449,32 @@ def analyze(
|
|
|
418
449
|
|
|
419
450
|
|
|
420
451
|
@app.command("modules")
|
|
421
|
-
def modules(
|
|
452
|
+
def modules(
|
|
453
|
+
json_output: bool = typer.Option(
|
|
454
|
+
False,
|
|
455
|
+
"--json",
|
|
456
|
+
help="Emit the installed analyzer modules as a JSON array "
|
|
457
|
+
"(name/display_name/description/scope/ecosystems/enabled_by_default). "
|
|
458
|
+
"Network-free — the remote module-repository section is omitted. "
|
|
459
|
+
"Intended for tool / GUI front-ends that offer analyzer selection.",
|
|
460
|
+
),
|
|
461
|
+
) -> None:
|
|
422
462
|
available_modules = get_available_modules()
|
|
463
|
+
if json_output:
|
|
464
|
+
payload = [
|
|
465
|
+
{
|
|
466
|
+
"name": meta.name,
|
|
467
|
+
"display_name": meta.display_name or meta.name,
|
|
468
|
+
"description": meta.description,
|
|
469
|
+
"scope": meta.scope,
|
|
470
|
+
"ecosystems": list(meta.ecosystems),
|
|
471
|
+
"enabled_by_default": meta.enabled_by_default,
|
|
472
|
+
}
|
|
473
|
+
for meta in available_modules
|
|
474
|
+
]
|
|
475
|
+
typer.echo(json.dumps(payload, indent=2))
|
|
476
|
+
return
|
|
477
|
+
|
|
423
478
|
if not available_modules:
|
|
424
479
|
typer.echo("No analyzer modules are currently available.")
|
|
425
480
|
else:
|
|
@@ -37,11 +37,34 @@ from .review_prompts import (
|
|
|
37
37
|
render_review_prompts,
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
-
DEFAULT_MODEL = "claude-opus-4-
|
|
40
|
+
DEFAULT_MODEL = "claude-opus-4-8"
|
|
41
41
|
DEFAULT_EFFORT: Literal["low", "medium", "high", "xhigh", "max"] = "high"
|
|
42
42
|
DEFAULT_MAX_TOKENS = 32000
|
|
43
|
+
|
|
44
|
+
# Fast mode (2.5x output speed, premium price) is a research-preview beta and
|
|
45
|
+
# only runs on the Opus 4.8 / 4.7 tiers via the API backend — the `claude` CLI
|
|
46
|
+
# backend and other models silently fall back to standard speed.
|
|
47
|
+
FAST_MODE_BETA = "fast-mode-2026-02-01"
|
|
48
|
+
FAST_CAPABLE_MODELS = frozenset({"claude-opus-4-8", "claude-opus-4-7"})
|
|
43
49
|
CLAUDE_CLI_TIMEOUT_SECONDS = 600
|
|
44
50
|
|
|
51
|
+
# OpenAI / Codex provider. `gpt-5-codex` is the code-optimized GPT-5 (Responses
|
|
52
|
+
# API only) and the safe default; any other model ID is passed through verbatim
|
|
53
|
+
# so we never have to chase OpenAI's point-release churn. Reasoning effort maps
|
|
54
|
+
# onto the Responses API's `reasoning.effort` — OpenAI exposes low/medium/high,
|
|
55
|
+
# so our extended tiers (xhigh/max) clamp down to "high".
|
|
56
|
+
OPENAI_DEFAULT_MODEL = "gpt-5-codex"
|
|
57
|
+
OPENAI_API_TIMEOUT_SECONDS = 900
|
|
58
|
+
CODEX_CLI_TIMEOUT_SECONDS = 900
|
|
59
|
+
OPENAI_EFFORT_MAP = {
|
|
60
|
+
"low": "low",
|
|
61
|
+
"medium": "medium",
|
|
62
|
+
"high": "high",
|
|
63
|
+
"xhigh": "high",
|
|
64
|
+
"max": "high",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
LlmProvider = Literal["claude", "openai"]
|
|
45
68
|
LlmBackend = Literal["auto", "api", "cli"]
|
|
46
69
|
|
|
47
70
|
|
|
@@ -121,6 +144,7 @@ def _run_via_sdk(
|
|
|
121
144
|
max_tokens: int,
|
|
122
145
|
api_key: str | None,
|
|
123
146
|
client: Any | None,
|
|
147
|
+
speed: str = "standard",
|
|
124
148
|
) -> LlmReviewResult:
|
|
125
149
|
sdk_client, _ = _resolve_sdk_client(api_key, client)
|
|
126
150
|
|
|
@@ -135,8 +159,17 @@ def _run_via_sdk(
|
|
|
135
159
|
"output_config": {"effort": effort},
|
|
136
160
|
}
|
|
137
161
|
|
|
162
|
+
# Fast mode needs the beta endpoint + flag + top-level speed, and only on
|
|
163
|
+
# the Opus 4.8/4.7 tiers. Anything else runs at standard speed.
|
|
164
|
+
fast = speed == "fast" and model in FAST_CAPABLE_MODELS
|
|
165
|
+
messages_api = sdk_client.messages
|
|
166
|
+
if fast:
|
|
167
|
+
request_kwargs["speed"] = "fast"
|
|
168
|
+
request_kwargs["betas"] = [FAST_MODE_BETA]
|
|
169
|
+
messages_api = sdk_client.beta.messages
|
|
170
|
+
|
|
138
171
|
try:
|
|
139
|
-
with
|
|
172
|
+
with messages_api.stream(**request_kwargs) as stream:
|
|
140
173
|
final_message = stream.get_final_message()
|
|
141
174
|
except LlmReviewError:
|
|
142
175
|
raise
|
|
@@ -285,9 +318,212 @@ def _run_via_claude_cli(
|
|
|
285
318
|
)
|
|
286
319
|
|
|
287
320
|
|
|
321
|
+
# ---------- OpenAI SDK backend (Responses API) ----------
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _resolve_openai_client(api_key: str | None, client: Any | None) -> Any:
|
|
325
|
+
if client is not None:
|
|
326
|
+
return client
|
|
327
|
+
try:
|
|
328
|
+
import openai
|
|
329
|
+
except ImportError as exc:
|
|
330
|
+
raise LlmReviewError(
|
|
331
|
+
"The openai SDK is not installed. Install with `pip install attackmap[llm]` "
|
|
332
|
+
"to use the OpenAI API backend, or install the `codex` CLI for the CLI backend."
|
|
333
|
+
) from exc
|
|
334
|
+
|
|
335
|
+
resolved_key = api_key or os.environ.get("OPENAI_API_KEY")
|
|
336
|
+
if not resolved_key:
|
|
337
|
+
raise LlmReviewError(
|
|
338
|
+
"No OpenAI API credentials found. Set OPENAI_API_KEY, or install the `codex` "
|
|
339
|
+
"CLI and run `codex login` to use --llm-backend cli."
|
|
340
|
+
)
|
|
341
|
+
return openai.OpenAI(api_key=resolved_key, timeout=OPENAI_API_TIMEOUT_SECONDS)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _openai_output_text(response: Any) -> str:
|
|
345
|
+
text = getattr(response, "output_text", None)
|
|
346
|
+
if isinstance(text, str) and text.strip():
|
|
347
|
+
return text.strip()
|
|
348
|
+
# Fallback: walk output items → content blocks → text, for SDK/response
|
|
349
|
+
# shapes where the `output_text` convenience isn't populated.
|
|
350
|
+
parts: list[str] = []
|
|
351
|
+
for item in getattr(response, "output", None) or []:
|
|
352
|
+
for block in getattr(item, "content", None) or []:
|
|
353
|
+
value = getattr(block, "text", None)
|
|
354
|
+
if isinstance(value, str) and value:
|
|
355
|
+
parts.append(value)
|
|
356
|
+
return "\n".join(parts).strip()
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _openai_usage(usage: Any) -> dict[str, int]:
|
|
360
|
+
out: dict[str, int] = {}
|
|
361
|
+
for field in ("input_tokens", "output_tokens"):
|
|
362
|
+
value = getattr(usage, field, None)
|
|
363
|
+
if isinstance(value, int):
|
|
364
|
+
out[field] = value
|
|
365
|
+
return out
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _run_via_openai_sdk(
|
|
369
|
+
rendered_system: str,
|
|
370
|
+
rendered_user: str,
|
|
371
|
+
*,
|
|
372
|
+
model: str,
|
|
373
|
+
effort: str,
|
|
374
|
+
max_tokens: int,
|
|
375
|
+
api_key: str | None,
|
|
376
|
+
client: Any | None,
|
|
377
|
+
) -> LlmReviewResult:
|
|
378
|
+
oi = _resolve_openai_client(api_key, client)
|
|
379
|
+
reasoning_effort = OPENAI_EFFORT_MAP.get(effort, "high")
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
response = oi.responses.create(
|
|
383
|
+
model=model,
|
|
384
|
+
instructions=rendered_system,
|
|
385
|
+
input=rendered_user,
|
|
386
|
+
reasoning={"effort": reasoning_effort},
|
|
387
|
+
max_output_tokens=max_tokens,
|
|
388
|
+
)
|
|
389
|
+
except LlmReviewError:
|
|
390
|
+
raise
|
|
391
|
+
except Exception as exc: # pragma: no cover - surface SDK errors verbatim
|
|
392
|
+
raise LlmReviewError(f"OpenAI API call failed: {exc}") from exc
|
|
393
|
+
|
|
394
|
+
markdown = _openai_output_text(response)
|
|
395
|
+
if not markdown:
|
|
396
|
+
raise LlmReviewError("OpenAI returned no text content for the review.")
|
|
397
|
+
|
|
398
|
+
status = getattr(response, "status", None)
|
|
399
|
+
return LlmReviewResult(
|
|
400
|
+
markdown=markdown,
|
|
401
|
+
model=str(getattr(response, "model", None) or model),
|
|
402
|
+
stop_reason=status if isinstance(status, str) else None,
|
|
403
|
+
usage=_openai_usage(getattr(response, "usage", None)),
|
|
404
|
+
backend="api",
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ---------- Codex CLI backend ----------
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _codex_cli_available() -> bool:
|
|
412
|
+
return shutil.which("codex") is not None
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _run_via_codex_cli(
|
|
416
|
+
rendered_system: str,
|
|
417
|
+
rendered_user: str,
|
|
418
|
+
*,
|
|
419
|
+
model: str,
|
|
420
|
+
effort: str,
|
|
421
|
+
runner: Any | None = None,
|
|
422
|
+
) -> LlmReviewResult:
|
|
423
|
+
"""Invoke `codex exec` non-interactively and return the final message.
|
|
424
|
+
|
|
425
|
+
`codex exec` treats the positional prompt as the instruction and piped
|
|
426
|
+
stdin as additional context, so we pass the rendered system prompt as the
|
|
427
|
+
argument and stream the (large) evidence pack in on stdin. It prints only
|
|
428
|
+
the final agent message to stdout. Runs read-only so it can never mutate the
|
|
429
|
+
working tree, and `--skip-git-repo-check` lets it run outside a git repo.
|
|
430
|
+
|
|
431
|
+
The `runner` argument is only used by tests to inject a fake subprocess
|
|
432
|
+
runner; in production we go straight to subprocess.run.
|
|
433
|
+
"""
|
|
434
|
+
if runner is None and not _codex_cli_available():
|
|
435
|
+
raise LlmReviewError(
|
|
436
|
+
"`codex` CLI was not found on PATH. Install it (npm install -g @openai/codex) "
|
|
437
|
+
"and run `codex login`, or set OPENAI_API_KEY to use --llm-backend api."
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
reasoning_effort = OPENAI_EFFORT_MAP.get(effort, "high")
|
|
441
|
+
cmd = [
|
|
442
|
+
"codex",
|
|
443
|
+
"exec",
|
|
444
|
+
"--model",
|
|
445
|
+
model,
|
|
446
|
+
"-c",
|
|
447
|
+
f'model_reasoning_effort="{reasoning_effort}"',
|
|
448
|
+
"--sandbox",
|
|
449
|
+
"read-only",
|
|
450
|
+
"--skip-git-repo-check",
|
|
451
|
+
rendered_system,
|
|
452
|
+
]
|
|
453
|
+
|
|
454
|
+
try:
|
|
455
|
+
if runner is None:
|
|
456
|
+
completed = subprocess.run(
|
|
457
|
+
cmd,
|
|
458
|
+
input=rendered_user,
|
|
459
|
+
text=True,
|
|
460
|
+
capture_output=True,
|
|
461
|
+
check=False,
|
|
462
|
+
timeout=CODEX_CLI_TIMEOUT_SECONDS,
|
|
463
|
+
)
|
|
464
|
+
else:
|
|
465
|
+
completed = runner(cmd, rendered_user)
|
|
466
|
+
except FileNotFoundError as exc:
|
|
467
|
+
raise LlmReviewError("`codex` CLI was not found on PATH.") from exc
|
|
468
|
+
except subprocess.TimeoutExpired as exc:
|
|
469
|
+
raise LlmReviewError(
|
|
470
|
+
f"`codex` CLI did not respond within {CODEX_CLI_TIMEOUT_SECONDS}s."
|
|
471
|
+
) from exc
|
|
472
|
+
|
|
473
|
+
stdout = (completed.stdout or "").strip()
|
|
474
|
+
stderr = (completed.stderr or "").strip()
|
|
475
|
+
if completed.returncode != 0:
|
|
476
|
+
hint = ""
|
|
477
|
+
low = stderr.lower()
|
|
478
|
+
if "not logged in" in low or "login" in low or "auth" in low:
|
|
479
|
+
hint = (
|
|
480
|
+
" Run `codex login`, or use --llm-backend api with OPENAI_API_KEY set."
|
|
481
|
+
)
|
|
482
|
+
raise LlmReviewError(
|
|
483
|
+
f"`codex` CLI exited with status {completed.returncode}: "
|
|
484
|
+
f"{stderr or '<no stderr>'}.{hint}"
|
|
485
|
+
)
|
|
486
|
+
if not stdout:
|
|
487
|
+
raise LlmReviewError("`codex` CLI produced no output.")
|
|
488
|
+
|
|
489
|
+
return LlmReviewResult(
|
|
490
|
+
markdown=stdout,
|
|
491
|
+
model=model,
|
|
492
|
+
stop_reason=None,
|
|
493
|
+
usage={},
|
|
494
|
+
backend="cli",
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
|
|
288
498
|
# ---------- Public entry point ----------
|
|
289
499
|
|
|
290
500
|
|
|
501
|
+
def _resolve_openai_backend(
|
|
502
|
+
backend: LlmBackend,
|
|
503
|
+
*,
|
|
504
|
+
api_key: str | None,
|
|
505
|
+
client: Any | None,
|
|
506
|
+
codex_runner: Any | None,
|
|
507
|
+
) -> Literal["api", "cli"]:
|
|
508
|
+
if backend == "api":
|
|
509
|
+
return "api"
|
|
510
|
+
if backend == "cli":
|
|
511
|
+
if codex_runner is None and not _codex_cli_available():
|
|
512
|
+
raise LlmReviewError(
|
|
513
|
+
"`--llm-backend cli` was requested but the `codex` CLI is not on PATH."
|
|
514
|
+
)
|
|
515
|
+
return "cli"
|
|
516
|
+
# auto
|
|
517
|
+
if client is not None or api_key or os.environ.get("OPENAI_API_KEY"):
|
|
518
|
+
return "api"
|
|
519
|
+
if _codex_cli_available():
|
|
520
|
+
return "cli"
|
|
521
|
+
raise LlmReviewError(
|
|
522
|
+
"No OpenAI backend available. Set OPENAI_API_KEY, or install the `codex` "
|
|
523
|
+
"CLI and run `codex login`."
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
|
|
291
527
|
def _resolve_backend(
|
|
292
528
|
backend: LlmBackend,
|
|
293
529
|
*,
|
|
@@ -329,17 +565,23 @@ def generate_llm_review(
|
|
|
329
565
|
backend: LlmBackend = "auto",
|
|
330
566
|
cli_runner: Any | None = None,
|
|
331
567
|
mode: Literal["review", "hunt", "hunt_verify", "remediate"] = "review",
|
|
568
|
+
speed: Literal["standard", "fast"] = "standard",
|
|
569
|
+
provider: LlmProvider = "claude",
|
|
570
|
+
openai_client: Any | None = None,
|
|
571
|
+
codex_runner: Any | None = None,
|
|
332
572
|
) -> LlmReviewResult:
|
|
333
573
|
"""Produce a narrative defensive review — or, with ``mode="hunt"``, ranked
|
|
334
|
-
vulnerability hypotheses (#80) — by calling
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
574
|
+
vulnerability hypotheses (#80) — by calling an LLM.
|
|
575
|
+
|
|
576
|
+
``provider`` selects Claude (default) or OpenAI/Codex. For each provider,
|
|
577
|
+
the backend resolves to an API SDK or a subscription CLI based on available
|
|
578
|
+
auth, then runs the appropriate prompt pack through it. The Claude SDK path
|
|
579
|
+
streams so we never hit HTTP timeouts on long reviews; the `claude` CLI path
|
|
580
|
+
runs `claude -p --output-format=json`. The OpenAI path uses the Responses
|
|
581
|
+
API; the `codex` CLI path runs `codex exec`. All modes share the same
|
|
582
|
+
evidence grounding contract (every claim cites evidence IDs). ``speed`` (Fast
|
|
583
|
+
mode) is Claude/API-only and ignored for OpenAI.
|
|
341
584
|
"""
|
|
342
|
-
resolved_model = model or os.environ.get("ATTACKMAP_LLM_MODEL") or DEFAULT_MODEL
|
|
343
585
|
resolved_effort = effort or DEFAULT_EFFORT
|
|
344
586
|
|
|
345
587
|
render = {
|
|
@@ -348,6 +590,36 @@ def generate_llm_review(
|
|
|
348
590
|
"remediate": render_remediation_prompts,
|
|
349
591
|
}.get(mode, render_review_prompts)
|
|
350
592
|
rendered = render(scan, attack_surfaces, findings, attack_paths)
|
|
593
|
+
|
|
594
|
+
if provider == "openai":
|
|
595
|
+
resolved_model = (
|
|
596
|
+
model or os.environ.get("ATTACKMAP_OPENAI_MODEL") or OPENAI_DEFAULT_MODEL
|
|
597
|
+
)
|
|
598
|
+
if backend == "cli" and codex_runner is not None:
|
|
599
|
+
chosen: Literal["api", "cli"] = "cli"
|
|
600
|
+
else:
|
|
601
|
+
chosen = _resolve_openai_backend(
|
|
602
|
+
backend, api_key=api_key, client=openai_client, codex_runner=codex_runner
|
|
603
|
+
)
|
|
604
|
+
if chosen == "api":
|
|
605
|
+
return _run_via_openai_sdk(
|
|
606
|
+
rendered.system,
|
|
607
|
+
rendered.user,
|
|
608
|
+
model=resolved_model,
|
|
609
|
+
effort=resolved_effort,
|
|
610
|
+
max_tokens=max_tokens,
|
|
611
|
+
api_key=api_key,
|
|
612
|
+
client=openai_client,
|
|
613
|
+
)
|
|
614
|
+
return _run_via_codex_cli(
|
|
615
|
+
rendered.system,
|
|
616
|
+
rendered.user,
|
|
617
|
+
model=resolved_model,
|
|
618
|
+
effort=resolved_effort,
|
|
619
|
+
runner=codex_runner,
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
resolved_model = model or os.environ.get("ATTACKMAP_LLM_MODEL") or DEFAULT_MODEL
|
|
351
623
|
if backend == "cli" and cli_runner is not None:
|
|
352
624
|
chosen_backend: Literal["api", "cli"] = "cli"
|
|
353
625
|
else:
|
|
@@ -362,6 +634,7 @@ def generate_llm_review(
|
|
|
362
634
|
max_tokens=max_tokens,
|
|
363
635
|
api_key=api_key,
|
|
364
636
|
client=client,
|
|
637
|
+
speed=speed,
|
|
365
638
|
)
|
|
366
639
|
return _run_via_claude_cli(
|
|
367
640
|
rendered.system,
|
|
@@ -376,7 +649,9 @@ __all__ = [
|
|
|
376
649
|
"LlmReviewError",
|
|
377
650
|
"LlmReviewResult",
|
|
378
651
|
"LlmBackend",
|
|
652
|
+
"LlmProvider",
|
|
379
653
|
"DEFAULT_MODEL",
|
|
654
|
+
"OPENAI_DEFAULT_MODEL",
|
|
380
655
|
"DEFAULT_EFFORT",
|
|
381
656
|
"DEFAULT_MAX_TOKENS",
|
|
382
657
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: attackmap
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
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>
|
|
@@ -36,6 +36,7 @@ Requires-Dist: pydantic>=2.7.0
|
|
|
36
36
|
Requires-Dist: networkx>=3.3
|
|
37
37
|
Provides-Extra: llm
|
|
38
38
|
Requires-Dist: anthropic>=0.40.0; extra == "llm"
|
|
39
|
+
Requires-Dist: openai>=1.60.0; extra == "llm"
|
|
39
40
|
Provides-Extra: all
|
|
40
41
|
Requires-Dist: attackmap[llm]; extra == "all"
|
|
41
42
|
Requires-Dist: attackmap-analyzer-python>=0.1.0; extra == "all"
|
|
@@ -105,6 +106,12 @@ to use your existing Pro/Max subscription:
|
|
|
105
106
|
attackmap analyze /path/to/repo --output reports --llm
|
|
106
107
|
```
|
|
107
108
|
|
|
109
|
+
Prefer OpenAI? Add `--llm-provider openai` to run the same review/hunt/remediate
|
|
110
|
+
phases on OpenAI. Set `OPENAI_API_KEY` for the Responses API, or log in once with
|
|
111
|
+
the [`codex` CLI](https://developers.openai.com/codex) to use your subscription
|
|
112
|
+
(`--llm-backend cli`). Default model is `gpt-5-codex`; `--llm-model` accepts any
|
|
113
|
+
model ID verbatim.
|
|
114
|
+
|
|
108
115
|
Read `reports/defensive-review.md` (heuristic) and `reports/defensive-review-llm.md`
|
|
109
116
|
(LLM-narrated) side by side.
|
|
110
117
|
|
|
@@ -482,7 +489,9 @@ trust-boundary violations, and more.
|
|
|
482
489
|
|
|
483
490
|
**4. LLM narrative review.** With `--llm`, Claude Opus generates a final review
|
|
484
491
|
from the structured evidence pack. The model is forced to cite real
|
|
485
|
-
surface/asset/control IDs, so it can't invent findings.
|
|
492
|
+
surface/asset/control IDs, so it can't invent findings. Add `--llm-provider
|
|
493
|
+
openai` to run the same phase on OpenAI/Codex instead (`gpt-5-codex` by default,
|
|
494
|
+
via `OPENAI_API_KEY` or the `codex` CLI) — the grounding contract is identical.
|
|
486
495
|
|
|
487
496
|
**5. Vulnerability-hypothesis hunting (`--hunt`).** The honest core of the
|
|
488
497
|
"find the unknown" ask. `attackmap analyze . --hunt` has Claude reason over the
|
|
@@ -542,6 +551,8 @@ attackmap analyze <path> --module python --module rust # only these analyzers
|
|
|
542
551
|
attackmap analyze <path> --cve # cross-reference SBOM against OSV.dev
|
|
543
552
|
attackmap analyze <path> --llm # add LLM narrative (auto-resolve auth)
|
|
544
553
|
attackmap analyze <path> --llm --llm-backend cli # force Claude CLI
|
|
554
|
+
attackmap analyze <path> --llm --llm-provider openai # use OpenAI/Codex (OPENAI_API_KEY or `codex` CLI)
|
|
555
|
+
attackmap analyze <path> --llm --llm-provider openai --llm-model gpt-5.5 # any OpenAI model ID, passed through
|
|
545
556
|
attackmap analyze <path> --hunt # LLM vulnerability-hypothesis hunt (leads to confirm)
|
|
546
557
|
attackmap analyze <path> --hunt --verify # adjudicate each lead vs. source (confirmed/refuted/review)
|
|
547
558
|
attackmap analyze <path> --remediate # LLM review-first fix suggestions (remediation.md)
|