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,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}")
|