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,600 @@
|
|
|
1
|
+
"""Montar uma imagem base a partir de escolhas, e dizer o que cada uma custa.
|
|
2
|
+
|
|
3
|
+
Uma imagem base é o piso de tudo que vem depois: cada pacote instalado aqui
|
|
4
|
+
existe em toda aplicação que a consome, e toda CVE dele vira trabalho de
|
|
5
|
+
triagem para times que nem sabem que ele está lá. É por isso que a escolha
|
|
6
|
+
merece uma tela em vez de um Dockerfile copiado de outro projeto -- e é por
|
|
7
|
+
isso que cada item deste catálogo carrega **para que serve** e **o que custa**,
|
|
8
|
+
lado a lado.
|
|
9
|
+
|
|
10
|
+
O catálogo não é uma lista de pacotes disponíveis; é a lista curta do que
|
|
11
|
+
aparece de verdade numa imagem base de produção. Oferecer tudo que existe no
|
|
12
|
+
repositório da distribuição transformaria a escolha em paralisia e faria as
|
|
13
|
+
pessoas marcarem tudo "por via das dúvidas", que é exatamente o resultado que
|
|
14
|
+
uma imagem base não pode ter.
|
|
15
|
+
|
|
16
|
+
Três recusas estão codificadas aqui, e todas vêm da mesma ideia -- conveniência
|
|
17
|
+
que se paga em superfície de ataque não é conveniência:
|
|
18
|
+
|
|
19
|
+
* **distroless não instala nada.** Não há gerenciador de pacotes nem shell na
|
|
20
|
+
imagem; pedir pacotes ali é um mal-entendido sobre o que distroless é, e a
|
|
21
|
+
resposta certa é explicar isso em vez de gerar um Dockerfile que falha.
|
|
22
|
+
* **`sudo` não está no catálogo.** Numa imagem que já roda sem privilégio, ele
|
|
23
|
+
existe para cruzar a fronteira que a imagem acabou de estabelecer.
|
|
24
|
+
* **o cache do gerenciador sai na mesma camada que o criou**, sempre, sem ser
|
|
25
|
+
uma opção. Removê-lo depois deixa os bytes na camada anterior e a imagem
|
|
26
|
+
carrega o peso e a superfície mesmo parecendo não carregar.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from dataclasses import dataclass, field, replace
|
|
32
|
+
from enum import StrEnum
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class OsFamily(StrEnum):
|
|
36
|
+
"""A distribuição base. Decide libc, gerenciador de pacotes e nomes."""
|
|
37
|
+
|
|
38
|
+
ALPINE = "alpine"
|
|
39
|
+
DEBIAN = "debian"
|
|
40
|
+
UBUNTU = "ubuntu"
|
|
41
|
+
DISTROLESS = "distroless"
|
|
42
|
+
#: Wolfi/Chainguard. Modelado como o Distroless: uma imagem pronta,
|
|
43
|
+
#: mínima, sem gerenciador de pacotes exposto neste menu -- não porque
|
|
44
|
+
#: o Wolfi careça de `apk` (ele tem, é um fork do apk-tools), mas
|
|
45
|
+
#: porque o catálogo gratuito da Chainguard não publica tag de versão
|
|
46
|
+
#: pinada nem imagem "builder + apk add" para customizar, só as tags
|
|
47
|
+
#: móveis (`latest`, `latest-dev`) que `hardened.py` já consome em
|
|
48
|
+
#: `recommend`. Tratar como apk-instalável aqui geraria um `apk add`
|
|
49
|
+
#: contra um índice que este projeto nunca verificou responder do
|
|
50
|
+
#: jeito que o Alpine responde.
|
|
51
|
+
WOLFI = "wolfi"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def uses_apk(self) -> bool:
|
|
55
|
+
return self is OsFamily.ALPINE
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def installs_packages(self) -> bool:
|
|
59
|
+
"""Distroless e Wolfi não oferecem pacote neste menu -- o primeiro
|
|
60
|
+
porque não tem gerenciador nenhum, o segundo porque só a tag móvel
|
|
61
|
+
pronta é servida pelo tier gratuito, sem uma imagem "builder" para
|
|
62
|
+
customizar sobre ela."""
|
|
63
|
+
return self not in (OsFamily.DISTROLESS, OsFamily.WOLFI)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def libc(self) -> str:
|
|
67
|
+
return "musl" if self is OsFamily.ALPINE else "glibc"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Runtime(StrEnum):
|
|
71
|
+
"""O runtime de linguagem que a base carrega, se carregar algum."""
|
|
72
|
+
|
|
73
|
+
NONE = "none"
|
|
74
|
+
JAVA = "java"
|
|
75
|
+
NODE = "node"
|
|
76
|
+
PYTHON = "python"
|
|
77
|
+
GO = "go"
|
|
78
|
+
RUBY = "ruby"
|
|
79
|
+
PHP = "php"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class RuntimeBase:
|
|
84
|
+
"""A imagem oficial que serve de ponto de partida para um runtime."""
|
|
85
|
+
|
|
86
|
+
image: str
|
|
87
|
+
tag: str
|
|
88
|
+
#: Caminhos de um gerenciador de pacotes que a imagem oficial embute e que
|
|
89
|
+
#: uma base de *runtime* não precisa. O npm é o caso que motivou isto: ele
|
|
90
|
+
#: vem com a própria árvore de dependências em node_modules, fora do
|
|
91
|
+
#: alcance do apk, e é de onde saem quase todas as CVEs de uma imagem
|
|
92
|
+
#: `node:*-alpine` recém-construída.
|
|
93
|
+
bundled_manager: tuple[str, ...] = ()
|
|
94
|
+
bundled_manager_note: str = ""
|
|
95
|
+
#: Usuário não-root que a imagem oficial já traz, quando traz. Criar outro
|
|
96
|
+
#: por cima seria duplicar o que existe e confundir quem consome.
|
|
97
|
+
builtin_user: str = ""
|
|
98
|
+
note: str = ""
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def reference(self) -> str:
|
|
102
|
+
return f"{self.image}:{self.tag}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
#: (runtime, família) -> imagem base. Só combinações que existem de verdade:
|
|
106
|
+
#: oferecer `go` sobre distroless levaria a `gcr.io/distroless/static`, que não
|
|
107
|
+
#: tem runtime nenhum e é uma resposta diferente da que a pessoa pediu.
|
|
108
|
+
RUNTIME_BASES: dict[tuple[Runtime, OsFamily], RuntimeBase] = {
|
|
109
|
+
(Runtime.NONE, OsFamily.ALPINE): RuntimeBase("alpine", "3.21"),
|
|
110
|
+
(Runtime.NONE, OsFamily.DEBIAN): RuntimeBase("debian", "12-slim"),
|
|
111
|
+
(Runtime.NONE, OsFamily.UBUNTU): RuntimeBase("ubuntu", "24.04"),
|
|
112
|
+
(Runtime.NONE, OsFamily.DISTROLESS): RuntimeBase(
|
|
113
|
+
"gcr.io/distroless/base-debian12", "nonroot", builtin_user="nonroot"
|
|
114
|
+
),
|
|
115
|
+
# `builtin_user="nonroot"`: convenção pública e estável das imagens
|
|
116
|
+
# Chainguard (a mesma que `gcr.io/distroless/*:nonroot` usa), não uma
|
|
117
|
+
# medição feita a partir deste ambiente -- este projeto não tem como
|
|
118
|
+
# confirmar contra `cgr.dev` daqui.
|
|
119
|
+
(Runtime.NONE, OsFamily.WOLFI): RuntimeBase(
|
|
120
|
+
"cgr.dev/chainguard/wolfi-base", "latest", builtin_user="nonroot"
|
|
121
|
+
),
|
|
122
|
+
(Runtime.JAVA, OsFamily.ALPINE): RuntimeBase(
|
|
123
|
+
"eclipse-temurin",
|
|
124
|
+
"21-jre-alpine",
|
|
125
|
+
note="JRE, not JDK: a compiler and build tooling are not needed to run a jar",
|
|
126
|
+
),
|
|
127
|
+
(Runtime.JAVA, OsFamily.DEBIAN): RuntimeBase("eclipse-temurin", "21-jre"),
|
|
128
|
+
(Runtime.JAVA, OsFamily.UBUNTU): RuntimeBase("eclipse-temurin", "21-jre-noble"),
|
|
129
|
+
(Runtime.JAVA, OsFamily.DISTROLESS): RuntimeBase(
|
|
130
|
+
"gcr.io/distroless/java21-debian12", "nonroot", builtin_user="nonroot"
|
|
131
|
+
),
|
|
132
|
+
(Runtime.JAVA, OsFamily.WOLFI): RuntimeBase(
|
|
133
|
+
"cgr.dev/chainguard/jre", "latest", builtin_user="nonroot"
|
|
134
|
+
),
|
|
135
|
+
(Runtime.NODE, OsFamily.ALPINE): RuntimeBase(
|
|
136
|
+
"node",
|
|
137
|
+
"22-alpine",
|
|
138
|
+
builtin_user="node",
|
|
139
|
+
note="the official image already ships the `node` user",
|
|
140
|
+
bundled_manager=(
|
|
141
|
+
"/usr/local/lib/node_modules/npm",
|
|
142
|
+
"/usr/local/bin/npm",
|
|
143
|
+
"/usr/local/bin/npx",
|
|
144
|
+
"/opt/yarn-*",
|
|
145
|
+
"/usr/local/bin/yarn",
|
|
146
|
+
"/usr/local/bin/yarnpkg",
|
|
147
|
+
),
|
|
148
|
+
bundled_manager_note="npm and yarn",
|
|
149
|
+
),
|
|
150
|
+
(Runtime.NODE, OsFamily.DEBIAN): RuntimeBase(
|
|
151
|
+
"node",
|
|
152
|
+
"22-slim",
|
|
153
|
+
builtin_user="node",
|
|
154
|
+
bundled_manager=(
|
|
155
|
+
"/usr/local/lib/node_modules/npm",
|
|
156
|
+
"/usr/local/bin/npm",
|
|
157
|
+
"/usr/local/bin/npx",
|
|
158
|
+
"/opt/yarn-*",
|
|
159
|
+
"/usr/local/bin/yarn",
|
|
160
|
+
"/usr/local/bin/yarnpkg",
|
|
161
|
+
),
|
|
162
|
+
bundled_manager_note="npm and yarn",
|
|
163
|
+
),
|
|
164
|
+
(Runtime.NODE, OsFamily.DISTROLESS): RuntimeBase(
|
|
165
|
+
"gcr.io/distroless/nodejs22-debian12", "nonroot", builtin_user="nonroot"
|
|
166
|
+
),
|
|
167
|
+
(Runtime.NODE, OsFamily.WOLFI): RuntimeBase(
|
|
168
|
+
"cgr.dev/chainguard/node", "latest", builtin_user="nonroot"
|
|
169
|
+
),
|
|
170
|
+
(Runtime.PYTHON, OsFamily.ALPINE): RuntimeBase(
|
|
171
|
+
"python",
|
|
172
|
+
"3.12-alpine",
|
|
173
|
+
note="musl: wheels must be musllinux or the package compiles at build time",
|
|
174
|
+
),
|
|
175
|
+
(Runtime.PYTHON, OsFamily.DEBIAN): RuntimeBase("python", "3.12-slim-bookworm"),
|
|
176
|
+
(Runtime.PYTHON, OsFamily.DISTROLESS): RuntimeBase(
|
|
177
|
+
"gcr.io/distroless/python3-debian12", "nonroot", builtin_user="nonroot"
|
|
178
|
+
),
|
|
179
|
+
(Runtime.PYTHON, OsFamily.WOLFI): RuntimeBase(
|
|
180
|
+
"cgr.dev/chainguard/python", "latest", builtin_user="nonroot"
|
|
181
|
+
),
|
|
182
|
+
(Runtime.GO, OsFamily.ALPINE): RuntimeBase("golang", "1.23-alpine"),
|
|
183
|
+
(Runtime.GO, OsFamily.DEBIAN): RuntimeBase("golang", "1.23-bookworm"),
|
|
184
|
+
# Sem Ubuntu nem distroless para Ruby/PHP, pelo mesmo motivo que Node,
|
|
185
|
+
# Python e Go também não têm: nenhum dos dois publica uma imagem
|
|
186
|
+
# `ruby:*-ubuntu`/`php:*-ubuntu` oficial (o template `*-ubuntu` instala
|
|
187
|
+
# via apt sobre Ubuntu puro, um padrão multi-stage que este gerador de
|
|
188
|
+
# base única não modela), e o catálogo distroless do Google não publica
|
|
189
|
+
# runtime para nenhum dos dois.
|
|
190
|
+
(Runtime.RUBY, OsFamily.ALPINE): RuntimeBase("ruby", "3.3-alpine"),
|
|
191
|
+
(Runtime.RUBY, OsFamily.DEBIAN): RuntimeBase("ruby", "3.3-slim-bookworm"),
|
|
192
|
+
(Runtime.PHP, OsFamily.ALPINE): RuntimeBase("php", "8.3-cli-alpine"),
|
|
193
|
+
(Runtime.PHP, OsFamily.DEBIAN): RuntimeBase("php", "8.3-cli-bookworm"),
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#: O molde da tag para cada combinação, com a versão de fora -- é o que
|
|
197
|
+
#: permite `--os-version`/`--runtime-version` trocar só o número e manter o
|
|
198
|
+
#: sufixo de família (`-alpine`, `-slim`, `-jre-noble`...) que `RUNTIME_BASES`
|
|
199
|
+
#: já usa. Não é uma segunda fonte de versão: as versões continuam vindo do
|
|
200
|
+
#: registry (`--runtime-version` explícito, ou a descoberta dinâmica), isto
|
|
201
|
+
#: só sabe *onde* o número entra na tag.
|
|
202
|
+
TAG_TEMPLATES: dict[tuple[Runtime, OsFamily], str] = {
|
|
203
|
+
(Runtime.NONE, OsFamily.ALPINE): "{version}",
|
|
204
|
+
(Runtime.NONE, OsFamily.DEBIAN): "{version}-slim",
|
|
205
|
+
(Runtime.NONE, OsFamily.UBUNTU): "{version}",
|
|
206
|
+
(Runtime.JAVA, OsFamily.ALPINE): "{version}-jre-alpine",
|
|
207
|
+
(Runtime.JAVA, OsFamily.DEBIAN): "{version}-jre",
|
|
208
|
+
(Runtime.JAVA, OsFamily.UBUNTU): "{version}-jre-noble",
|
|
209
|
+
(Runtime.NODE, OsFamily.ALPINE): "{version}-alpine",
|
|
210
|
+
(Runtime.NODE, OsFamily.DEBIAN): "{version}-slim",
|
|
211
|
+
(Runtime.PYTHON, OsFamily.ALPINE): "{version}-alpine",
|
|
212
|
+
(Runtime.PYTHON, OsFamily.DEBIAN): "{version}-slim-bookworm",
|
|
213
|
+
(Runtime.GO, OsFamily.ALPINE): "{version}-alpine",
|
|
214
|
+
(Runtime.GO, OsFamily.DEBIAN): "{version}-bookworm",
|
|
215
|
+
(Runtime.RUBY, OsFamily.ALPINE): "{version}-alpine",
|
|
216
|
+
(Runtime.RUBY, OsFamily.DEBIAN): "{version}-slim-bookworm",
|
|
217
|
+
(Runtime.PHP, OsFamily.ALPINE): "{version}-cli-alpine",
|
|
218
|
+
(Runtime.PHP, OsFamily.DEBIAN): "{version}-cli-bookworm",
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def with_version(runtime: Runtime, family: OsFamily, version: str) -> RuntimeBase:
|
|
223
|
+
"""O `RuntimeBase` padrão desta combinação, com a tag trocada para
|
|
224
|
+
`version`.
|
|
225
|
+
|
|
226
|
+
Levanta a mesma `UnsupportedCombinationError` da combinação em si
|
|
227
|
+
quando ela não existe, e diz que a família não tem molde de tag quando
|
|
228
|
+
a combinação existe mas ninguém ensinou o formato (hoje, distroless --
|
|
229
|
+
ele não versiona por número solto, versiona pelo runtime que embute).
|
|
230
|
+
"""
|
|
231
|
+
key = (runtime, family)
|
|
232
|
+
if key not in RUNTIME_BASES:
|
|
233
|
+
raise UnsupportedCombinationError(f"no base image is published for {runtime} on {family}")
|
|
234
|
+
template = TAG_TEMPLATES.get(key)
|
|
235
|
+
if template is None:
|
|
236
|
+
raise UnsupportedCombinationError(
|
|
237
|
+
f"{family} does not take an explicit version override for {runtime}"
|
|
238
|
+
)
|
|
239
|
+
base = RUNTIME_BASES[key]
|
|
240
|
+
return replace(base, tag=template.format(version=version))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass(frozen=True)
|
|
244
|
+
class PackageChoice:
|
|
245
|
+
"""Um pacote oferecido no menu, com o que ganha e o que custa."""
|
|
246
|
+
|
|
247
|
+
key: str
|
|
248
|
+
purpose: str
|
|
249
|
+
#: O preço em superfície de ataque, dito na hora da escolha e não depois.
|
|
250
|
+
cost: str
|
|
251
|
+
#: Nome no apk (Alpine) e no apt (Debian/Ubuntu). Divergem com frequência.
|
|
252
|
+
apk: str = ""
|
|
253
|
+
apt: str = ""
|
|
254
|
+
#: Já presente na maioria das bases; marcar não faz mal, mas não faz nada.
|
|
255
|
+
usually_present: bool = False
|
|
256
|
+
|
|
257
|
+
def package_for(self, family: OsFamily) -> str:
|
|
258
|
+
"""O nome nesta família, ou "" quando o pacote não existe nela.
|
|
259
|
+
|
|
260
|
+
O fallback para `key` só vale quando *nenhum* nome específico foi
|
|
261
|
+
declarado. Aplicá-lo a um pacote que existe só numa família --
|
|
262
|
+
`libc6-compat`, que é do Alpine -- geraria um `apt-get install
|
|
263
|
+
libc6-compat` que quebra o build: o vazio aqui significa "não se
|
|
264
|
+
aplica", e é o que faz o menu não oferecê-lo onde não cabe.
|
|
265
|
+
"""
|
|
266
|
+
if not self.apk and not self.apt:
|
|
267
|
+
return self.key
|
|
268
|
+
return self.apk if family.uses_apk else self.apt
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
#: O menu. Curto de propósito: uma lista longa faz as pessoas marcarem tudo
|
|
272
|
+
#: "por via das dúvidas", que é o pior resultado possível numa imagem base.
|
|
273
|
+
PACKAGE_CATALOG: tuple[PackageChoice, ...] = (
|
|
274
|
+
PackageChoice(
|
|
275
|
+
key="ca-certificates",
|
|
276
|
+
purpose="validating TLS when talking to any HTTPS service",
|
|
277
|
+
cost="practically none; without it every TLS connection fails verification",
|
|
278
|
+
apk="ca-certificates",
|
|
279
|
+
apt="ca-certificates",
|
|
280
|
+
usually_present=True,
|
|
281
|
+
),
|
|
282
|
+
PackageChoice(
|
|
283
|
+
key="tzdata",
|
|
284
|
+
purpose="time zones; without it the container stays on UTC and local dates are wrong",
|
|
285
|
+
cost="a few MB of data, no new executable",
|
|
286
|
+
apk="tzdata",
|
|
287
|
+
apt="tzdata",
|
|
288
|
+
),
|
|
289
|
+
PackageChoice(
|
|
290
|
+
key="curl",
|
|
291
|
+
purpose="HTTP HEALTHCHECK and network diagnostics",
|
|
292
|
+
cost="a full HTTP client inside the container -- what an attacker uses to "
|
|
293
|
+
"fetch the second stage",
|
|
294
|
+
apk="curl",
|
|
295
|
+
apt="curl",
|
|
296
|
+
),
|
|
297
|
+
PackageChoice(
|
|
298
|
+
key="wget",
|
|
299
|
+
purpose="an alternative to curl for downloading files",
|
|
300
|
+
cost="the same cost as curl; having both doubles the surface, not the use",
|
|
301
|
+
apk="wget",
|
|
302
|
+
apt="wget",
|
|
303
|
+
),
|
|
304
|
+
PackageChoice(
|
|
305
|
+
key="bash",
|
|
306
|
+
purpose="scripts relying on features the Alpine `sh` does not have",
|
|
307
|
+
cost="a more capable shell is a more useful shell for whoever breaks in",
|
|
308
|
+
apk="bash",
|
|
309
|
+
apt="bash",
|
|
310
|
+
),
|
|
311
|
+
PackageChoice(
|
|
312
|
+
key="git",
|
|
313
|
+
purpose="cloning or inspecting repositories at runtime",
|
|
314
|
+
cost="rarely needed in production, and pulls a large dependency tree; it "
|
|
315
|
+
"almost always belongs in the build stage",
|
|
316
|
+
apk="git",
|
|
317
|
+
apt="git",
|
|
318
|
+
),
|
|
319
|
+
PackageChoice(
|
|
320
|
+
key="jq",
|
|
321
|
+
purpose="parsing JSON in entrypoint scripts",
|
|
322
|
+
cost="small and self-contained",
|
|
323
|
+
apk="jq",
|
|
324
|
+
apt="jq",
|
|
325
|
+
),
|
|
326
|
+
PackageChoice(
|
|
327
|
+
key="openssl",
|
|
328
|
+
purpose="generating certificates or debugging TLS from inside the container",
|
|
329
|
+
cost="the library is already there; this adds the command-line *tool*",
|
|
330
|
+
apk="openssl",
|
|
331
|
+
apt="openssl",
|
|
332
|
+
),
|
|
333
|
+
PackageChoice(
|
|
334
|
+
key="tini",
|
|
335
|
+
purpose="a minimal init that forwards signals and reaps orphaned processes",
|
|
336
|
+
cost="almost nothing, and it fixes the pid 1 that ignores SIGTERM",
|
|
337
|
+
apk="tini",
|
|
338
|
+
apt="tini",
|
|
339
|
+
),
|
|
340
|
+
PackageChoice(
|
|
341
|
+
key="libc6-compat",
|
|
342
|
+
purpose="a glibc compatibility layer on Alpine, for pre-compiled binaries",
|
|
343
|
+
cost="only makes sense on Alpine; a Debian already has real glibc",
|
|
344
|
+
apk="libc6-compat",
|
|
345
|
+
apt="",
|
|
346
|
+
),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
#: Pacotes que este catálogo recusa a oferecer, com o motivo. São recusas, não
|
|
350
|
+
#: omissões: alguém que procurar por eles merece a explicação.
|
|
351
|
+
REFUSED_PACKAGES: dict[str, str] = {
|
|
352
|
+
"sudo": (
|
|
353
|
+
"in an image that already runs unprivileged, `sudo` exists to cross the "
|
|
354
|
+
"boundary it just established -- and it is setuid in order to"
|
|
355
|
+
),
|
|
356
|
+
"su-exec": (
|
|
357
|
+
"switching user at runtime undoes the image `USER`; if the process needs a "
|
|
358
|
+
"different user, declare it in `USER`"
|
|
359
|
+
),
|
|
360
|
+
"docker": (
|
|
361
|
+
"the Docker client inside the container implies access to the daemon "
|
|
362
|
+
"socket, which is equivalent to root on the host"
|
|
363
|
+
),
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class UnsupportedCombinationError(ValueError):
|
|
368
|
+
"""A combinação de runtime e sistema operacional não existe."""
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@dataclass(frozen=True)
|
|
372
|
+
class BaseRecipe:
|
|
373
|
+
"""Tudo que decide o conteúdo do Dockerfile de uma imagem base."""
|
|
374
|
+
|
|
375
|
+
family: OsFamily
|
|
376
|
+
runtime: Runtime = Runtime.NONE
|
|
377
|
+
packages: tuple[str, ...] = ()
|
|
378
|
+
#: Digest resolvido da base. Vazio deixa a tag móvel -- e o gerador diz
|
|
379
|
+
#: isso em voz alta em vez de fingir que está fixado.
|
|
380
|
+
digest: str = ""
|
|
381
|
+
title: str = "base"
|
|
382
|
+
description: str = ""
|
|
383
|
+
owner: str = ""
|
|
384
|
+
source: str = ""
|
|
385
|
+
uid: int = 10001
|
|
386
|
+
user_name: str = "appuser"
|
|
387
|
+
#: Remover o gerenciador de pacotes que a imagem oficial embute. Vale para
|
|
388
|
+
#: uma base de execução: as dependências que o npm carrega dentro de si
|
|
389
|
+
#: respondem por quase toda CVE de uma `node:*-alpine`, e nada delas é
|
|
390
|
+
#: necessário para *rodar* uma aplicação cujas dependências já foram
|
|
391
|
+
#: instaladas no estágio de build.
|
|
392
|
+
strip_bundled_manager: bool = False
|
|
393
|
+
extra: dict[str, str] = field(default_factory=dict)
|
|
394
|
+
#: Substitui o `RuntimeBase` do catálogo por um com outra tag -- de
|
|
395
|
+
#: `--os-version`/`--runtime-version`, resolvida contra o registry pelo
|
|
396
|
+
#: chamador. `None` é o padrão em todo lugar que não pediu isso, e
|
|
397
|
+
#: preserva exatamente o comportamento de sempre: o catálogo decide.
|
|
398
|
+
base_override: RuntimeBase | None = None
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def base(self) -> RuntimeBase:
|
|
402
|
+
if self.base_override is not None:
|
|
403
|
+
return self.base_override
|
|
404
|
+
try:
|
|
405
|
+
return RUNTIME_BASES[(self.runtime, self.family)]
|
|
406
|
+
except KeyError as e:
|
|
407
|
+
raise UnsupportedCombinationError(
|
|
408
|
+
f"no base image is published for {self.runtime} on {self.family}"
|
|
409
|
+
) from e
|
|
410
|
+
|
|
411
|
+
def validate(self) -> None:
|
|
412
|
+
base = self.base # levanta se a combinação não existe
|
|
413
|
+
if self.packages and not self.family.installs_packages:
|
|
414
|
+
raise UnsupportedCombinationError(
|
|
415
|
+
f"{self.family.value} has no package manager exposed here: nothing "
|
|
416
|
+
"can be installed into it this way. Use alpine or debian if you need "
|
|
417
|
+
"packages, or no packages at all if what you want is the smallest "
|
|
418
|
+
"possible surface"
|
|
419
|
+
)
|
|
420
|
+
for package in self.packages:
|
|
421
|
+
if package in REFUSED_PACKAGES:
|
|
422
|
+
raise UnsupportedCombinationError(
|
|
423
|
+
f"{package} is not offered: {REFUSED_PACKAGES[package]}"
|
|
424
|
+
)
|
|
425
|
+
if package not in {choice.key for choice in PACKAGE_CATALOG}:
|
|
426
|
+
raise UnsupportedCombinationError(f"unknown package: {package}")
|
|
427
|
+
if base.builtin_user and self.user_name != base.builtin_user:
|
|
428
|
+
# Não é erro -- só não vale a pena criar um usuário quando a
|
|
429
|
+
# imagem oficial já traz um. O gerador reaproveita o existente.
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def render(recipe: BaseRecipe) -> str:
|
|
434
|
+
"""O Dockerfile da imagem base, pronto para construir."""
|
|
435
|
+
recipe.validate()
|
|
436
|
+
base = recipe.base
|
|
437
|
+
lines: list[str] = ["# syntax=docker/dockerfile:1", ""]
|
|
438
|
+
lines += _header(recipe, base)
|
|
439
|
+
|
|
440
|
+
reference = base.reference
|
|
441
|
+
if recipe.digest:
|
|
442
|
+
lines += [
|
|
443
|
+
f"ARG BASE_DIGEST={recipe.digest}",
|
|
444
|
+
"",
|
|
445
|
+
f"FROM {reference}@${{BASE_DIGEST}}",
|
|
446
|
+
]
|
|
447
|
+
else:
|
|
448
|
+
lines += [
|
|
449
|
+
"# WARNING: base not pinned by digest. What you test and what is built",
|
|
450
|
+
"# tomorrow can be different bytes, with no change of yours.",
|
|
451
|
+
f"FROM {reference}",
|
|
452
|
+
]
|
|
453
|
+
lines.append("")
|
|
454
|
+
lines += _labels(recipe)
|
|
455
|
+
lines.append("")
|
|
456
|
+
|
|
457
|
+
if recipe.family.installs_packages:
|
|
458
|
+
lines += _packages(recipe)
|
|
459
|
+
lines.append("")
|
|
460
|
+
|
|
461
|
+
if recipe.strip_bundled_manager and base.bundled_manager:
|
|
462
|
+
lines += _strip_manager(base)
|
|
463
|
+
lines.append("")
|
|
464
|
+
|
|
465
|
+
user = base.builtin_user or recipe.user_name
|
|
466
|
+
if not base.builtin_user and recipe.family.installs_packages:
|
|
467
|
+
lines += _create_user(recipe)
|
|
468
|
+
lines.append("")
|
|
469
|
+
|
|
470
|
+
lines += [
|
|
471
|
+
"WORKDIR /app",
|
|
472
|
+
f"USER {user}",
|
|
473
|
+
"",
|
|
474
|
+
"# No ENTRYPOINT, EXPOSE or HEALTHCHECK: a base image does not know which",
|
|
475
|
+
"# port the application listens on, nor what 'healthy' means for it.",
|
|
476
|
+
"# Declaring them here would be inherited wrongly by every consumer.",
|
|
477
|
+
]
|
|
478
|
+
return "\n".join(lines) + "\n"
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _header(recipe: BaseRecipe, base: RuntimeBase) -> list[str]:
|
|
482
|
+
lines = [
|
|
483
|
+
f"# BASE image generated by DockerLs: {recipe.family}"
|
|
484
|
+
+ (f" + {recipe.runtime}" if recipe.runtime is not Runtime.NONE else ""),
|
|
485
|
+
"#",
|
|
486
|
+
"# Contains no application. Consumers do `FROM this-image` and bring",
|
|
487
|
+
"# their own artifact.",
|
|
488
|
+
]
|
|
489
|
+
if base.note:
|
|
490
|
+
lines += ["#", f"# {base.note}."]
|
|
491
|
+
lines += ["#", f"# libc: {recipe.family.libc}.", ""]
|
|
492
|
+
return lines
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _labels(recipe: BaseRecipe) -> list[str]:
|
|
496
|
+
labels = {
|
|
497
|
+
"maintainer": recipe.owner,
|
|
498
|
+
"security.scanner": "dockerls",
|
|
499
|
+
"org.opencontainers.image.title": recipe.title,
|
|
500
|
+
"org.opencontainers.image.description": recipe.description,
|
|
501
|
+
"org.opencontainers.image.source": recipe.source,
|
|
502
|
+
**recipe.extra,
|
|
503
|
+
}
|
|
504
|
+
entries = [(key, value) for key, value in labels.items() if value]
|
|
505
|
+
if not entries:
|
|
506
|
+
return []
|
|
507
|
+
rendered = ["LABEL " + f'{entries[0][0]}="{entries[0][1]}"' + (" \\" if entries[1:] else "")]
|
|
508
|
+
for index, (key, value) in enumerate(entries[1:]):
|
|
509
|
+
suffix = " \\" if index < len(entries) - 2 else ""
|
|
510
|
+
rendered.append(f' {key}="{value}"{suffix}')
|
|
511
|
+
return rendered
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _packages(recipe: BaseRecipe) -> list[str]:
|
|
515
|
+
chosen = [choice for choice in PACKAGE_CATALOG if choice.key in recipe.packages]
|
|
516
|
+
names = sorted(
|
|
517
|
+
{
|
|
518
|
+
choice.package_for(recipe.family)
|
|
519
|
+
for choice in chosen
|
|
520
|
+
if choice.package_for(recipe.family)
|
|
521
|
+
}
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
comment = [
|
|
525
|
+
"# The digest freezes the base on the day it was published; without this line,",
|
|
526
|
+
"# a package fixed after that date would stay old here.",
|
|
527
|
+
]
|
|
528
|
+
if recipe.family.uses_apk:
|
|
529
|
+
if not names:
|
|
530
|
+
return [*comment, "RUN apk upgrade --no-cache"]
|
|
531
|
+
pacotes = " \\\n ".join(names)
|
|
532
|
+
return [
|
|
533
|
+
*comment,
|
|
534
|
+
"# `--no-cache` leaves no index behind: there is nothing to clean up in",
|
|
535
|
+
"# a later layer, and removing the cache afterwards would still leave it",
|
|
536
|
+
"# behind in this layer.",
|
|
537
|
+
"RUN apk upgrade --no-cache && \\",
|
|
538
|
+
" apk add --no-cache \\",
|
|
539
|
+
f" {pacotes}",
|
|
540
|
+
]
|
|
541
|
+
|
|
542
|
+
if not names:
|
|
543
|
+
return [
|
|
544
|
+
*comment,
|
|
545
|
+
"RUN apt-get update && apt-get upgrade -y --no-install-recommends && \\",
|
|
546
|
+
" rm -rf /var/lib/apt/lists/*",
|
|
547
|
+
]
|
|
548
|
+
pacotes = " \\\n ".join(names)
|
|
549
|
+
return [
|
|
550
|
+
*comment,
|
|
551
|
+
"# The index lists go out in the layer that created them: removing them later",
|
|
552
|
+
"# would still leave the bytes behind in the earlier layer.",
|
|
553
|
+
"RUN apt-get update && apt-get upgrade -y --no-install-recommends && \\",
|
|
554
|
+
" apt-get install -y --no-install-recommends \\",
|
|
555
|
+
f" {pacotes} && \\",
|
|
556
|
+
" rm -rf /var/lib/apt/lists/*",
|
|
557
|
+
]
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _create_user(recipe: BaseRecipe) -> list[str]:
|
|
561
|
+
comment = [
|
|
562
|
+
"# a high, fixed uid: consumers inherit the user without recreating it,",
|
|
563
|
+
"# and a high uid does not collide with host users on a bind mount.",
|
|
564
|
+
]
|
|
565
|
+
if recipe.family.uses_apk:
|
|
566
|
+
return [
|
|
567
|
+
*comment,
|
|
568
|
+
f"RUN addgroup -g {recipe.uid} {recipe.user_name} && \\",
|
|
569
|
+
f" adduser -u {recipe.uid} -G {recipe.user_name} "
|
|
570
|
+
f"-h /home/{recipe.user_name} -s /sbin/nologin -D {recipe.user_name}",
|
|
571
|
+
]
|
|
572
|
+
return [
|
|
573
|
+
*comment,
|
|
574
|
+
f"RUN groupadd -g {recipe.uid} {recipe.user_name} && \\",
|
|
575
|
+
f" useradd -u {recipe.uid} -g {recipe.user_name} "
|
|
576
|
+
f"-s /usr/sbin/nologin -m {recipe.user_name}",
|
|
577
|
+
]
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _strip_manager(base: RuntimeBase) -> list[str]:
|
|
581
|
+
"""Remove o gerenciador de pacotes que a imagem oficial embute.
|
|
582
|
+
|
|
583
|
+
O `apk upgrade` não alcança essas dependências: elas vivem em
|
|
584
|
+
`node_modules` dentro do próprio npm, não no banco de pacotes da
|
|
585
|
+
distribuição. Numa base de execução elas são superfície pura -- as
|
|
586
|
+
dependências da aplicação já foram instaladas no estágio de build de quem
|
|
587
|
+
consome, e nada aqui precisa instalar mais nada.
|
|
588
|
+
|
|
589
|
+
Quem *precisa* de npm em runtime (um `npm start` que resolve dependências
|
|
590
|
+
na subida, por exemplo) simplesmente não marca esta opção.
|
|
591
|
+
"""
|
|
592
|
+
caminhos = " \\\n ".join(base.bundled_manager)
|
|
593
|
+
return [
|
|
594
|
+
f"# Removes {base.bundled_manager_note}: in a runtime base, the dependencies",
|
|
595
|
+
"# a package manager carries inside itself are pure surface -- and they sit",
|
|
596
|
+
"# outside the reach of a system upgrade, not being distribution packages.",
|
|
597
|
+
"USER root",
|
|
598
|
+
"RUN rm -rf \\",
|
|
599
|
+
f" {caminhos}",
|
|
600
|
+
]
|