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,91 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from dockerls.application.dto.analysis import (
6
+ ComparisonResult,
7
+ ImageAnalysis,
8
+ UnverifiedImage,
9
+ )
10
+ from dockerls.application.services.teardown import close_quietly
11
+
12
+ if TYPE_CHECKING:
13
+ from dockerls.application.use_cases.analyze_image import AnalyzeImageUseCase
14
+ from dockerls.domain.entities.vulnerability import Vulnerability
15
+
16
+
17
+ class CompareImagesUseCase:
18
+ def __init__(self, analyze_use_case: AnalyzeImageUseCase):
19
+ self._analyze = analyze_use_case
20
+
21
+ async def execute(self, references: list[str]) -> ComparisonResult:
22
+ try:
23
+ return await self._compare(references)
24
+ finally:
25
+ # The inner use case holds the scanner and the repository's
26
+ # connection pool; it cannot release them itself because it is
27
+ # called once per image.
28
+ await close_quietly(self._analyze)
29
+
30
+ async def _compare(self, references: list[str]) -> ComparisonResult:
31
+ # A separação acontece aqui, antes de qualquer conta: uma imagem que
32
+ # não pôde ser escaneada não tem score para comparar, e deixá-la
33
+ # entrar no `max()` abaixo permitia que ela fosse *escolhida* como
34
+ # vencedora contra um campo de imagens igualmente não medidas -- ou,
35
+ # pior, que servisse de piso para o delta de quem foi medido.
36
+ analyses: list[ImageAnalysis] = []
37
+ unverified: list[UnverifiedImage] = []
38
+ for ref in references:
39
+ analysis = await self._analyze.execute(ref)
40
+ if analysis.scan.is_verified:
41
+ analyses.append(analysis)
42
+ continue
43
+ unverified.append(
44
+ UnverifiedImage(
45
+ image_reference=analysis.image.full_reference or ref,
46
+ status=analysis.scan.status.value,
47
+ reason=analysis.scan.error_message or "no details",
48
+ kind=analysis.scan.error_kind.value,
49
+ )
50
+ )
51
+
52
+ if not analyses:
53
+ return ComparisonResult(images=[], unverified=unverified)
54
+
55
+ winner = max(analyses, key=lambda a: a.security_score)
56
+
57
+ all_cve_sets: list[set[str]] = []
58
+ cve_map: dict[str, Vulnerability] = {}
59
+ for a in analyses:
60
+ cve_ids: set[str] = set()
61
+ for v in a.scan.vulnerabilities:
62
+ cve_ids.add(v.cve_id)
63
+ cve_map[v.cve_id] = v
64
+ all_cve_sets.append(cve_ids)
65
+
66
+ common_ids = set.intersection(*all_cve_sets) if all_cve_sets else set()
67
+ common_vulns = [cve_map[cid] for cid in common_ids if cid in cve_map]
68
+
69
+ unique_vulns: dict[str, list[Vulnerability]] = {}
70
+ for a, cve_ids_set in zip(analyses, all_cve_sets, strict=True):
71
+ unique_ids = cve_ids_set - common_ids
72
+ unique_vulns[a.image.full_reference] = [
73
+ cve_map[uid] for uid in unique_ids if uid in cve_map
74
+ ]
75
+
76
+ # Uma linha só, com vencedor, score absoluto e delta misturados e
77
+ # separados por ponto e vírgula, produzia
78
+ # `...; node:22-bookworm-slim: -36.0 points` -- em que o `-36.0` lê
79
+ # como um score negativo em vez de uma diferença. Os dados vão
80
+ # estruturados; quem renderiza decide o formato.
81
+ return ComparisonResult(
82
+ images=analyses,
83
+ winner=winner.image.full_reference,
84
+ summary=(
85
+ f"{winner.image.full_reference} scores highest "
86
+ f"({winner.security_score}, tier {winner.tier})"
87
+ ),
88
+ common_vulns=common_vulns,
89
+ unique_vulns=unique_vulns,
90
+ unverified=unverified,
91
+ )
@@ -0,0 +1,240 @@
1
+ """Percorrer uma árvore de repositórios e ler todo Dockerfile que houver.
2
+
3
+ A metade pura está em `domain/value_objects/fleet.py`. Aqui mora o que ela não
4
+ pode fazer: andar no disco.
5
+
6
+ Andar no disco é justamente onde este comando pode se machucar, e três limites
7
+ existem por isso:
8
+
9
+ * **Nada de symlink.** Um link para `/` transformaria uma varredura de um
10
+ repositório numa varredura da máquina inteira, e um link para fora da árvore
11
+ faria o relatório falar de arquivos que não pertencem a ela.
12
+ * **Teto de arquivos e de profundidade.** Uma varredura que roda por vinte
13
+ minutos é uma varredura que ninguém roda de novo. Quando o teto é atingido o
14
+ relatório **diz que foi truncado** -- um retrato parcial que se apresenta
15
+ como completo é pior do que nenhum retrato.
16
+ * **Diretórios de dependência ficam de fora.** Os `Dockerfile` dentro de
17
+ `node_modules` ou de um `.venv` são de terceiros: incluí-los enche o
18
+ relatório de linhas sobre as quais ninguém pode agir, e a lista deixa de ser
19
+ fila de trabalho.
20
+
21
+ Um arquivo que não pôde ser lido vira entrada com erro, nunca desaparece do
22
+ relatório: sumir com o ilegível faria a frota parecer menor e mais em ordem do
23
+ que é.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from dataclasses import dataclass
29
+ from pathlib import Path
30
+ from typing import TYPE_CHECKING
31
+
32
+ from loguru import logger
33
+
34
+ from dockerls.domain.entities.dockerfile_analysis import (
35
+ DockerfileAnalysis,
36
+ ValidationStatus,
37
+ )
38
+ from dockerls.domain.value_objects.base_upgrade import parse_bases
39
+ from dockerls.domain.value_objects.build_policy import (
40
+ BaseFact,
41
+ PolicyFacts,
42
+ PolicyViolation,
43
+ evaluate,
44
+ )
45
+ from dockerls.domain.value_objects.fleet import FleetEntry, FleetReport
46
+ from dockerls.domain.value_objects.image_reference import registry_host_of
47
+ from dockerls.domain.value_objects.tristate import Tristate
48
+
49
+ if TYPE_CHECKING:
50
+ from dockerls.domain.interfaces.dockerfile_validator import DockerfileValidatorInterface
51
+ from dockerls.domain.value_objects.build_policy import BuildPolicy
52
+
53
+ #: Nomes reconhecidos como Dockerfile. `Dockerfile.prod`, `Dockerfile.hardened`
54
+ #: e afins entram: são arquivos que constroem imagens de verdade, e deixá-los
55
+ #: de fora esconderia exatamente as variantes que ninguém revisa.
56
+ _DOCKERFILE_PREFIX = "dockerfile"
57
+
58
+ #: Diretórios pulados sempre. São dependências de terceiros ou artefatos.
59
+ _SKIPPED_DIRECTORIES = frozenset(
60
+ {
61
+ ".git",
62
+ ".hg",
63
+ ".svn",
64
+ ".tox",
65
+ ".venv",
66
+ "venv",
67
+ "node_modules",
68
+ "vendor",
69
+ "__pycache__",
70
+ ".mypy_cache",
71
+ ".pytest_cache",
72
+ "dist",
73
+ "build",
74
+ "target",
75
+ ".terraform",
76
+ }
77
+ )
78
+
79
+ MAX_DOCKERFILES = 2_000
80
+ MAX_DEPTH = 12
81
+
82
+
83
+ @dataclass
84
+ class FleetScanRequest:
85
+ root: str
86
+ policy: BuildPolicy | None = None
87
+ max_dockerfiles: int = MAX_DOCKERFILES
88
+ max_depth: int = MAX_DEPTH
89
+
90
+
91
+ class FleetScanUseCase:
92
+ """Lê todo Dockerfile sob uma raiz e monta o retrato agregado."""
93
+
94
+ def __init__(self, validator: DockerfileValidatorInterface):
95
+ self._validator = validator
96
+
97
+ def execute(self, request: FleetScanRequest) -> FleetReport:
98
+ root = Path(request.root)
99
+ if not root.is_dir():
100
+ return FleetReport(root=str(root), unreadable_paths=(str(root),))
101
+
102
+ found, truncated, unreadable = _walk(
103
+ root, max_files=request.max_dockerfiles, max_depth=request.max_depth
104
+ )
105
+ # A política estática é um subconjunto: as regras que dependem de scan
106
+ # continuam valendo no `build`, e aplicá-las aqui produziria uma
107
+ # violação idêntica por arquivo -- uma lista toda vermelha não
108
+ # distingue nada.
109
+ static = request.policy.static_subset() if request.policy else None
110
+
111
+ entries = tuple(self._read(path, root, static) for path in found)
112
+ return FleetReport(
113
+ root=str(root),
114
+ entries=entries,
115
+ unreadable_paths=unreadable,
116
+ truncated=truncated,
117
+ policy_applied=request.policy is not None,
118
+ )
119
+
120
+ def _read(self, path: Path, root: Path, policy: BuildPolicy | None) -> FleetEntry:
121
+ relative = _relative(path, root)
122
+ try:
123
+ analysis = self._validator.analyze(str(path))
124
+ except Exception as e:
125
+ # Um arquivo ilegível continua no relatório: sumir com ele faria a
126
+ # frota parecer menor e mais em ordem do que é.
127
+ logger.debug(f"Could not analyze {path}: {e}")
128
+ return FleetEntry(path=relative, error=str(e) or "could not be analyzed")
129
+
130
+ info = analysis.info
131
+ nonroot = _nonroot(analysis)
132
+
133
+ # As bases saem do `parse_bases`, não de `info.base_images`: ele
134
+ # expande os `ARG`, e sem isso um `FROM python:3.12@${PYTHON_DIGEST}`
135
+ # -- que é a forma *correta* de fixar -- seria contado como não
136
+ # fixado. Uma varredura que reprova quem fez certo é uma varredura que
137
+ # ensina a fazer errado.
138
+ try:
139
+ declared = parse_bases(path.read_text(encoding="utf-8", errors="replace"))
140
+ except OSError as e:
141
+ logger.debug(f"Could not re-read {path}: {e}")
142
+ declared = []
143
+
144
+ facts = tuple(
145
+ BaseFact(
146
+ reference=base.reference,
147
+ registry=registry_host_of(base.name),
148
+ pinned=base.is_pinned,
149
+ )
150
+ for base in declared
151
+ )
152
+ bases = tuple(fact.reference for fact in facts)
153
+ pinned = sum(1 for fact in facts if fact.pinned)
154
+
155
+ violations: tuple[PolicyViolation, ...] = ()
156
+ if policy is not None:
157
+ violations = tuple(
158
+ evaluate(
159
+ policy,
160
+ PolicyFacts(bases=facts, labels=dict(info.labels), nonroot=nonroot),
161
+ )
162
+ )
163
+
164
+ return FleetEntry(
165
+ path=relative,
166
+ bases=bases,
167
+ pinned_bases=pinned,
168
+ total_bases=len(facts),
169
+ nonroot=nonroot,
170
+ stages=info.stages,
171
+ violations=violations,
172
+ )
173
+
174
+
175
+ def _nonroot(analysis: DockerfileAnalysis) -> Tristate:
176
+ """O veredito do DF002, ou `UNKNOWN` quando a checagem não está lá.
177
+
178
+ A ausência é `UNKNOWN` e não `FALSE`: não ter medido não é ter medido e
179
+ reprovado, e a frota conta os dois em colunas separadas justamente porque
180
+ a fila de trabalho de cada um é diferente.
181
+ """
182
+ for check in analysis.validation.checks:
183
+ if check.rule_id == "DF002":
184
+ return Tristate.of(check.status is ValidationStatus.PASS)
185
+ return Tristate.UNKNOWN
186
+
187
+
188
+ def _relative(path: Path, root: Path) -> str:
189
+ try:
190
+ return path.relative_to(root).as_posix()
191
+ except ValueError: # pragma: no cover - a varredura nunca sai da raiz
192
+ return str(path)
193
+
194
+
195
+ def _walk(
196
+ root: Path, *, max_files: int, max_depth: int
197
+ ) -> tuple[list[Path], bool, tuple[str, ...]]:
198
+ """Os Dockerfiles sob `root`, ordenados, sem seguir symlink."""
199
+ found: list[Path] = []
200
+ unreadable: list[str] = []
201
+ truncated = False
202
+ stack: list[tuple[Path, int]] = [(root, 0)]
203
+
204
+ while stack:
205
+ directory, depth = stack.pop()
206
+ if depth > max_depth:
207
+ truncated = True
208
+ continue
209
+ try:
210
+ children = sorted(directory.iterdir())
211
+ except OSError as e:
212
+ logger.debug(f"Could not list {directory}: {e}")
213
+ unreadable.append(_relative(directory, root))
214
+ continue
215
+
216
+ for child in children:
217
+ # Symlinks nunca são seguidos: um link para `/` transformaria a
218
+ # varredura de um repositório numa varredura da máquina.
219
+ if child.is_symlink():
220
+ continue
221
+ if child.is_dir():
222
+ if child.name not in _SKIPPED_DIRECTORIES:
223
+ stack.append((child, depth + 1))
224
+ elif _is_dockerfile(child.name):
225
+ if len(found) >= max_files:
226
+ truncated = True
227
+ continue
228
+ found.append(child)
229
+
230
+ found.sort()
231
+ return found, truncated, tuple(sorted(unreadable))
232
+
233
+
234
+ def _is_dockerfile(name: str) -> bool:
235
+ lowered = name.lower()
236
+ if lowered == _DOCKERFILE_PREFIX or lowered.startswith(f"{_DOCKERFILE_PREFIX}."):
237
+ return True
238
+ # `app.Dockerfile` é a outra convenção comum, e ignorá-la deixaria de fora
239
+ # repositórios inteiros que a usam.
240
+ return lowered.endswith(f".{_DOCKERFILE_PREFIX}")