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,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