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.
- dockerls/__init__.py +31 -0
- dockerls/application/__init__.py +0 -0
- dockerls/application/dto/__init__.py +3 -0
- dockerls/application/dto/analysis.py +257 -0
- dockerls/application/services/__init__.py +0 -0
- dockerls/application/services/alternatives_lookup.py +167 -0
- dockerls/application/services/composite_repository.py +106 -0
- dockerls/application/services/cross_validation.py +233 -0
- dockerls/application/services/ecosystems.py +350 -0
- dockerls/application/services/fallback_scanner.py +97 -0
- dockerls/application/services/hardening_analysis.py +174 -0
- dockerls/application/services/migration.py +297 -0
- dockerls/application/services/progress.py +56 -0
- dockerls/application/services/remediation.py +321 -0
- dockerls/application/services/scan_history_store.py +88 -0
- dockerls/application/services/scanner_factory.py +88 -0
- dockerls/application/services/source_registry.py +151 -0
- dockerls/application/services/tag_history_store.py +76 -0
- dockerls/application/services/teardown.py +50 -0
- dockerls/application/services/verdict.py +298 -0
- dockerls/application/services/version_discovery.py +108 -0
- dockerls/application/use_cases/__init__.py +0 -0
- dockerls/application/use_cases/analyze_dockerfile.py +103 -0
- dockerls/application/use_cases/analyze_image.py +173 -0
- dockerls/application/use_cases/build_image.py +1795 -0
- dockerls/application/use_cases/compare_images.py +91 -0
- dockerls/application/use_cases/fleet_scan.py +240 -0
- dockerls/application/use_cases/recommend_images.py +1078 -0
- dockerls/application/use_cases/registry_audit.py +133 -0
- dockerls/application/use_cases/search_images.py +23 -0
- dockerls/application/use_cases/upgrade_base.py +167 -0
- dockerls/cache/__init__.py +0 -0
- dockerls/cache/sqlite_cache.py +184 -0
- dockerls/cli/__init__.py +0 -0
- dockerls/cli/analysis_baseline.py +98 -0
- dockerls/cli/app.py +294 -0
- dockerls/cli/commands/__init__.py +0 -0
- dockerls/cli/commands/advisor.py +262 -0
- dockerls/cli/commands/alternatives.py +291 -0
- dockerls/cli/commands/analyze.py +429 -0
- dockerls/cli/commands/analyze_dockerfile.py +104 -0
- dockerls/cli/commands/base_cmd.py +244 -0
- dockerls/cli/commands/base_image.py +551 -0
- dockerls/cli/commands/build.py +1300 -0
- dockerls/cli/commands/cache_cmd.py +104 -0
- dockerls/cli/commands/compare.py +177 -0
- dockerls/cli/commands/controls.py +110 -0
- dockerls/cli/commands/doctor.py +566 -0
- dockerls/cli/commands/export.py +81 -0
- dockerls/cli/commands/fleet.py +159 -0
- dockerls/cli/commands/health.py +84 -0
- dockerls/cli/commands/login.py +53 -0
- dockerls/cli/commands/policy_cmd.py +111 -0
- dockerls/cli/commands/provenance_cmd.py +162 -0
- dockerls/cli/commands/recommend.py +761 -0
- dockerls/cli/commands/registry_audit_cmd.py +103 -0
- dockerls/cli/commands/sbom.py +144 -0
- dockerls/cli/commands/search.py +86 -0
- dockerls/cli/commands/verify.py +115 -0
- dockerls/cli/commands/version.py +12 -0
- dockerls/cli/commands/vex_cmd.py +117 -0
- dockerls/cli/dependencies.py +530 -0
- dockerls/cli/image_names.py +79 -0
- dockerls/cli/options.py +42 -0
- dockerls/cli/progress.py +145 -0
- dockerls/cli/publish_prompt.py +123 -0
- dockerls/cli/rendering.py +214 -0
- dockerls/cli/runtime.py +65 -0
- dockerls/cli/scan_failure.py +71 -0
- dockerls/cli/text.py +39 -0
- dockerls/cli/validators.py +35 -0
- dockerls/cli/vulnerability_view.py +154 -0
- dockerls/domain/__init__.py +0 -0
- dockerls/domain/entities/__init__.py +75 -0
- dockerls/domain/entities/declared_metadata.py +147 -0
- dockerls/domain/entities/dockerfile_analysis.py +318 -0
- dockerls/domain/entities/image.py +109 -0
- dockerls/domain/entities/image_facts.py +137 -0
- dockerls/domain/entities/recommendation.py +33 -0
- dockerls/domain/entities/scan_result.py +129 -0
- dockerls/domain/entities/vulnerability.py +232 -0
- dockerls/domain/interfaces/__init__.py +17 -0
- dockerls/domain/interfaces/cache_store.py +18 -0
- dockerls/domain/interfaces/dockerfile_validator.py +99 -0
- dockerls/domain/interfaces/eol_checker.py +11 -0
- dockerls/domain/interfaces/image_repository.py +15 -0
- dockerls/domain/interfaces/scanner.py +15 -0
- dockerls/domain/security_controls.py +362 -0
- dockerls/domain/value_objects/__init__.py +49 -0
- dockerls/domain/value_objects/attack_surface.py +198 -0
- dockerls/domain/value_objects/base_recipe.py +600 -0
- dockerls/domain/value_objects/base_upgrade.py +292 -0
- dockerls/domain/value_objects/build_labels.py +99 -0
- dockerls/domain/value_objects/build_policy.py +412 -0
- dockerls/domain/value_objects/confidence.py +156 -0
- dockerls/domain/value_objects/fleet.py +174 -0
- dockerls/domain/value_objects/gate.py +327 -0
- dockerls/domain/value_objects/hardening.py +303 -0
- dockerls/domain/value_objects/image_reference.py +90 -0
- dockerls/domain/value_objects/inheritance.py +352 -0
- dockerls/domain/value_objects/network_policy.py +280 -0
- dockerls/domain/value_objects/production_readiness.py +145 -0
- dockerls/domain/value_objects/provenance.py +211 -0
- dockerls/domain/value_objects/recipe_diff.py +188 -0
- dockerls/domain/value_objects/registry_audit.py +195 -0
- dockerls/domain/value_objects/registry_target.py +225 -0
- dockerls/domain/value_objects/remediation_score.py +62 -0
- dockerls/domain/value_objects/scan_history.py +183 -0
- dockerls/domain/value_objects/scan_plan.py +193 -0
- dockerls/domain/value_objects/scanner_db.py +143 -0
- dockerls/domain/value_objects/security_score.py +160 -0
- dockerls/domain/value_objects/security_tier.py +122 -0
- dockerls/domain/value_objects/tag_history.py +180 -0
- dockerls/domain/value_objects/tool_release.py +253 -0
- dockerls/domain/value_objects/tristate.py +47 -0
- dockerls/domain/value_objects/vex.py +249 -0
- dockerls/exit_codes.py +23 -0
- dockerls/exporters/__init__.py +0 -0
- dockerls/exporters/base.py +17 -0
- dockerls/exporters/csv_exporter.py +85 -0
- dockerls/exporters/factory.py +31 -0
- dockerls/exporters/html_exporter.py +105 -0
- dockerls/exporters/json_exporter.py +19 -0
- dockerls/exporters/markdown_exporter.py +84 -0
- dockerls/exporters/sarif_exporter.py +245 -0
- dockerls/infrastructure/__init__.py +0 -0
- dockerls/infrastructure/config/__init__.py +0 -0
- dockerls/infrastructure/config/policy_file.py +165 -0
- dockerls/infrastructure/config/settings.py +197 -0
- dockerls/infrastructure/database/__init__.py +0 -0
- dockerls/infrastructure/database/models.py +78 -0
- dockerls/infrastructure/dockerfile_validator.py +1899 -0
- dockerls/infrastructure/evidence.py +99 -0
- dockerls/infrastructure/hashing.py +165 -0
- dockerls/infrastructure/logging/__init__.py +0 -0
- dockerls/infrastructure/logging/setup.py +98 -0
- dockerls/infrastructure/network/__init__.py +0 -0
- dockerls/infrastructure/network/guarded_client.py +107 -0
- dockerls/infrastructure/network/host_guard.py +117 -0
- dockerls/infrastructure/redaction.py +135 -0
- dockerls/infrastructure/templates/hardening/alpine.dockerfile +44 -0
- dockerls/infrastructure/templates/hardening/debian.dockerfile +45 -0
- dockerls/infrastructure/templates/hardening/distroless.dockerfile +34 -0
- dockerls/infrastructure/templates/hardening/go-alpine.dockerfile +52 -0
- dockerls/infrastructure/templates/hardening/go-debian.dockerfile +54 -0
- dockerls/infrastructure/templates/hardening/go-distroless.dockerfile +43 -0
- dockerls/infrastructure/templates/hardening/go-scratch.dockerfile +48 -0
- dockerls/infrastructure/templates/hardening/go.dockerfile +50 -0
- dockerls/infrastructure/templates/hardening/gradle-alpine.dockerfile +51 -0
- dockerls/infrastructure/templates/hardening/gradle.dockerfile +52 -0
- dockerls/infrastructure/templates/hardening/java-alpine.dockerfile +50 -0
- dockerls/infrastructure/templates/hardening/java-debian.dockerfile +50 -0
- dockerls/infrastructure/templates/hardening/java-distroless.dockerfile +39 -0
- dockerls/infrastructure/templates/hardening/java-ubuntu.dockerfile +54 -0
- dockerls/infrastructure/templates/hardening/java.dockerfile +60 -0
- dockerls/infrastructure/templates/hardening/maven-alpine.dockerfile +56 -0
- dockerls/infrastructure/templates/hardening/maven.dockerfile +57 -0
- dockerls/infrastructure/templates/hardening/node-alpine.dockerfile +47 -0
- dockerls/infrastructure/templates/hardening/node-debian.dockerfile +54 -0
- dockerls/infrastructure/templates/hardening/node-distroless.dockerfile +46 -0
- dockerls/infrastructure/templates/hardening/node-ubuntu.dockerfile +63 -0
- dockerls/infrastructure/templates/hardening/node.dockerfile +61 -0
- dockerls/infrastructure/templates/hardening/php-alpine.dockerfile +45 -0
- dockerls/infrastructure/templates/hardening/php-debian.dockerfile +45 -0
- dockerls/infrastructure/templates/hardening/php-ubuntu.dockerfile +49 -0
- dockerls/infrastructure/templates/hardening/php.dockerfile +44 -0
- dockerls/infrastructure/templates/hardening/python-alpine.dockerfile +51 -0
- dockerls/infrastructure/templates/hardening/python-debian.dockerfile +54 -0
- dockerls/infrastructure/templates/hardening/python-distroless.dockerfile +51 -0
- dockerls/infrastructure/templates/hardening/python-ubuntu.dockerfile +60 -0
- dockerls/infrastructure/templates/hardening/python.dockerfile +58 -0
- dockerls/infrastructure/templates/hardening/ruby-alpine.dockerfile +48 -0
- dockerls/infrastructure/templates/hardening/ruby-debian.dockerfile +50 -0
- dockerls/infrastructure/templates/hardening/rust-alpine.dockerfile +50 -0
- dockerls/infrastructure/templates/hardening/rust-debian.dockerfile +48 -0
- dockerls/infrastructure/templates/hardening/rust-scratch.dockerfile +44 -0
- dockerls/infrastructure/templates/hardening/rust.dockerfile +54 -0
- dockerls/infrastructure/templates/hardening/ubuntu.dockerfile +49 -0
- dockerls/infrastructure/toolchain/__init__.py +0 -0
- dockerls/infrastructure/toolchain/db_metadata.py +115 -0
- dockerls/infrastructure/toolchain/installer.py +435 -0
- dockerls/integrations/__init__.py +0 -0
- dockerls/integrations/dhi/__init__.py +0 -0
- dockerls/integrations/dhi/catalog.py +457 -0
- dockerls/integrations/dhi/definition.py +151 -0
- dockerls/integrations/dhi/repository.py +238 -0
- dockerls/integrations/dockerhub/__init__.py +0 -0
- dockerls/integrations/dockerhub/client.py +318 -0
- dockerls/integrations/dockerhub/urls.py +75 -0
- dockerls/integrations/endoflife/__init__.py +0 -0
- dockerls/integrations/endoflife/checker.py +216 -0
- dockerls/integrations/engine/__init__.py +0 -0
- dockerls/integrations/engine/batch.py +197 -0
- dockerls/integrations/engine/client.py +330 -0
- dockerls/integrations/engine/locator.py +96 -0
- dockerls/integrations/exploitdb/__init__.py +0 -0
- dockerls/integrations/exploitdb/client.py +271 -0
- dockerls/integrations/grype/__init__.py +0 -0
- dockerls/integrations/grype/scanner.py +336 -0
- dockerls/integrations/registry/__init__.py +0 -0
- dockerls/integrations/registry/hardened.py +284 -0
- dockerls/integrations/registry/inspector.py +420 -0
- dockerls/integrations/registry/oci.py +259 -0
- dockerls/integrations/registry/private.py +79 -0
- dockerls/integrations/registry/urls.py +36 -0
- dockerls/integrations/scan_errors.py +67 -0
- dockerls/integrations/scan_target.py +57 -0
- dockerls/integrations/signing/__init__.py +0 -0
- dockerls/integrations/signing/cosign.py +484 -0
- dockerls/integrations/threat_intel/__init__.py +0 -0
- dockerls/integrations/threat_intel/client.py +290 -0
- dockerls/integrations/trivy/__init__.py +0 -0
- dockerls/integrations/trivy/cache_pool.py +176 -0
- dockerls/integrations/trivy/scanner.py +447 -0
- dockerls/utils/__init__.py +0 -0
- dockerls/utils/auth.py +108 -0
- dockerls/utils/executables.py +39 -0
- dockerls/utils/ignore_file.py +130 -0
- dockerls/utils/rate_limit.py +130 -0
- dockerls/utils/resources.py +183 -0
- dockerls/utils/retry.py +31 -0
- dockerls/utils/safe_yaml.py +166 -0
- dockerls/utils/subprocess_runner.py +216 -0
- dockerls/utils/validation.py +72 -0
- dockerls-1.0.0.dist-info/METADATA +563 -0
- dockerls-1.0.0.dist-info/RECORD +230 -0
- dockerls-1.0.0.dist-info/WHEEL +5 -0
- dockerls-1.0.0.dist-info/entry_points.txt +2 -0
- dockerls-1.0.0.dist-info/licenses/LICENSE +21 -0
- dockerls-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""`dockerls alternatives <image:tag>` -- safer replacements for what you run today.
|
|
2
|
+
|
|
3
|
+
`recommend` answers "what should I use for node"; this answers the question
|
|
4
|
+
people actually have, which is "I am running *this*, is there something
|
|
5
|
+
better, and what does switching cost me". The difference is the baseline: a
|
|
6
|
+
concrete image the user already depends on, scanned under the same pipeline
|
|
7
|
+
as the candidates so the comparison is between two measurements rather than
|
|
8
|
+
between a measurement and a reputation.
|
|
9
|
+
|
|
10
|
+
Three properties make the output honest:
|
|
11
|
+
|
|
12
|
+
* the current image is scanned, not assumed. If it cannot be scanned, the
|
|
13
|
+
command says so and refuses to claim an improvement it cannot compute;
|
|
14
|
+
* every alternative is ranked by the same multi-source policy as
|
|
15
|
+
`recommend`, so a hardened-catalogue image wins on evidence rather than
|
|
16
|
+
on the name of its vendor;
|
|
17
|
+
* trade-offs are printed next to gains. A libc change, a missing shell or a
|
|
18
|
+
different package manager will break somebody's build, and a migration
|
|
19
|
+
suggestion that hides that is worse than no suggestion.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import json
|
|
26
|
+
from typing import TYPE_CHECKING
|
|
27
|
+
|
|
28
|
+
import typer
|
|
29
|
+
from rich.console import Console
|
|
30
|
+
from rich.panel import Panel
|
|
31
|
+
from rich.table import Table
|
|
32
|
+
|
|
33
|
+
from dockerls.application.services.migration import MigrationPlan, plan_migration
|
|
34
|
+
from dockerls.application.services.source_registry import UnknownSourceError
|
|
35
|
+
from dockerls.cli.dependencies import build_analyze_use_case, build_recommend_use_case
|
|
36
|
+
from dockerls.cli.image_names import display_reference, split_repository_and_tag
|
|
37
|
+
from dockerls.cli.options import OutputFormat, parse_output_format
|
|
38
|
+
from dockerls.cli.progress import RichScanObserver
|
|
39
|
+
from dockerls.cli.scan_failure import describe_scan_failure
|
|
40
|
+
from dockerls.cli.text import safe
|
|
41
|
+
from dockerls.cli.validators import check_workers
|
|
42
|
+
from dockerls.exit_codes import EXIT_ERROR, EXIT_OK
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from dockerls.application.dto.analysis import ImageAnalysis
|
|
46
|
+
|
|
47
|
+
console = Console()
|
|
48
|
+
# Diagnostics go to stderr, results to stdout. Printing a warning to stdout
|
|
49
|
+
# put a human sentence in front of the JSON document and made `--format json`
|
|
50
|
+
# unparseable -- a machine-readable format is only machine-readable if
|
|
51
|
+
# nothing else can land in the stream.
|
|
52
|
+
diagnostics = Console(stderr=True)
|
|
53
|
+
|
|
54
|
+
#: Exit codes, consistent with `recommend`:
|
|
55
|
+
#: 0 = a safer alternative was found (or the current image is already best)
|
|
56
|
+
#: 1 = a technical failure: the current image could not be measured
|
|
57
|
+
#: 2 = alternatives exist but none of them clears the baseline
|
|
58
|
+
EXIT_FOUND = EXIT_OK
|
|
59
|
+
EXIT_BELOW_BASELINE = 2
|
|
60
|
+
|
|
61
|
+
#: How many alternatives to present. More than this and the output stops
|
|
62
|
+
#: being a decision and becomes a catalogue.
|
|
63
|
+
TOP_ALTERNATIVES = 4
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def alternatives(
|
|
67
|
+
image: str = typer.Argument(help="The image you run today (e.g. node:22, python:3.12-slim)"),
|
|
68
|
+
workers: int | None = typer.Option(
|
|
69
|
+
None,
|
|
70
|
+
"--workers",
|
|
71
|
+
"-w",
|
|
72
|
+
help="Concurrent scanner processes; 0 sizes it to this machine [config: workers]",
|
|
73
|
+
),
|
|
74
|
+
source: list[str] = typer.Option(
|
|
75
|
+
[],
|
|
76
|
+
"--source",
|
|
77
|
+
"-s",
|
|
78
|
+
help="Image source to search; repeatable. dockerhub, chainguard, distroless, dhi, "
|
|
79
|
+
"private (if configured), all",
|
|
80
|
+
),
|
|
81
|
+
all_sources: bool = typer.Option(
|
|
82
|
+
False, "--all-sources", help="Search every configured source, including opt-in ones"
|
|
83
|
+
),
|
|
84
|
+
output_format: str = typer.Option(
|
|
85
|
+
OutputFormat.TABLE.value, "--format", "-f", help="Output format: table or json"
|
|
86
|
+
),
|
|
87
|
+
no_color: bool = typer.Option(False, "--no-color", help="Disable colored output"),
|
|
88
|
+
no_progress: bool = typer.Option(False, "--no-progress", help="Disable the progress display"),
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Find safer alternatives to an image you already run, with trade-offs."""
|
|
91
|
+
if no_color:
|
|
92
|
+
console.no_color = True
|
|
93
|
+
fmt = parse_output_format(output_format)
|
|
94
|
+
# `0` means "size it to this machine", so it is passed through rather
|
|
95
|
+
# than validated: the resolver, not the flag, decides what it becomes.
|
|
96
|
+
if workers:
|
|
97
|
+
workers = check_workers(workers)
|
|
98
|
+
try:
|
|
99
|
+
asyncio.run(
|
|
100
|
+
_alternatives(
|
|
101
|
+
image,
|
|
102
|
+
workers,
|
|
103
|
+
fmt,
|
|
104
|
+
sources=list(source) or None,
|
|
105
|
+
all_sources=all_sources,
|
|
106
|
+
show_progress=not no_progress and fmt != OutputFormat.JSON,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
except UnknownSourceError as e:
|
|
110
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
111
|
+
raise typer.Exit(EXIT_ERROR) from e
|
|
112
|
+
except ValueError as e:
|
|
113
|
+
console.print(f"[red]Invalid configuration:[/red] {e}")
|
|
114
|
+
raise typer.Exit(EXIT_ERROR) from e
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def _alternatives(
|
|
118
|
+
reference: str,
|
|
119
|
+
workers: int | None,
|
|
120
|
+
output_format: OutputFormat,
|
|
121
|
+
*,
|
|
122
|
+
sources: list[str] | None = None,
|
|
123
|
+
all_sources: bool = False,
|
|
124
|
+
show_progress: bool = True,
|
|
125
|
+
) -> None:
|
|
126
|
+
# A regra compartilhada, e não um `rsplit(":", 1)`: a porta de um
|
|
127
|
+
# registry (`registry.internal:5000/app`) não é tag, e procurar o
|
|
128
|
+
# repositório "registry.internal" não devolve nada.
|
|
129
|
+
repository, tag = split_repository_and_tag(reference)
|
|
130
|
+
|
|
131
|
+
current = await _analyze_current(reference)
|
|
132
|
+
if current is None:
|
|
133
|
+
# No measurement of the current image means no honest claim about
|
|
134
|
+
# an improvement over it. The command fails rather than presenting
|
|
135
|
+
# candidates against an unknown baseline.
|
|
136
|
+
_report_unmeasurable(reference, output_format)
|
|
137
|
+
raise typer.Exit(EXIT_ERROR)
|
|
138
|
+
|
|
139
|
+
with RichScanObserver(enabled=show_progress) as observer:
|
|
140
|
+
use_case = await build_recommend_use_case(
|
|
141
|
+
workers=workers,
|
|
142
|
+
observer=observer,
|
|
143
|
+
sources=sources,
|
|
144
|
+
all_sources=all_sources,
|
|
145
|
+
)
|
|
146
|
+
result = await use_case.execute(repository)
|
|
147
|
+
|
|
148
|
+
candidates = [
|
|
149
|
+
a
|
|
150
|
+
for a in (result.recommendations or result.alternatives)
|
|
151
|
+
if a.image.full_reference != current.image.full_reference and a.confidence.is_recommendable
|
|
152
|
+
][:TOP_ALTERNATIVES]
|
|
153
|
+
|
|
154
|
+
plans = [plan_migration(current, candidate) for candidate in candidates]
|
|
155
|
+
|
|
156
|
+
if output_format == OutputFormat.JSON:
|
|
157
|
+
console.print(
|
|
158
|
+
json.dumps(
|
|
159
|
+
{
|
|
160
|
+
"current": current.model_dump(),
|
|
161
|
+
"alternatives": [
|
|
162
|
+
{"image": c.model_dump(), "migration": p.model_dump()}
|
|
163
|
+
for c, p in zip(candidates, plans, strict=True)
|
|
164
|
+
],
|
|
165
|
+
"baseline_met": result.baseline_met,
|
|
166
|
+
"sources_searched": result.sources_searched,
|
|
167
|
+
"metrics": result.metrics.model_dump(),
|
|
168
|
+
},
|
|
169
|
+
indent=2,
|
|
170
|
+
default=str,
|
|
171
|
+
),
|
|
172
|
+
soft_wrap=True,
|
|
173
|
+
)
|
|
174
|
+
raise typer.Exit(EXIT_FOUND if result.baseline_met else EXIT_BELOW_BASELINE)
|
|
175
|
+
|
|
176
|
+
_render(reference, tag, current, candidates, plans)
|
|
177
|
+
raise typer.Exit(EXIT_FOUND if result.baseline_met else EXIT_BELOW_BASELINE)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def _analyze_current(reference: str) -> ImageAnalysis | None:
|
|
181
|
+
"""Scan the image the user runs today, or return None if it cannot be.
|
|
182
|
+
|
|
183
|
+
A falha do scan chega de duas formas, e as duas significam a mesma
|
|
184
|
+
coisa aqui. Uma exceção é o caso antigo; o caso comum é um
|
|
185
|
+
`ImageAnalysis` cujo scan não completou, que carrega score 0.0 e tier F
|
|
186
|
+
por construção. Tratar o segundo como medição faria a baseline desta
|
|
187
|
+
comparação valer zero, e toda alternativa apareceria como uma melhoria
|
|
188
|
+
enorme sobre uma imagem que ninguém mediu.
|
|
189
|
+
"""
|
|
190
|
+
use_case = await build_analyze_use_case()
|
|
191
|
+
try:
|
|
192
|
+
analysis = await use_case.execute(reference)
|
|
193
|
+
except (ValueError, RuntimeError) as e:
|
|
194
|
+
diagnostics.print(f"[yellow]Could not analyze {reference}: {e}[/yellow]")
|
|
195
|
+
return None
|
|
196
|
+
finally:
|
|
197
|
+
# O scanner e o pool de conexões do repositório ficam abertos até
|
|
198
|
+
# alguém fechá-los, e este comando abre um segundo conjunto logo
|
|
199
|
+
# abaixo para a busca. `analyze` fecha no mesmo ponto.
|
|
200
|
+
await use_case.close()
|
|
201
|
+
|
|
202
|
+
if not analysis.scan.is_verified:
|
|
203
|
+
cause = describe_scan_failure(analysis.scan.error_kind, analysis.scan.error_message)
|
|
204
|
+
diagnostics.print(f"[yellow]Could not analyze {safe(reference)}: {safe(cause)}[/yellow]")
|
|
205
|
+
return None
|
|
206
|
+
return analysis
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _report_unmeasurable(reference: str, output_format: OutputFormat) -> None:
|
|
210
|
+
message = (
|
|
211
|
+
f"{reference} could not be scanned, so no improvement over it can be measured. "
|
|
212
|
+
"This is a technical failure, not a verdict about the image."
|
|
213
|
+
)
|
|
214
|
+
if output_format == OutputFormat.JSON:
|
|
215
|
+
console.print(json.dumps({"error": message, "current": reference}), soft_wrap=True)
|
|
216
|
+
else:
|
|
217
|
+
console.print(f"[red]{message}[/red]")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _render(
|
|
221
|
+
reference: str,
|
|
222
|
+
tag: str,
|
|
223
|
+
current: ImageAnalysis,
|
|
224
|
+
candidates: list[ImageAnalysis],
|
|
225
|
+
plans: list[MigrationPlan],
|
|
226
|
+
) -> None:
|
|
227
|
+
console.print(Panel(f"[bold cyan]Alternatives to {reference}[/bold cyan]", expand=False))
|
|
228
|
+
|
|
229
|
+
console.print("\n[bold]CURRENT[/bold]")
|
|
230
|
+
console.print(
|
|
231
|
+
f" {safe(current.image.full_reference)} [dim]{safe(current.image.source)}[/dim]"
|
|
232
|
+
)
|
|
233
|
+
console.print(
|
|
234
|
+
f" score {current.security_score} tier {current.tier} "
|
|
235
|
+
f"C/H/M {current.scan.critical_count}/{current.scan.high_count}/"
|
|
236
|
+
f"{current.scan.medium_count}" + (f" [dim](tag {tag})[/dim]" if tag else "")
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if not candidates:
|
|
240
|
+
console.print(
|
|
241
|
+
"\n[yellow]No alternative scored better than what you already run.[/yellow]\n"
|
|
242
|
+
"[dim]That is a result, not a failure: staying put is the right call "
|
|
243
|
+
"when nothing measured better.[/dim]"
|
|
244
|
+
)
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
console.print("\n[bold]RECOMMENDED ALTERNATIVES[/bold]")
|
|
248
|
+
table = Table()
|
|
249
|
+
table.add_column("#", justify="right", style="dim")
|
|
250
|
+
table.add_column("Image", style="cyan bold", overflow="fold")
|
|
251
|
+
table.add_column("Source", style="magenta")
|
|
252
|
+
table.add_column("Score", justify="right")
|
|
253
|
+
table.add_column("Delta", justify="right")
|
|
254
|
+
table.add_column("C/H/M", justify="center", no_wrap=True)
|
|
255
|
+
table.add_column("Conf", justify="center")
|
|
256
|
+
|
|
257
|
+
for i, (candidate, plan) in enumerate(zip(candidates, plans, strict=True), 1):
|
|
258
|
+
delta = plan.score_delta
|
|
259
|
+
delta_text = f"[green]+{delta:.1f}[/green]" if delta > 0 else f"[red]{delta:.1f}[/red]"
|
|
260
|
+
table.add_row(
|
|
261
|
+
str(i),
|
|
262
|
+
safe(display_reference(candidate.image.name, candidate.image.tag)),
|
|
263
|
+
safe(candidate.image.source),
|
|
264
|
+
f"{candidate.security_score:.1f}",
|
|
265
|
+
delta_text,
|
|
266
|
+
f"{candidate.scan.critical_count}/{candidate.scan.high_count}/"
|
|
267
|
+
f"{candidate.scan.medium_count}",
|
|
268
|
+
candidate.confidence.value[:4],
|
|
269
|
+
)
|
|
270
|
+
console.print(table)
|
|
271
|
+
|
|
272
|
+
best, best_plan = candidates[0], plans[0]
|
|
273
|
+
console.print(f"\n[bold]WHY {safe(best.image.full_reference)}[/bold]")
|
|
274
|
+
for reason in best_plan.improvements[:8]:
|
|
275
|
+
console.print(f" [green]+[/green] {safe(reason)}")
|
|
276
|
+
|
|
277
|
+
if best_plan.trade_offs:
|
|
278
|
+
console.print("\n[bold]TRADE-OFFS[/bold]")
|
|
279
|
+
for cost in best_plan.trade_offs[:8]:
|
|
280
|
+
console.print(f" [yellow]![/yellow] {safe(cost)}")
|
|
281
|
+
|
|
282
|
+
console.print("\n[bold]MIGRATION CHECKLIST[/bold]")
|
|
283
|
+
for step_number, step in enumerate(best_plan.checklist, 1):
|
|
284
|
+
console.print(f" {step_number}. {safe(step)}")
|
|
285
|
+
|
|
286
|
+
if best.image.digest_known:
|
|
287
|
+
console.print(f"\n[dim]Pin to: {best.pinned_reference}[/dim]", soft_wrap=True)
|
|
288
|
+
console.print(
|
|
289
|
+
"\n[dim]Compatibility is never assumed: the checklist above exists because "
|
|
290
|
+
"no scan can tell you whether your application still runs.[/dim]"
|
|
291
|
+
)
|