dockerls 1.0.0__py3-none-any.whl

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.
Files changed (230) hide show
  1. dockerls/__init__.py +31 -0
  2. dockerls/application/__init__.py +0 -0
  3. dockerls/application/dto/__init__.py +3 -0
  4. dockerls/application/dto/analysis.py +257 -0
  5. dockerls/application/services/__init__.py +0 -0
  6. dockerls/application/services/alternatives_lookup.py +167 -0
  7. dockerls/application/services/composite_repository.py +106 -0
  8. dockerls/application/services/cross_validation.py +233 -0
  9. dockerls/application/services/ecosystems.py +350 -0
  10. dockerls/application/services/fallback_scanner.py +97 -0
  11. dockerls/application/services/hardening_analysis.py +174 -0
  12. dockerls/application/services/migration.py +297 -0
  13. dockerls/application/services/progress.py +56 -0
  14. dockerls/application/services/remediation.py +321 -0
  15. dockerls/application/services/scan_history_store.py +88 -0
  16. dockerls/application/services/scanner_factory.py +88 -0
  17. dockerls/application/services/source_registry.py +151 -0
  18. dockerls/application/services/tag_history_store.py +76 -0
  19. dockerls/application/services/teardown.py +50 -0
  20. dockerls/application/services/verdict.py +298 -0
  21. dockerls/application/services/version_discovery.py +108 -0
  22. dockerls/application/use_cases/__init__.py +0 -0
  23. dockerls/application/use_cases/analyze_dockerfile.py +103 -0
  24. dockerls/application/use_cases/analyze_image.py +173 -0
  25. dockerls/application/use_cases/build_image.py +1795 -0
  26. dockerls/application/use_cases/compare_images.py +91 -0
  27. dockerls/application/use_cases/fleet_scan.py +240 -0
  28. dockerls/application/use_cases/recommend_images.py +1078 -0
  29. dockerls/application/use_cases/registry_audit.py +133 -0
  30. dockerls/application/use_cases/search_images.py +23 -0
  31. dockerls/application/use_cases/upgrade_base.py +167 -0
  32. dockerls/cache/__init__.py +0 -0
  33. dockerls/cache/sqlite_cache.py +184 -0
  34. dockerls/cli/__init__.py +0 -0
  35. dockerls/cli/analysis_baseline.py +98 -0
  36. dockerls/cli/app.py +294 -0
  37. dockerls/cli/commands/__init__.py +0 -0
  38. dockerls/cli/commands/advisor.py +262 -0
  39. dockerls/cli/commands/alternatives.py +291 -0
  40. dockerls/cli/commands/analyze.py +429 -0
  41. dockerls/cli/commands/analyze_dockerfile.py +104 -0
  42. dockerls/cli/commands/base_cmd.py +244 -0
  43. dockerls/cli/commands/base_image.py +551 -0
  44. dockerls/cli/commands/build.py +1300 -0
  45. dockerls/cli/commands/cache_cmd.py +104 -0
  46. dockerls/cli/commands/compare.py +177 -0
  47. dockerls/cli/commands/controls.py +110 -0
  48. dockerls/cli/commands/doctor.py +566 -0
  49. dockerls/cli/commands/export.py +81 -0
  50. dockerls/cli/commands/fleet.py +159 -0
  51. dockerls/cli/commands/health.py +84 -0
  52. dockerls/cli/commands/login.py +53 -0
  53. dockerls/cli/commands/policy_cmd.py +111 -0
  54. dockerls/cli/commands/provenance_cmd.py +162 -0
  55. dockerls/cli/commands/recommend.py +761 -0
  56. dockerls/cli/commands/registry_audit_cmd.py +103 -0
  57. dockerls/cli/commands/sbom.py +144 -0
  58. dockerls/cli/commands/search.py +86 -0
  59. dockerls/cli/commands/verify.py +115 -0
  60. dockerls/cli/commands/version.py +12 -0
  61. dockerls/cli/commands/vex_cmd.py +117 -0
  62. dockerls/cli/dependencies.py +530 -0
  63. dockerls/cli/image_names.py +79 -0
  64. dockerls/cli/options.py +42 -0
  65. dockerls/cli/progress.py +145 -0
  66. dockerls/cli/publish_prompt.py +123 -0
  67. dockerls/cli/rendering.py +214 -0
  68. dockerls/cli/runtime.py +65 -0
  69. dockerls/cli/scan_failure.py +71 -0
  70. dockerls/cli/text.py +39 -0
  71. dockerls/cli/validators.py +35 -0
  72. dockerls/cli/vulnerability_view.py +154 -0
  73. dockerls/domain/__init__.py +0 -0
  74. dockerls/domain/entities/__init__.py +75 -0
  75. dockerls/domain/entities/declared_metadata.py +147 -0
  76. dockerls/domain/entities/dockerfile_analysis.py +318 -0
  77. dockerls/domain/entities/image.py +109 -0
  78. dockerls/domain/entities/image_facts.py +137 -0
  79. dockerls/domain/entities/recommendation.py +33 -0
  80. dockerls/domain/entities/scan_result.py +129 -0
  81. dockerls/domain/entities/vulnerability.py +232 -0
  82. dockerls/domain/interfaces/__init__.py +17 -0
  83. dockerls/domain/interfaces/cache_store.py +18 -0
  84. dockerls/domain/interfaces/dockerfile_validator.py +99 -0
  85. dockerls/domain/interfaces/eol_checker.py +11 -0
  86. dockerls/domain/interfaces/image_repository.py +15 -0
  87. dockerls/domain/interfaces/scanner.py +15 -0
  88. dockerls/domain/security_controls.py +362 -0
  89. dockerls/domain/value_objects/__init__.py +49 -0
  90. dockerls/domain/value_objects/attack_surface.py +198 -0
  91. dockerls/domain/value_objects/base_recipe.py +600 -0
  92. dockerls/domain/value_objects/base_upgrade.py +292 -0
  93. dockerls/domain/value_objects/build_labels.py +99 -0
  94. dockerls/domain/value_objects/build_policy.py +412 -0
  95. dockerls/domain/value_objects/confidence.py +156 -0
  96. dockerls/domain/value_objects/fleet.py +174 -0
  97. dockerls/domain/value_objects/gate.py +327 -0
  98. dockerls/domain/value_objects/hardening.py +303 -0
  99. dockerls/domain/value_objects/image_reference.py +90 -0
  100. dockerls/domain/value_objects/inheritance.py +352 -0
  101. dockerls/domain/value_objects/network_policy.py +280 -0
  102. dockerls/domain/value_objects/production_readiness.py +145 -0
  103. dockerls/domain/value_objects/provenance.py +211 -0
  104. dockerls/domain/value_objects/recipe_diff.py +188 -0
  105. dockerls/domain/value_objects/registry_audit.py +195 -0
  106. dockerls/domain/value_objects/registry_target.py +225 -0
  107. dockerls/domain/value_objects/remediation_score.py +62 -0
  108. dockerls/domain/value_objects/scan_history.py +183 -0
  109. dockerls/domain/value_objects/scan_plan.py +193 -0
  110. dockerls/domain/value_objects/scanner_db.py +143 -0
  111. dockerls/domain/value_objects/security_score.py +160 -0
  112. dockerls/domain/value_objects/security_tier.py +122 -0
  113. dockerls/domain/value_objects/tag_history.py +180 -0
  114. dockerls/domain/value_objects/tool_release.py +253 -0
  115. dockerls/domain/value_objects/tristate.py +47 -0
  116. dockerls/domain/value_objects/vex.py +249 -0
  117. dockerls/exit_codes.py +23 -0
  118. dockerls/exporters/__init__.py +0 -0
  119. dockerls/exporters/base.py +17 -0
  120. dockerls/exporters/csv_exporter.py +85 -0
  121. dockerls/exporters/factory.py +31 -0
  122. dockerls/exporters/html_exporter.py +105 -0
  123. dockerls/exporters/json_exporter.py +19 -0
  124. dockerls/exporters/markdown_exporter.py +84 -0
  125. dockerls/exporters/sarif_exporter.py +245 -0
  126. dockerls/infrastructure/__init__.py +0 -0
  127. dockerls/infrastructure/config/__init__.py +0 -0
  128. dockerls/infrastructure/config/policy_file.py +165 -0
  129. dockerls/infrastructure/config/settings.py +197 -0
  130. dockerls/infrastructure/database/__init__.py +0 -0
  131. dockerls/infrastructure/database/models.py +78 -0
  132. dockerls/infrastructure/dockerfile_validator.py +1899 -0
  133. dockerls/infrastructure/evidence.py +99 -0
  134. dockerls/infrastructure/hashing.py +165 -0
  135. dockerls/infrastructure/logging/__init__.py +0 -0
  136. dockerls/infrastructure/logging/setup.py +98 -0
  137. dockerls/infrastructure/network/__init__.py +0 -0
  138. dockerls/infrastructure/network/guarded_client.py +107 -0
  139. dockerls/infrastructure/network/host_guard.py +117 -0
  140. dockerls/infrastructure/redaction.py +135 -0
  141. dockerls/infrastructure/templates/hardening/alpine.dockerfile +44 -0
  142. dockerls/infrastructure/templates/hardening/debian.dockerfile +45 -0
  143. dockerls/infrastructure/templates/hardening/distroless.dockerfile +34 -0
  144. dockerls/infrastructure/templates/hardening/go-alpine.dockerfile +52 -0
  145. dockerls/infrastructure/templates/hardening/go-debian.dockerfile +54 -0
  146. dockerls/infrastructure/templates/hardening/go-distroless.dockerfile +43 -0
  147. dockerls/infrastructure/templates/hardening/go-scratch.dockerfile +48 -0
  148. dockerls/infrastructure/templates/hardening/go.dockerfile +50 -0
  149. dockerls/infrastructure/templates/hardening/gradle-alpine.dockerfile +51 -0
  150. dockerls/infrastructure/templates/hardening/gradle.dockerfile +52 -0
  151. dockerls/infrastructure/templates/hardening/java-alpine.dockerfile +50 -0
  152. dockerls/infrastructure/templates/hardening/java-debian.dockerfile +50 -0
  153. dockerls/infrastructure/templates/hardening/java-distroless.dockerfile +39 -0
  154. dockerls/infrastructure/templates/hardening/java-ubuntu.dockerfile +54 -0
  155. dockerls/infrastructure/templates/hardening/java.dockerfile +60 -0
  156. dockerls/infrastructure/templates/hardening/maven-alpine.dockerfile +56 -0
  157. dockerls/infrastructure/templates/hardening/maven.dockerfile +57 -0
  158. dockerls/infrastructure/templates/hardening/node-alpine.dockerfile +47 -0
  159. dockerls/infrastructure/templates/hardening/node-debian.dockerfile +54 -0
  160. dockerls/infrastructure/templates/hardening/node-distroless.dockerfile +46 -0
  161. dockerls/infrastructure/templates/hardening/node-ubuntu.dockerfile +63 -0
  162. dockerls/infrastructure/templates/hardening/node.dockerfile +61 -0
  163. dockerls/infrastructure/templates/hardening/php-alpine.dockerfile +45 -0
  164. dockerls/infrastructure/templates/hardening/php-debian.dockerfile +45 -0
  165. dockerls/infrastructure/templates/hardening/php-ubuntu.dockerfile +49 -0
  166. dockerls/infrastructure/templates/hardening/php.dockerfile +44 -0
  167. dockerls/infrastructure/templates/hardening/python-alpine.dockerfile +51 -0
  168. dockerls/infrastructure/templates/hardening/python-debian.dockerfile +54 -0
  169. dockerls/infrastructure/templates/hardening/python-distroless.dockerfile +51 -0
  170. dockerls/infrastructure/templates/hardening/python-ubuntu.dockerfile +60 -0
  171. dockerls/infrastructure/templates/hardening/python.dockerfile +58 -0
  172. dockerls/infrastructure/templates/hardening/ruby-alpine.dockerfile +48 -0
  173. dockerls/infrastructure/templates/hardening/ruby-debian.dockerfile +50 -0
  174. dockerls/infrastructure/templates/hardening/rust-alpine.dockerfile +50 -0
  175. dockerls/infrastructure/templates/hardening/rust-debian.dockerfile +48 -0
  176. dockerls/infrastructure/templates/hardening/rust-scratch.dockerfile +44 -0
  177. dockerls/infrastructure/templates/hardening/rust.dockerfile +54 -0
  178. dockerls/infrastructure/templates/hardening/ubuntu.dockerfile +49 -0
  179. dockerls/infrastructure/toolchain/__init__.py +0 -0
  180. dockerls/infrastructure/toolchain/db_metadata.py +115 -0
  181. dockerls/infrastructure/toolchain/installer.py +435 -0
  182. dockerls/integrations/__init__.py +0 -0
  183. dockerls/integrations/dhi/__init__.py +0 -0
  184. dockerls/integrations/dhi/catalog.py +457 -0
  185. dockerls/integrations/dhi/definition.py +151 -0
  186. dockerls/integrations/dhi/repository.py +238 -0
  187. dockerls/integrations/dockerhub/__init__.py +0 -0
  188. dockerls/integrations/dockerhub/client.py +318 -0
  189. dockerls/integrations/dockerhub/urls.py +75 -0
  190. dockerls/integrations/endoflife/__init__.py +0 -0
  191. dockerls/integrations/endoflife/checker.py +216 -0
  192. dockerls/integrations/engine/__init__.py +0 -0
  193. dockerls/integrations/engine/batch.py +197 -0
  194. dockerls/integrations/engine/client.py +330 -0
  195. dockerls/integrations/engine/locator.py +96 -0
  196. dockerls/integrations/exploitdb/__init__.py +0 -0
  197. dockerls/integrations/exploitdb/client.py +271 -0
  198. dockerls/integrations/grype/__init__.py +0 -0
  199. dockerls/integrations/grype/scanner.py +336 -0
  200. dockerls/integrations/registry/__init__.py +0 -0
  201. dockerls/integrations/registry/hardened.py +284 -0
  202. dockerls/integrations/registry/inspector.py +420 -0
  203. dockerls/integrations/registry/oci.py +259 -0
  204. dockerls/integrations/registry/private.py +79 -0
  205. dockerls/integrations/registry/urls.py +36 -0
  206. dockerls/integrations/scan_errors.py +67 -0
  207. dockerls/integrations/scan_target.py +57 -0
  208. dockerls/integrations/signing/__init__.py +0 -0
  209. dockerls/integrations/signing/cosign.py +484 -0
  210. dockerls/integrations/threat_intel/__init__.py +0 -0
  211. dockerls/integrations/threat_intel/client.py +290 -0
  212. dockerls/integrations/trivy/__init__.py +0 -0
  213. dockerls/integrations/trivy/cache_pool.py +176 -0
  214. dockerls/integrations/trivy/scanner.py +447 -0
  215. dockerls/utils/__init__.py +0 -0
  216. dockerls/utils/auth.py +108 -0
  217. dockerls/utils/executables.py +39 -0
  218. dockerls/utils/ignore_file.py +130 -0
  219. dockerls/utils/rate_limit.py +130 -0
  220. dockerls/utils/resources.py +183 -0
  221. dockerls/utils/retry.py +31 -0
  222. dockerls/utils/safe_yaml.py +166 -0
  223. dockerls/utils/subprocess_runner.py +216 -0
  224. dockerls/utils/validation.py +72 -0
  225. dockerls-1.0.0.dist-info/METADATA +563 -0
  226. dockerls-1.0.0.dist-info/RECORD +230 -0
  227. dockerls-1.0.0.dist-info/WHEEL +5 -0
  228. dockerls-1.0.0.dist-info/entry_points.txt +2 -0
  229. dockerls-1.0.0.dist-info/licenses/LICENSE +21 -0
  230. dockerls-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,244 @@
1
+ """`dockerls base` -- as bases do seu Dockerfile, conferidas contra o registry.
2
+
3
+ O `analyze-dockerfile` lia o seu projeto e não media nada; o `recommend` media
4
+ e nunca olhava o seu projeto. Este comando é a ponte: ele lê os `FROM`,
5
+ pergunta ao registry qual digest cada tag aponta **agora**, e diz quais bases
6
+ apodreceram.
7
+
8
+ Aplicar por padrão é escolha deliberada, e `--dry-run` existe para quem quer
9
+ ver antes. O motivo é o caso que este comando existe para pegar: uma base
10
+ fixada num digest velho não avisa ninguém, não quebra nada, e continua entrando
11
+ em produção build após build. Um comando que só relata delega ao esquecimento
12
+ exatamente o problema que o esquecimento causou.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import json
19
+ from typing import TYPE_CHECKING
20
+
21
+ import typer
22
+ from loguru import logger
23
+ from rich.console import Console
24
+
25
+ from dockerls.cli.dependencies import build_host_guard, build_registry_credentials
26
+ from dockerls.cli.options import OutputFormat, parse_output_format
27
+ from dockerls.cli.text import safe
28
+ from dockerls.domain.value_objects.base_upgrade import BaseStatus
29
+ from dockerls.exit_codes import EXIT_ERROR, EXIT_OK, EXIT_POLICY
30
+
31
+ if TYPE_CHECKING:
32
+ from dockerls.application.services.alternatives_lookup import (
33
+ AlternativeFailure,
34
+ AlternativeSuggestion,
35
+ )
36
+ from dockerls.application.use_cases.upgrade_base import UpgradeBaseResult
37
+
38
+ Alternative = AlternativeSuggestion | AlternativeFailure
39
+
40
+ console = Console()
41
+
42
+ #: Cor por estado. `UNRESOLVED` é amarelo, não verde: não medir não é estar em
43
+ #: dia.
44
+ _COLORS = {
45
+ BaseStatus.PINNED_CURRENT: "green",
46
+ BaseStatus.PINNED_STALE: "red",
47
+ BaseStatus.UNPINNED: "yellow",
48
+ BaseStatus.UNRESOLVED: "yellow",
49
+ }
50
+
51
+
52
+ def base(
53
+ path: str = typer.Argument(".", help="Directory containing a Dockerfile, or the file itself"),
54
+ dry_run: bool = typer.Option(
55
+ False, "--dry-run", help="Show what would change without writing to the file"
56
+ ),
57
+ alternatives: bool = typer.Option(
58
+ False,
59
+ "--alternatives",
60
+ help=(
61
+ "Beyond refreshing the digest, look for a safer base for each FROM and "
62
+ "measure both. Requires a scanner and takes minutes"
63
+ ),
64
+ ),
65
+ output_format: str = typer.Option(
66
+ OutputFormat.TABLE.value, "--format", "-f", help="Output format: table or json"
67
+ ),
68
+ no_color: bool = typer.Option(False, "--no-color", help="Disable colored output"),
69
+ ) -> None:
70
+ """Check the Dockerfile bases against the registry and refresh their digests."""
71
+ if no_color:
72
+ console.no_color = True
73
+ fmt = parse_output_format(output_format)
74
+ asyncio.run(_base(path, apply=not dry_run, output_format=fmt, with_alternatives=alternatives))
75
+
76
+
77
+ async def _base(
78
+ path: str, *, apply: bool, output_format: OutputFormat, with_alternatives: bool = False
79
+ ) -> None:
80
+ # Import tardio: montar o inspector puxa configuração e cliente HTTP, e o
81
+ # `--help` deste comando não precisa de nada disso.
82
+ from dockerls.application.services.tag_history_store import TagHistoryStore
83
+ from dockerls.application.use_cases.upgrade_base import UpgradeBaseUseCase
84
+ from dockerls.cli.dependencies import build_cache
85
+ from dockerls.integrations.registry.inspector import RegistryInspector
86
+
87
+ inspector = RegistryInspector(
88
+ guard=build_host_guard(), credentials=build_registry_credentials()
89
+ )
90
+ # O histórico é um extra sobre o diagnóstico: se o cache não abrir, o
91
+ # comando continua sem ele em vez de falhar por causa de um enfeite.
92
+ try:
93
+ history = TagHistoryStore(build_cache())
94
+ except Exception as e: # pragma: no cover - abrir o cache é o caminho instável
95
+ logger.debug(f"Tag history unavailable: {e}")
96
+ history = TagHistoryStore(None)
97
+ try:
98
+ result = await UpgradeBaseUseCase(inspector, history).execute(path, apply=apply)
99
+ finally:
100
+ await inspector.close()
101
+
102
+ suggestions = await _alternatives_for(result) if with_alternatives else []
103
+
104
+ if output_format == OutputFormat.JSON:
105
+ payload = result.to_dict()
106
+ if with_alternatives:
107
+ payload["alternatives"] = [s.to_dict() for s in suggestions]
108
+ console.print(json.dumps(payload, indent=2, ensure_ascii=False), soft_wrap=True)
109
+ else:
110
+ _render(result, apply=apply)
111
+ if with_alternatives:
112
+ _render_alternatives(suggestions)
113
+
114
+ if result.error:
115
+ raise typer.Exit(EXIT_ERROR)
116
+ # Reprova quando sobrou base desatualizada: em `--dry-run` isso é o portão
117
+ # de CI, e depois de aplicar só sobra o que não deu para corrigir.
118
+ raise typer.Exit(EXIT_POLICY if result.needs_action and not result.applied else EXIT_OK)
119
+
120
+
121
+ def _render(result: UpgradeBaseResult, *, apply: bool) -> None:
122
+ if result.error:
123
+ console.print(f"[red]Error:[/red] {safe(result.error)}")
124
+ return
125
+
126
+ console.print(f"[bold]{safe(result.dockerfile)}[/bold]\n")
127
+ for finding in result.findings:
128
+ color = _COLORS.get(finding.status, "white")
129
+ stage = f" [dim](stage {safe(finding.base.stage)})[/dim]" if finding.base.stage else ""
130
+ console.print(
131
+ f" line {finding.base.line} [{color}]{finding.status}[/{color}]{stage}\n"
132
+ f" {safe(finding.base.reference)}"
133
+ )
134
+ console.print(f" [dim]{safe(finding.explain())}[/dim]")
135
+ historico = result.history_for(finding.base)
136
+ if historico is not None and historico.moves:
137
+ console.print(f" [dim]history: {safe(historico.explain())}[/dim]")
138
+ if finding.proposed_reference:
139
+ alvo = (
140
+ f"ARG {safe(finding.base.digest_arg)}"
141
+ if finding.base.digest_arg
142
+ else f"line {finding.base.line}"
143
+ )
144
+ console.print(
145
+ f" [green]->[/green] {safe(finding.proposed_reference)} [dim]({alvo})[/dim]"
146
+ )
147
+ console.print()
148
+
149
+ resumo = []
150
+ if result.outdated:
151
+ resumo.append(f"{len(result.outdated)} outdated")
152
+ if result.unpinned:
153
+ resumo.append(f"{len(result.unpinned)} without a digest")
154
+ if resumo:
155
+ console.print(f"[bold]{', '.join(resumo)}[/bold]\n")
156
+
157
+ if result.applied:
158
+ console.print(
159
+ f"[green]{result.applied} update(s) written to {safe(result.dockerfile)}.[/green]"
160
+ )
161
+ console.print(
162
+ "[dim]Rebuild and scan before publishing: changing the base digest "
163
+ "changes the image, and only a scan tells you whether for the better.[/dim]"
164
+ )
165
+ elif result.needs_action:
166
+ acao = "Nothing was written (--dry-run)." if not apply else "Nothing could be written."
167
+ console.print(f"[yellow]{acao}[/yellow]")
168
+ else:
169
+ console.print("[green]Every base is at the digest its tag points to today.[/green]")
170
+
171
+ if result.unresolved:
172
+ console.print(
173
+ f"[yellow]{len(result.unresolved)} base(s) could not be queried on the "
174
+ "registry -- that is an absent answer, not confirmation that they are "
175
+ "up to date.[/yellow]"
176
+ )
177
+
178
+
179
+ async def _alternatives_for(result: UpgradeBaseResult) -> list[Alternative]:
180
+ """Uma alternativa medida para cada base distinta do Dockerfile.
181
+
182
+ Cada base é consultada uma vez: um Dockerfile multi-estágio costuma repetir
183
+ a mesma imagem, e escanear a mesma coisa três vezes só gastaria minutos
184
+ para imprimir a mesma linha.
185
+ """
186
+ from dockerls.application.services.alternatives_lookup import best_alternative
187
+ from dockerls.cli.dependencies import build_analyze_use_case, build_recommend_use_case
188
+
189
+ referencias: list[str] = []
190
+ for finding in result.findings:
191
+ reference = f"{finding.base.name}:{finding.base.tag or 'latest'}"
192
+ if finding.base.name and reference not in referencias:
193
+ referencias.append(reference)
194
+ if not referencias:
195
+ return []
196
+
197
+ console.print(
198
+ "\n[dim]Measuring alternatives: each base is scanned alongside the "
199
+ "candidates, because a comparison between a measurement and a reputation is "
200
+ "not a comparison.[/dim]"
201
+ )
202
+ analyzer = await build_analyze_use_case()
203
+ recommender = await build_recommend_use_case()
204
+ return [
205
+ await best_alternative(reference, analyzer=analyzer, recommender=recommender)
206
+ for reference in referencias
207
+ ]
208
+
209
+
210
+ def _render_alternatives(suggestions: list[Alternative]) -> None:
211
+ from dockerls.application.services.alternatives_lookup import AlternativeFailure
212
+
213
+ console.print("\n[bold]Measured alternatives[/bold]\n")
214
+ for item in suggestions:
215
+ if isinstance(item, AlternativeFailure):
216
+ # Não medir nunca vira "não há nada melhor": são frases diferentes
217
+ # e levam a decisões diferentes.
218
+ console.print(f" [yellow]?[/yellow] {safe(item.reference)}")
219
+ console.print(f" [dim]{safe(item.reason)}[/dim]")
220
+ continue
221
+ cor = "green" if item.improves else "yellow"
222
+ console.print(f" {safe(item.reference)}")
223
+ console.print(
224
+ f" [{cor}]-> {safe(item.plan.to_pinned_reference or item.plan.to_reference)}[/{cor}]"
225
+ )
226
+ console.print(
227
+ f" [dim]CRITICAL {item.plan.critical_delta:+d}, "
228
+ f"HIGH {item.plan.high_delta:+d}, score {item.plan.score_delta:+.1f}[/dim]"
229
+ )
230
+ for troca in item.plan.trade_offs:
231
+ console.print(f" [yellow]![/yellow] [dim]{safe(troca)}[/dim]")
232
+ if not item.improves:
233
+ console.print(
234
+ " [dim]the best-placed candidate does not improve on what was "
235
+ "measured; reported anyway, because hiding what came out worse would "
236
+ "turn the list into an argument.[/dim]"
237
+ )
238
+ console.print()
239
+
240
+ console.print(
241
+ "[dim]Nothing here is applied: switching base family is an architecture "
242
+ "decision, not a digest refresh. `base` writes digests; swapping the image "
243
+ "is yours.[/dim]"
244
+ )