attackmap 0.4.1__tar.gz → 0.4.3__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 (98) hide show
  1. {attackmap-0.4.1/src/attackmap.egg-info → attackmap-0.4.3}/PKG-INFO +13 -2
  2. {attackmap-0.4.1 → attackmap-0.4.3}/README.md +11 -1
  3. {attackmap-0.4.1 → attackmap-0.4.3}/pyproject.toml +2 -1
  4. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/__init__.py +1 -1
  5. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/anomalies.py +22 -2
  6. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/cli.py +83 -33
  7. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/llm_review.py +285 -10
  8. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/scanner.py +3 -4
  9. {attackmap-0.4.1 → attackmap-0.4.3/src/attackmap.egg-info}/PKG-INFO +13 -2
  10. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap.egg-info/requires.txt +1 -0
  11. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_anomalies.py +26 -0
  12. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_llm_review.py +284 -2
  13. {attackmap-0.4.1 → attackmap-0.4.3}/LICENSE +0 -0
  14. {attackmap-0.4.1 → attackmap-0.4.3}/setup.cfg +0 -0
  15. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/analyzer.py +0 -0
  16. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/analyzer_contracts.py +0 -0
  17. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/analyzers.py +0 -0
  18. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/asset_model.py +0 -0
  19. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/attack_taxonomy.py +0 -0
  20. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/authz.py +0 -0
  21. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/config_scanner.py +0 -0
  22. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/context_pack.py +0 -0
  23. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/control_model.py +0 -0
  24. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/crypto.py +0 -0
  25. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/cve.py +0 -0
  26. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/defensive_review.py +0 -0
  27. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/detection_opportunities.py +0 -0
  28. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/diagrams.py +0 -0
  29. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/diff.py +0 -0
  30. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/exploitability.py +0 -0
  31. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/graph.py +0 -0
  32. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/insights.py +0 -0
  33. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/merge.py +0 -0
  34. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/models.py +0 -0
  35. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/progress.py +0 -0
  36. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/recon_models.py +0 -0
  37. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/recon_to_analysis.py +0 -0
  38. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/report.py +0 -0
  39. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/review_eval.py +0 -0
  40. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/review_json.py +0 -0
  41. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/review_prompts.py +0 -0
  42. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/sarif.py +0 -0
  43. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/sbom.py +0 -0
  44. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/sdk/__init__.py +0 -0
  45. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/sdk/contracts.py +0 -0
  46. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/sdk/models.py +0 -0
  47. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/security_overlay.py +0 -0
  48. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/srcpaths.py +0 -0
  49. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/suggest.py +0 -0
  50. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/taint.py +0 -0
  51. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/threat_model.py +0 -0
  52. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/topology.py +0 -0
  53. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/weaknesses.py +0 -0
  54. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap/webhardening.py +0 -0
  55. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap.egg-info/SOURCES.txt +0 -0
  56. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap.egg-info/dependency_links.txt +0 -0
  57. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap.egg-info/entry_points.txt +0 -0
  58. {attackmap-0.4.1 → attackmap-0.4.3}/src/attackmap.egg-info/top_level.txt +0 -0
  59. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_analyzer.py +0 -0
  60. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_analyzer_metadata_schema.py +0 -0
  61. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_analyzers.py +0 -0
  62. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_artifact_schemas.py +0 -0
  63. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_authz.py +0 -0
  64. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_config_scanner.py +0 -0
  65. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_context_pack.py +0 -0
  66. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_crypto.py +0 -0
  67. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_cve.py +0 -0
  68. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_defensive_review.py +0 -0
  69. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_diagrams.py +0 -0
  70. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_diff.py +0 -0
  71. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_exploitability.py +0 -0
  72. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_graph.py +0 -0
  73. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_hunt.py +0 -0
  74. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_injection_sinks.py +0 -0
  75. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_merge.py +0 -0
  76. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_pr_comment.py +0 -0
  77. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_progress.py +0 -0
  78. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_provenance.py +0 -0
  79. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_recon_to_analysis.py +0 -0
  80. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_remediate.py +0 -0
  81. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_report.py +0 -0
  82. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_review_eval.py +0 -0
  83. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_review_json.py +0 -0
  84. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_review_prompts.py +0 -0
  85. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_sarif.py +0 -0
  86. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_sbom.py +0 -0
  87. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_scanner.py +0 -0
  88. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_security_overlay.py +0 -0
  89. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_shared_contract_imports.py +0 -0
  90. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_signal_v2.py +0 -0
  91. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_srcpaths.py +0 -0
  92. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_suggest.py +0 -0
  93. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_taint.py +0 -0
  94. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_threat_intel.py +0 -0
  95. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_threat_model.py +0 -0
  96. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_topology.py +0 -0
  97. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_weaknesses.py +0 -0
  98. {attackmap-0.4.1 → attackmap-0.4.3}/tests/test_webhardening.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: attackmap
3
- Version: 0.4.1
3
+ Version: 0.4.3
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.1"
7
+ version = "0.4.3"
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.1"
2
+ __version__ = "0.4.3"
@@ -36,10 +36,16 @@ from __future__ import annotations
36
36
  import re
37
37
  from dataclasses import dataclass
38
38
  from pathlib import Path
39
+ from typing import TYPE_CHECKING
39
40
 
40
41
  from .models import Anomaly, Route, ScanResult
41
42
  from .srcpaths import is_test_file
42
43
 
44
+ if TYPE_CHECKING:
45
+ from .progress import JsonScanProgress, ScanProgress
46
+
47
+ _Progress = ScanProgress | JsonScanProgress
48
+
43
49
 
44
50
  @dataclass
45
51
  class _SignalCtx:
@@ -106,8 +112,17 @@ _MIN_GROUP = 3
106
112
  _SCAFFOLD_RE = re.compile(r"^(?:api|rest|graphql|gql|v\d+(?:beta\d*|alpha\d*)?)$", re.IGNORECASE)
107
113
 
108
114
 
109
- def find_anomalies(scan: ScanResult, root: str | Path | None = None) -> list[Anomaly]:
110
- """Return within-repo consistency outliers for the routes in ``scan``."""
115
+ def find_anomalies(
116
+ scan: ScanResult,
117
+ root: str | Path | None = None,
118
+ progress: "_Progress | None" = None,
119
+ ) -> list[Anomaly]:
120
+ """Return within-repo consistency outliers for the routes in ``scan``.
121
+
122
+ On a large route surface the per-cohort source-window reads dominate this
123
+ pass, so when a ``progress`` reporter is supplied it drives a determinate
124
+ bar over the cohorts (rather than an open-ended spinner).
125
+ """
111
126
  root_path = Path(root or scan.root).resolve()
112
127
 
113
128
  routes = [r for r in scan.routes if not is_test_file(r.file)]
@@ -129,8 +144,13 @@ def find_anomalies(scan: ScanResult, root: str | Path | None = None) -> list[Ano
129
144
 
130
145
  ctx = _SignalCtx(root_path, line_cache, file_route_lines)
131
146
 
147
+ if progress is not None:
148
+ progress.begin(len(groups), "Anomaly / outlier detection")
149
+
132
150
  out: list[Anomaly] = []
133
151
  for key, members in groups.items():
152
+ if progress is not None:
153
+ progress.advance(key)
134
154
  if len(members) < _MIN_GROUP:
135
155
  continue
136
156
  if not _is_route_like_cohort(key, members):
@@ -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,12 +165,27 @@ 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)
161
182
  if cve and scan.dependencies:
162
183
  typer.echo(f"Checking {len(scan.dependencies)} dependencies against OSV.dev…")
163
- vulns, cve_summary = query_vulnerabilities(scan.dependencies)
184
+ scan_progress.stage(f"Checking {len(scan.dependencies)} dependencies against OSV.dev")
185
+ try:
186
+ vulns, cve_summary = query_vulnerabilities(scan.dependencies)
187
+ finally:
188
+ scan_progress.done()
164
189
  scan.vulnerabilities = vulns
165
190
  typer.echo(
166
191
  f"CVE lookup: {len(vulns)} vulnerabilit"
@@ -255,17 +280,25 @@ def analyze(
255
280
 
256
281
  typer.echo("")
257
282
  typer.echo(
258
- 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)..."
259
284
  )
260
- result = generate_llm_review(
261
- scan,
262
- attack_surfaces,
263
- findings,
264
- attack_paths,
265
- model=llm_model,
266
- effort=effort_value, # type: ignore[arg-type]
267
- backend=llm_backend, # type: ignore[arg-type]
285
+ scan_progress.stage(
286
+ f"{llm_display} is writing the defensive review (backend={llm_backend})"
268
287
  )
288
+ try:
289
+ result = generate_llm_review(
290
+ scan,
291
+ attack_surfaces,
292
+ findings,
293
+ attack_paths,
294
+ model=llm_model,
295
+ effort=effort_value, # type: ignore[arg-type]
296
+ backend=llm_backend, # type: ignore[arg-type]
297
+ speed=llm_speed, # type: ignore[arg-type]
298
+ provider=llm_provider, # type: ignore[arg-type]
299
+ )
300
+ finally:
301
+ scan_progress.done()
269
302
  except LlmReviewError as exc:
270
303
  typer.echo(f"LLM review skipped: {exc}", err=True)
271
304
  else:
@@ -304,19 +337,28 @@ def analyze(
304
337
  typer.echo("")
305
338
  hunt_mode = "hunt_verify" if verify else "hunt"
306
339
  typer.echo(
307
- f"Hunting for vulnerability hypotheses via Claude "
340
+ f"Hunting for vulnerability hypotheses via {llm_display} "
308
341
  f"({'adjudicated against source, ' if verify else ''}backend={llm_backend}, may take a minute)..."
309
342
  )
310
- hunt_result = generate_llm_review(
311
- scan,
312
- attack_surfaces,
313
- findings,
314
- attack_paths,
315
- model=llm_model,
316
- effort=hunt_effort_value, # type: ignore[arg-type]
317
- backend=llm_backend, # type: ignore[arg-type]
318
- mode=hunt_mode, # type: ignore[arg-type]
343
+ scan_progress.stage(
344
+ f"{llm_display} is hunting exploit-chain hypotheses"
345
+ f"{' + verifying against source' if verify else ''} (backend={llm_backend})"
319
346
  )
347
+ try:
348
+ hunt_result = generate_llm_review(
349
+ scan,
350
+ attack_surfaces,
351
+ findings,
352
+ attack_paths,
353
+ model=llm_model,
354
+ effort=hunt_effort_value, # type: ignore[arg-type]
355
+ backend=llm_backend, # type: ignore[arg-type]
356
+ mode=hunt_mode, # type: ignore[arg-type]
357
+ speed=llm_speed, # type: ignore[arg-type]
358
+ provider=llm_provider, # type: ignore[arg-type]
359
+ )
360
+ finally:
361
+ scan_progress.done()
320
362
  except LlmReviewError as exc:
321
363
  typer.echo(f"Vulnerability hunt skipped: {exc}", err=True)
322
364
  else:
@@ -357,18 +399,26 @@ def analyze(
357
399
  )
358
400
  typer.echo("")
359
401
  typer.echo(
360
- 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)..."
361
403
  )
362
- rem_result = generate_llm_review(
363
- scan,
364
- attack_surfaces,
365
- findings,
366
- attack_paths,
367
- model=llm_model,
368
- effort=rem_effort_value, # type: ignore[arg-type]
369
- backend=llm_backend, # type: ignore[arg-type]
370
- mode="remediate",
404
+ scan_progress.stage(
405
+ f"{llm_display} is drafting remediation suggestions (backend={llm_backend})"
371
406
  )
407
+ try:
408
+ rem_result = generate_llm_review(
409
+ scan,
410
+ attack_surfaces,
411
+ findings,
412
+ attack_paths,
413
+ model=llm_model,
414
+ effort=rem_effort_value, # type: ignore[arg-type]
415
+ backend=llm_backend, # type: ignore[arg-type]
416
+ mode="remediate",
417
+ speed=llm_speed, # type: ignore[arg-type]
418
+ provider=llm_provider, # type: ignore[arg-type]
419
+ )
420
+ finally:
421
+ scan_progress.done()
372
422
  except LlmReviewError as exc:
373
423
  typer.echo(f"Remediation skipped: {exc}", err=True)
374
424
  else: