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.

Files changed (99) hide show
  1. {attackmap-0.4.2/src/attackmap.egg-info → attackmap-0.4.4}/PKG-INFO +13 -2
  2. {attackmap-0.4.2 → attackmap-0.4.4}/README.md +11 -1
  3. {attackmap-0.4.2 → attackmap-0.4.4}/pyproject.toml +2 -1
  4. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/__init__.py +1 -1
  5. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/cli.py +65 -10
  6. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/llm_review.py +285 -10
  7. {attackmap-0.4.2 → attackmap-0.4.4/src/attackmap.egg-info}/PKG-INFO +13 -2
  8. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap.egg-info/SOURCES.txt +1 -0
  9. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap.egg-info/requires.txt +1 -0
  10. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_llm_review.py +284 -2
  11. attackmap-0.4.4/tests/test_modules_cli.py +44 -0
  12. {attackmap-0.4.2 → attackmap-0.4.4}/LICENSE +0 -0
  13. {attackmap-0.4.2 → attackmap-0.4.4}/setup.cfg +0 -0
  14. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/analyzer.py +0 -0
  15. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/analyzer_contracts.py +0 -0
  16. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/analyzers.py +0 -0
  17. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/anomalies.py +0 -0
  18. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/asset_model.py +0 -0
  19. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/attack_taxonomy.py +0 -0
  20. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/authz.py +0 -0
  21. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/config_scanner.py +0 -0
  22. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/context_pack.py +0 -0
  23. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/control_model.py +0 -0
  24. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/crypto.py +0 -0
  25. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/cve.py +0 -0
  26. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/defensive_review.py +0 -0
  27. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/detection_opportunities.py +0 -0
  28. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/diagrams.py +0 -0
  29. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/diff.py +0 -0
  30. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/exploitability.py +0 -0
  31. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/graph.py +0 -0
  32. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/insights.py +0 -0
  33. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/merge.py +0 -0
  34. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/models.py +0 -0
  35. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/progress.py +0 -0
  36. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/recon_models.py +0 -0
  37. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/recon_to_analysis.py +0 -0
  38. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/report.py +0 -0
  39. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/review_eval.py +0 -0
  40. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/review_json.py +0 -0
  41. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/review_prompts.py +0 -0
  42. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/sarif.py +0 -0
  43. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/sbom.py +0 -0
  44. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/scanner.py +0 -0
  45. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/sdk/__init__.py +0 -0
  46. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/sdk/contracts.py +0 -0
  47. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/sdk/models.py +0 -0
  48. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/security_overlay.py +0 -0
  49. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/srcpaths.py +0 -0
  50. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/suggest.py +0 -0
  51. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/taint.py +0 -0
  52. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/threat_model.py +0 -0
  53. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/topology.py +0 -0
  54. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/weaknesses.py +0 -0
  55. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap/webhardening.py +0 -0
  56. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap.egg-info/dependency_links.txt +0 -0
  57. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap.egg-info/entry_points.txt +0 -0
  58. {attackmap-0.4.2 → attackmap-0.4.4}/src/attackmap.egg-info/top_level.txt +0 -0
  59. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_analyzer.py +0 -0
  60. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_analyzer_metadata_schema.py +0 -0
  61. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_analyzers.py +0 -0
  62. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_anomalies.py +0 -0
  63. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_artifact_schemas.py +0 -0
  64. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_authz.py +0 -0
  65. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_config_scanner.py +0 -0
  66. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_context_pack.py +0 -0
  67. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_crypto.py +0 -0
  68. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_cve.py +0 -0
  69. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_defensive_review.py +0 -0
  70. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_diagrams.py +0 -0
  71. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_diff.py +0 -0
  72. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_exploitability.py +0 -0
  73. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_graph.py +0 -0
  74. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_hunt.py +0 -0
  75. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_injection_sinks.py +0 -0
  76. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_merge.py +0 -0
  77. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_pr_comment.py +0 -0
  78. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_progress.py +0 -0
  79. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_provenance.py +0 -0
  80. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_recon_to_analysis.py +0 -0
  81. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_remediate.py +0 -0
  82. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_report.py +0 -0
  83. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_review_eval.py +0 -0
  84. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_review_json.py +0 -0
  85. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_review_prompts.py +0 -0
  86. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_sarif.py +0 -0
  87. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_sbom.py +0 -0
  88. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_scanner.py +0 -0
  89. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_security_overlay.py +0 -0
  90. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_shared_contract_imports.py +0 -0
  91. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_signal_v2.py +0 -0
  92. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_srcpaths.py +0 -0
  93. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_suggest.py +0 -0
  94. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_taint.py +0 -0
  95. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_threat_intel.py +0 -0
  96. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_threat_model.py +0 -0
  97. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_topology.py +0 -0
  98. {attackmap-0.4.2 → attackmap-0.4.4}/tests/test_weaknesses.py +0 -0
  99. {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.2
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.2"
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"
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="Claude model ID for --llm (defaults to claude-opus-4-7).",
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' (default) tries ANTHROPIC_API_KEY → ANTHROPIC_AUTH_TOKEN → `claude` CLI; 'api' forces the SDK; 'cli' forces the `claude` CLI (uses your `claude login` auth, e.g. Pro/Max subscription).",
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 Claude (backend={llm_backend}, may take a minute)..."
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 Claude "
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"Claude is hunting exploit-chain hypotheses"
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 Claude (backend={llm_backend}, may take a minute)..."
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() -> None:
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-7"
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 sdk_client.messages.stream(**request_kwargs) as stream:
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 Claude.
335
-
336
- Resolves which backend to use (API SDK or `claude` CLI) based on available
337
- auth, then runs the appropriate prompt pack through it. Streams the SDK path
338
- so we never hit HTTP timeouts on long reviews. The CLI path runs synchronously
339
- via `claude -p --output-format=json`. Both modes share the same evidence
340
- grounding contract (every claim cites evidence IDs).
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.2
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)
@@ -71,6 +71,7 @@ tests/test_hunt.py
71
71
  tests/test_injection_sinks.py
72
72
  tests/test_llm_review.py
73
73
  tests/test_merge.py
74
+ tests/test_modules_cli.py
74
75
  tests/test_pr_comment.py
75
76
  tests/test_progress.py
76
77
  tests/test_provenance.py