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,284 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from dockerls.domain.entities.image import DockerImage
|
|
9
|
+
from dockerls.domain.interfaces.image_repository import ImageRepositoryInterface
|
|
10
|
+
from dockerls.integrations.registry.oci import OCIRegistryClient, is_runnable_tag
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from dockerls.infrastructure.network.host_guard import HostGuard
|
|
14
|
+
|
|
15
|
+
CHAINGUARD = "Chainguard"
|
|
16
|
+
DISTROLESS = "Distroless"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HardenedRepository(ImageRepositoryInterface):
|
|
20
|
+
"""Base for free, security-hardened image sources exposed over the OCI
|
|
21
|
+
Distribution API.
|
|
22
|
+
|
|
23
|
+
These registries publish tag names only -- no size, no timestamps -- so
|
|
24
|
+
the resulting `DockerImage` carries the minimum the scan pipeline needs
|
|
25
|
+
and leaves the rest unset rather than inventing values.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
source: str = ""
|
|
29
|
+
host: str = ""
|
|
30
|
+
namespace: str = ""
|
|
31
|
+
# Query name -> repository names, most current first.
|
|
32
|
+
#
|
|
33
|
+
# This was a 1:1 alias map, and 1:1 was the bug. Distroless keeps its
|
|
34
|
+
# legacy repositories published: `distroless/nodejs` still lists tags
|
|
35
|
+
# `10`, `12` and `14`, and `distroless/java` still lists `11`. Mapping
|
|
36
|
+
# `node` onto that single repository meant this tool answered a request
|
|
37
|
+
# for a hardened alternative with a Node runtime that reached end of life
|
|
38
|
+
# years ago -- an insecure image carrying the tool's own recommendation.
|
|
39
|
+
# The current runtimes live in separate, version-named repositories
|
|
40
|
+
# (`nodejs22-debian12`), which no 1:1 alias could reach.
|
|
41
|
+
#
|
|
42
|
+
# A tuple also fixes the second half: an ecosystem legitimately has more
|
|
43
|
+
# than one hardened image (`jdk` and `jre`), and picking one for the
|
|
44
|
+
# reader is a decision that belongs to whoever knows whether the
|
|
45
|
+
# application compiles at runtime.
|
|
46
|
+
repositories: dict[str, tuple[str, ...]] = {}
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
timeout: int = 30,
|
|
51
|
+
guard: HostGuard | None = None,
|
|
52
|
+
*,
|
|
53
|
+
username: str = "",
|
|
54
|
+
password: str = "",
|
|
55
|
+
):
|
|
56
|
+
# `self.host` is a constant of this class, but the hops that follow
|
|
57
|
+
# it -- redirects and the token realm -- are chosen by the far end,
|
|
58
|
+
# so the guard travels with the client here for the same reason it
|
|
59
|
+
# does for a user-supplied registry.
|
|
60
|
+
#
|
|
61
|
+
# `username`/`password` are empty for Chainguard and Distroless --
|
|
62
|
+
# both are anonymous, free-tier catalogues -- and only meaningful
|
|
63
|
+
# for a subclass fronting a registry that actually requires them,
|
|
64
|
+
# like `PrivateRegistryRepository`.
|
|
65
|
+
self._client = OCIRegistryClient(
|
|
66
|
+
self.host, timeout=timeout, guard=guard, username=username, password=password
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def repositories_for(self, image_name: str) -> list[str]:
|
|
70
|
+
"""Every repository of this source that could answer the query.
|
|
71
|
+
|
|
72
|
+
Returns an empty list for references that already name a different
|
|
73
|
+
registry, so `dockerls recommend ghcr.io/org/app` never fans out to
|
|
74
|
+
Chainguard.
|
|
75
|
+
"""
|
|
76
|
+
name = image_name.strip().strip("/").lower()
|
|
77
|
+
if not name or "/" in name or ":" in name:
|
|
78
|
+
return []
|
|
79
|
+
candidates = self.repositories.get(name, (name,))
|
|
80
|
+
return [f"{self.namespace}/{candidate}" for candidate in candidates]
|
|
81
|
+
|
|
82
|
+
def repository_for(self, image_name: str) -> str | None:
|
|
83
|
+
"""The single most current repository for the query, or None.
|
|
84
|
+
|
|
85
|
+
Kept for the callers that identify a source by one path; discovery
|
|
86
|
+
uses `repositories_for`, which is what reaches the version-named
|
|
87
|
+
repositories the legacy alias could not.
|
|
88
|
+
"""
|
|
89
|
+
candidates = self.repositories_for(image_name)
|
|
90
|
+
return candidates[0] if candidates else None
|
|
91
|
+
|
|
92
|
+
def _full_name(self, repository: str) -> str:
|
|
93
|
+
return f"{self.host}/{repository}"
|
|
94
|
+
|
|
95
|
+
def _build_image(self, repository: str, tag: str, payload: dict[str, Any]) -> DockerImage:
|
|
96
|
+
return DockerImage(
|
|
97
|
+
name=self._full_name(repository),
|
|
98
|
+
tag=tag,
|
|
99
|
+
source=self.source,
|
|
100
|
+
is_official=True,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def _runnable_tags(self, payload: dict[str, Any]) -> list[str]:
|
|
104
|
+
return [t for t in (payload.get("tags") or []) if is_runnable_tag(t)]
|
|
105
|
+
|
|
106
|
+
async def search_tags(self, image_name: str, limit: int = 100) -> list[DockerImage]:
|
|
107
|
+
"""Every runnable tag this source offers for the query.
|
|
108
|
+
|
|
109
|
+
Each candidate repository is listed in order and the results are
|
|
110
|
+
concatenated, so the current runtime (`nodejs22-debian12`) is offered
|
|
111
|
+
ahead of the legacy one and a source that publishes both a JDK and a
|
|
112
|
+
JRE offers both instead of silently choosing.
|
|
113
|
+
|
|
114
|
+
A repository that does not exist is not an error: the candidate lists
|
|
115
|
+
below are per-source guesses about a catalogue that changes on its
|
|
116
|
+
own schedule, and a 404 on one of them must not lose the others.
|
|
117
|
+
"""
|
|
118
|
+
images: list[DockerImage] = []
|
|
119
|
+
for repository in self.repositories_for(image_name):
|
|
120
|
+
payload = await self._client.list_tags(repository)
|
|
121
|
+
if payload is None:
|
|
122
|
+
continue
|
|
123
|
+
# Build first, then rank: a source that dates its tags (GCR) must
|
|
124
|
+
# be ordered newest-first, or a lexical sort surfaces nodejs:10
|
|
125
|
+
# ahead of nodejs:22 and the tool recommends a years-old runtime.
|
|
126
|
+
found = [
|
|
127
|
+
self._build_image(repository, tag, payload) for tag in self._runnable_tags(payload)
|
|
128
|
+
]
|
|
129
|
+
found.sort(key=_image_rank)
|
|
130
|
+
images.extend(found)
|
|
131
|
+
if len(images) >= limit:
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
selected = images[:limit]
|
|
135
|
+
logger.info(f"{self.source}: {len(selected)} usable tags for {image_name}")
|
|
136
|
+
return selected
|
|
137
|
+
|
|
138
|
+
async def get_image_metadata(self, image_name: str, tag: str) -> DockerImage | None:
|
|
139
|
+
repository = self.repository_for(image_name)
|
|
140
|
+
if repository is None:
|
|
141
|
+
return None
|
|
142
|
+
payload = await self._client.list_tags(repository)
|
|
143
|
+
if payload is None or tag not in (payload.get("tags") or []):
|
|
144
|
+
return None
|
|
145
|
+
return self._build_image(repository, tag, payload)
|
|
146
|
+
|
|
147
|
+
async def tag_exists(self, image_name: str, tag: str) -> bool | None:
|
|
148
|
+
"""A tag returned by a live listing is confirmed by construction.
|
|
149
|
+
|
|
150
|
+
The listing is memoised by `OCIRegistryClient`, so verifying ten
|
|
151
|
+
candidates against this source costs the one request discovery
|
|
152
|
+
already made rather than ten more.
|
|
153
|
+
"""
|
|
154
|
+
repository = self.repository_for(image_name)
|
|
155
|
+
if repository is None:
|
|
156
|
+
return None
|
|
157
|
+
payload = await self._client.list_tags(repository)
|
|
158
|
+
if payload is None:
|
|
159
|
+
return None
|
|
160
|
+
return tag in (payload.get("tags") or [])
|
|
161
|
+
|
|
162
|
+
async def close(self) -> None:
|
|
163
|
+
"""Release the shared HTTP connection pool."""
|
|
164
|
+
await self._client.close()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
_PREFERRED_TAGS = ("latest", "latest-dev", "nonroot", "debug", "static")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _image_rank(image: DockerImage) -> tuple[int, str, float, str]:
|
|
171
|
+
"""Order: conventional entrypoints first, then newest published, then
|
|
172
|
+
name. Undated tags fall back to name ordering rather than pretending to
|
|
173
|
+
be either new or old."""
|
|
174
|
+
if image.tag in _PREFERRED_TAGS:
|
|
175
|
+
return (0, f"{_PREFERRED_TAGS.index(image.tag):02d}", 0.0, image.tag)
|
|
176
|
+
published = -image.last_updated.timestamp() if image.last_updated else 0.0
|
|
177
|
+
return (1, "", published, image.tag)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class ChainguardRepository(HardenedRepository):
|
|
181
|
+
"""Chainguard's free tier (cgr.dev/chainguard/<image>).
|
|
182
|
+
|
|
183
|
+
The free catalogue tracks only the moving tags -- `latest`,
|
|
184
|
+
`latest-dev` and friends; pinned version tags are a paid feature -- so a
|
|
185
|
+
handful of results here is the expected outcome, not a failure.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
source = CHAINGUARD
|
|
189
|
+
host = "cgr.dev"
|
|
190
|
+
namespace = "chainguard"
|
|
191
|
+
#: Verificado contra o registry em 2026-08-18: `chainguard/java` não
|
|
192
|
+
#: existe (responde 403 no token endpoint), então quem digitava `java`
|
|
193
|
+
#: recebia zero alternativas. As imagens reais são `jdk` e `jre`, e as
|
|
194
|
+
#: duas são oferecidas porque a escolha depende de a aplicação compilar
|
|
195
|
+
#: em runtime -- não é decisão de quem escaneia.
|
|
196
|
+
repositories = {
|
|
197
|
+
"node": ("node",),
|
|
198
|
+
"nodejs": ("node",),
|
|
199
|
+
"python": ("python",),
|
|
200
|
+
"python3": ("python",),
|
|
201
|
+
"go": ("go",),
|
|
202
|
+
"golang": ("go",),
|
|
203
|
+
"java": ("jdk", "jre"),
|
|
204
|
+
"openjdk": ("jdk", "jre"),
|
|
205
|
+
"temurin": ("jdk", "jre"),
|
|
206
|
+
"corretto": ("jdk", "jre"),
|
|
207
|
+
"jdk": ("jdk",),
|
|
208
|
+
"jre": ("jre",),
|
|
209
|
+
"maven": ("maven",),
|
|
210
|
+
"gradle": ("gradle",),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class DistrolessRepository(HardenedRepository):
|
|
215
|
+
"""Google's Distroless images (gcr.io/distroless/<image>)."""
|
|
216
|
+
|
|
217
|
+
source = DISTROLESS
|
|
218
|
+
host = "gcr.io"
|
|
219
|
+
namespace = "distroless"
|
|
220
|
+
#: Verificado contra o registry em 2026-08-18. Os repositórios sem sufixo
|
|
221
|
+
#: de versão continuam publicados e **são legado**: `distroless/nodejs`
|
|
222
|
+
#: lista `10`, `12` e `14`, e `distroless/java` lista `11`. Eles ficam de
|
|
223
|
+
#: fora: recomendar um runtime que morreu anos atrás, com o carimbo desta
|
|
224
|
+
#: ferramenta, é pior do que não recomendar nada. Os nomes versionados
|
|
225
|
+
#: também respondem à outra metade do problema -- `nodejs22-debian12` diz
|
|
226
|
+
#: o que é; `nodejs` não dizia.
|
|
227
|
+
repositories = {
|
|
228
|
+
"node": ("nodejs22-debian12", "nodejs20-debian12"),
|
|
229
|
+
"nodejs": ("nodejs22-debian12", "nodejs20-debian12"),
|
|
230
|
+
"python": ("python3-debian12",),
|
|
231
|
+
"python3": ("python3-debian12",),
|
|
232
|
+
# Go compila estático: a imagem de execução não carrega runtime algum.
|
|
233
|
+
"go": ("static-debian12", "base-debian12"),
|
|
234
|
+
"golang": ("static-debian12", "base-debian12"),
|
|
235
|
+
"java": ("java21-debian12", "java17-debian12"),
|
|
236
|
+
"openjdk": ("java21-debian12", "java17-debian12"),
|
|
237
|
+
"temurin": ("java21-debian12", "java17-debian12"),
|
|
238
|
+
"corretto": ("java21-debian12", "java17-debian12"),
|
|
239
|
+
"jdk": ("java21-debian12", "java17-debian12"),
|
|
240
|
+
# O Distroless não publica ferramenta de build: maven e gradle rodam
|
|
241
|
+
# no stage de build, não no de execução. Declarado como ausência em
|
|
242
|
+
# vez de mapeado para algo parecido.
|
|
243
|
+
"maven": (),
|
|
244
|
+
"gradle": (),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
def _build_image(self, repository: str, tag: str, payload: dict[str, Any]) -> DockerImage:
|
|
248
|
+
image = super()._build_image(repository, tag, payload)
|
|
249
|
+
# GCR uniquely returns a manifest map with upload timestamps and
|
|
250
|
+
# sizes, so distroless images can be dated instead of guessed at.
|
|
251
|
+
meta = _gcr_manifest_for_tag(payload, tag)
|
|
252
|
+
if not meta:
|
|
253
|
+
return image
|
|
254
|
+
dated: DockerImage = image.model_copy(update=meta)
|
|
255
|
+
return dated
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _gcr_manifest_for_tag(payload: dict[str, Any], tag: str) -> dict[str, Any]:
|
|
259
|
+
manifest = payload.get("manifest")
|
|
260
|
+
if not isinstance(manifest, dict):
|
|
261
|
+
return {}
|
|
262
|
+
for digest, entry in manifest.items():
|
|
263
|
+
if not isinstance(entry, dict) or tag not in (entry.get("tag") or []):
|
|
264
|
+
continue
|
|
265
|
+
update: dict[str, Any] = {"digest": digest}
|
|
266
|
+
size = entry.get("imageSizeBytes")
|
|
267
|
+
if size is not None:
|
|
268
|
+
with_int = _safe_int(size)
|
|
269
|
+
if with_int is not None:
|
|
270
|
+
update["size_bytes"] = with_int
|
|
271
|
+
uploaded = _safe_int(entry.get("timeUploadedMs"))
|
|
272
|
+
# GCR reports a sentinel far-past timeCreatedMs for many images;
|
|
273
|
+
# timeUploadedMs is the field that reflects reality.
|
|
274
|
+
if uploaded and uploaded > 0:
|
|
275
|
+
update["last_updated"] = datetime.fromtimestamp(uploaded / 1000, tz=UTC)
|
|
276
|
+
return update
|
|
277
|
+
return {}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _safe_int(value: Any) -> int | None:
|
|
281
|
+
try:
|
|
282
|
+
return int(value)
|
|
283
|
+
except (TypeError, ValueError):
|
|
284
|
+
return None
|