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,173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from dockerls.application.dto.analysis import ImageAnalysis
|
|
7
|
+
from dockerls.application.services.teardown import close_quietly, sources_of
|
|
8
|
+
from dockerls.application.services.verdict import apply_facts, finalize_verdict
|
|
9
|
+
from dockerls.application.use_cases.recommend_images import (
|
|
10
|
+
_enrich_with_threat_intel,
|
|
11
|
+
_eol_status,
|
|
12
|
+
)
|
|
13
|
+
from dockerls.domain.entities.image import DockerImage
|
|
14
|
+
from dockerls.domain.value_objects.image_reference import split_repository_and_tag
|
|
15
|
+
from dockerls.domain.value_objects.remediation_score import RemediationScore
|
|
16
|
+
from dockerls.domain.value_objects.security_score import SecurityScore
|
|
17
|
+
from dockerls.domain.value_objects.security_tier import SecurityTier
|
|
18
|
+
from dockerls.utils.ignore_file import active_ignored_cve_ids, load_ignore_rules
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from dockerls.application.services.hardening_analysis import HardeningAnalyzer
|
|
24
|
+
from dockerls.application.services.scan_history_store import ScanHistoryStore
|
|
25
|
+
from dockerls.application.services.tag_history_store import TagHistoryStore
|
|
26
|
+
from dockerls.domain.interfaces.eol_checker import EOLCheckerInterface
|
|
27
|
+
from dockerls.domain.interfaces.image_repository import ImageRepositoryInterface
|
|
28
|
+
from dockerls.domain.interfaces.scanner import ScannerInterface
|
|
29
|
+
from dockerls.integrations.exploitdb.client import ExploitDBClient
|
|
30
|
+
from dockerls.integrations.threat_intel.client import ThreatIntelClient
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AnalyzeImageUseCase:
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
repository: ImageRepositoryInterface,
|
|
37
|
+
scanner: ScannerInterface,
|
|
38
|
+
eol_checker: EOLCheckerInterface,
|
|
39
|
+
ignore_path: Path | None = None,
|
|
40
|
+
threat_intel: ThreatIntelClient | None = None,
|
|
41
|
+
hardening: HardeningAnalyzer | None = None,
|
|
42
|
+
exploitdb: ExploitDBClient | None = None,
|
|
43
|
+
tag_history: TagHistoryStore | None = None,
|
|
44
|
+
scan_history: ScanHistoryStore | None = None,
|
|
45
|
+
):
|
|
46
|
+
self._repository = repository
|
|
47
|
+
self._scanner = scanner
|
|
48
|
+
self._eol_checker = eol_checker
|
|
49
|
+
self._ignored_cves = active_ignored_cve_ids(load_ignore_rules(ignore_path))
|
|
50
|
+
self._threat_intel = threat_intel
|
|
51
|
+
self._hardening = hardening
|
|
52
|
+
self._exploitdb = exploitdb
|
|
53
|
+
self._tag_history = tag_history
|
|
54
|
+
self._scan_history = scan_history
|
|
55
|
+
|
|
56
|
+
async def execute(self, image_reference: str) -> ImageAnalysis:
|
|
57
|
+
name, tag = self._parse_reference(image_reference)
|
|
58
|
+
image = await self._repository.get_image_metadata(name, tag)
|
|
59
|
+
if not image:
|
|
60
|
+
image = DockerImage(name=name, tag=tag)
|
|
61
|
+
if "@" in image_reference:
|
|
62
|
+
# Um digest é a identidade exata de um conjunto de bytes, e
|
|
63
|
+
# `name:tag` não o reproduz: reconstruir mandaria o scanner
|
|
64
|
+
# medir `node:latest` no lugar do digest que foi pedido --
|
|
65
|
+
# outra imagem, apresentada com o nome desta.
|
|
66
|
+
image.full_reference = image_reference
|
|
67
|
+
|
|
68
|
+
scan = await self._scanner.scan(image.full_reference)
|
|
69
|
+
if self._ignored_cves:
|
|
70
|
+
filtered = [
|
|
71
|
+
v for v in scan.vulnerabilities if v.cve_id.upper() not in self._ignored_cves
|
|
72
|
+
]
|
|
73
|
+
if len(filtered) != len(scan.vulnerabilities):
|
|
74
|
+
scan = scan.model_copy(update={"vulnerabilities": filtered})
|
|
75
|
+
if self._threat_intel is not None:
|
|
76
|
+
scan = await _enrich_with_threat_intel(scan, self._threat_intel, self._exploitdb)
|
|
77
|
+
|
|
78
|
+
product = name.split("/")[-1]
|
|
79
|
+
match = re.match(r"^\d+(?:\.\d+){0,3}", tag)
|
|
80
|
+
version = match.group(0) if match else ""
|
|
81
|
+
|
|
82
|
+
eol_status = await _eol_status(self._eol_checker, product, version)
|
|
83
|
+
is_eol = eol_status.is_true
|
|
84
|
+
is_lts = await self._eol_checker.is_lts(product, version)
|
|
85
|
+
|
|
86
|
+
# `SecurityScore` requires a completed scan and raises on anything
|
|
87
|
+
# else. A failed scan is not scored at all here: the tier falls back
|
|
88
|
+
# to F (0.0), and `finalize_verdict` below reads `scan.is_verified`
|
|
89
|
+
# to keep that from ever being reported as a verdict rather than a
|
|
90
|
+
# measurement failure -- the same gate `recommend` applies before it
|
|
91
|
+
# will construct a `SecurityScore` in the first place.
|
|
92
|
+
security_score = 0.0
|
|
93
|
+
if scan.is_verified:
|
|
94
|
+
security_score = SecurityScore(image, scan, is_eol=is_eol, is_lts=is_lts).value
|
|
95
|
+
tier = SecurityTier(scan, security_score, is_eol=is_eol)
|
|
96
|
+
rem_score = RemediationScore(scan)
|
|
97
|
+
|
|
98
|
+
analysis = ImageAnalysis(
|
|
99
|
+
image=image,
|
|
100
|
+
scan=scan,
|
|
101
|
+
security_score=security_score,
|
|
102
|
+
tier=tier.tier.value,
|
|
103
|
+
remediation_score=rem_score.value,
|
|
104
|
+
is_eol=is_eol,
|
|
105
|
+
eol_status=eol_status,
|
|
106
|
+
is_lts=is_lts,
|
|
107
|
+
evidence_paths={scan.scanner: scan.evidence_path} if scan.evidence_path else {},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# The same evidence gathering `recommend` does for its finalists.
|
|
111
|
+
# Without it, `analyze` and `alternatives` would report the image
|
|
112
|
+
# they were asked about with every hardening fact unknown, while
|
|
113
|
+
# the candidates they are compared against carry measurements --
|
|
114
|
+
# and a comparison between a measurement and a blank is not one.
|
|
115
|
+
if self._hardening is not None:
|
|
116
|
+
digest, facts = await self._hardening.analyze(image, scan)
|
|
117
|
+
if digest and not image.digest:
|
|
118
|
+
image.digest = digest
|
|
119
|
+
apply_facts(analysis, facts)
|
|
120
|
+
|
|
121
|
+
# A digest is the identity of a fixed set of bytes; `name@sha256:...`
|
|
122
|
+
# was asked for exactly that byte set and has no *tag* to track a
|
|
123
|
+
# move for. Only a mutable `name:tag` reference has history worth
|
|
124
|
+
# keeping -- the same distinction `base` already draws for
|
|
125
|
+
# Dockerfile-pinned bases (`tag_history.py`).
|
|
126
|
+
if self._tag_history is not None and image.digest_known and "@" not in image_reference:
|
|
127
|
+
history = await self._tag_history.observe(f"{name}:{tag}", image.digest)
|
|
128
|
+
if history.moves:
|
|
129
|
+
analysis.tag_drift_note = history.explain()
|
|
130
|
+
|
|
131
|
+
# Unlike tag history, this is worth keeping for a digest reference
|
|
132
|
+
# too: a scanner's database learning about a new CVE can change the
|
|
133
|
+
# count for the exact same, unmoving digest between two runs.
|
|
134
|
+
if self._scan_history is not None and scan.is_verified and image.digest_known:
|
|
135
|
+
before = await self._scan_history.get(image.full_reference)
|
|
136
|
+
after = await self._scan_history.observe(
|
|
137
|
+
image.full_reference,
|
|
138
|
+
digest=image.digest,
|
|
139
|
+
critical=scan.critical_count,
|
|
140
|
+
high=scan.high_count,
|
|
141
|
+
medium=scan.medium_count,
|
|
142
|
+
low=scan.low_count,
|
|
143
|
+
total=scan.total_count,
|
|
144
|
+
)
|
|
145
|
+
if len(after.observations) > 1 and after.latest != before.latest:
|
|
146
|
+
analysis.vuln_trend_note = after.explain()
|
|
147
|
+
|
|
148
|
+
finalize_verdict(analysis, cross_validated=False)
|
|
149
|
+
return analysis
|
|
150
|
+
|
|
151
|
+
async def close(self) -> None:
|
|
152
|
+
"""Release the scanner and the repository's connection pool.
|
|
153
|
+
|
|
154
|
+
Not done inside `execute`, because `CompareImagesUseCase` calls it
|
|
155
|
+
once per image: closing there would leave the second comparison
|
|
156
|
+
talking to a client that had already been shut down.
|
|
157
|
+
"""
|
|
158
|
+
await close_quietly(self._scanner, self._hardening, *sources_of(self._repository))
|
|
159
|
+
|
|
160
|
+
def _parse_reference(self, reference: str) -> tuple[str, str]:
|
|
161
|
+
"""Repositório e tag, sem confundir a porta do registry com uma tag.
|
|
162
|
+
|
|
163
|
+
O `rsplit(":", 1)` que morava aqui lia `registry.internal:5000/app`
|
|
164
|
+
como ("registry.internal", "5000/app") e `node@sha256:...` como
|
|
165
|
+
("node@sha256", "..."). O alvo do scan sobrevivia por acidente --
|
|
166
|
+
`full_reference` reconstrói a string original a partir dos dois
|
|
167
|
+
pedaços errados --, mas o produto e a versão não: a consulta de
|
|
168
|
+
EOL/LTS recebia o produto "registry.internal" na versão "5000", e
|
|
169
|
+
`registry_host` perdia a porta. É a mesma regra que `search`,
|
|
170
|
+
`recommend`, `export`, `advisor` e `alternatives` já usam.
|
|
171
|
+
"""
|
|
172
|
+
repository, tag = split_repository_and_tag(reference)
|
|
173
|
+
return repository, tag or "latest"
|