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,35 @@
|
|
|
1
|
+
"""Typer-facing adapters over the pure validators in ``dockerls.utils.validation``.
|
|
2
|
+
|
|
3
|
+
They exist so an out-of-range option produces a readable CLI error instead of
|
|
4
|
+
either a raw ``ValueError`` traceback or -- worse, in the case of
|
|
5
|
+
``--workers 0`` -- a scan loop that blocks forever on a semaphore nobody can
|
|
6
|
+
acquire.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from dockerls.utils.validation import validate_threshold, validate_workers
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _hint(param: str) -> str:
|
|
17
|
+
return "--" + param.replace("_", "-")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check_threshold(value: int, param: str, *, minimum: int = 0) -> int:
|
|
21
|
+
try:
|
|
22
|
+
return validate_threshold(value, param, minimum=minimum)
|
|
23
|
+
except ValueError as e:
|
|
24
|
+
raise typer.BadParameter(str(e), param_hint=_hint(param)) from e
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_limit(value: int, param: str = "limit") -> int:
|
|
28
|
+
return check_threshold(value, param, minimum=1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_workers(value: int, param: str = "workers") -> int:
|
|
32
|
+
try:
|
|
33
|
+
return validate_workers(value, param)
|
|
34
|
+
except ValueError as e:
|
|
35
|
+
raise typer.BadParameter(str(e), param_hint=_hint(param)) from e
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Apresentação de listas de vulnerabilidades.
|
|
2
|
+
|
|
3
|
+
Vive na camada de apresentação de propósito: ordenar para leitura humana e
|
|
4
|
+
escolher rótulo de coluna são decisões de terminal, não regras de domínio. O
|
|
5
|
+
que é fato sobre a vulnerabilidade -- se ela está num pacote de SO ou de
|
|
6
|
+
linguagem -- mora em `Vulnerability.origin`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from dockerls.domain.entities.vulnerability import PackageOrigin
|
|
14
|
+
from dockerls.domain.value_objects.tristate import Tristate
|
|
15
|
+
from dockerls.integrations.exploitdb.client import EXPLOIT_URL_TEMPLATE
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Iterable
|
|
19
|
+
|
|
20
|
+
from dockerls.domain.entities.vulnerability import Vulnerability
|
|
21
|
+
|
|
22
|
+
# Ordem de leitura de um relatório de segurança: primeiro o que é mais grave.
|
|
23
|
+
# Ordenar só por CVSS decrescente -- que era o que a tabela fazia -- empurrava
|
|
24
|
+
# um CRITICAL de score 7.5 para baixo de sete HIGH de score 8.6, ou seja,
|
|
25
|
+
# escondia o achado mais grave na sexta linha.
|
|
26
|
+
SEVERITY_RANK = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "UNKNOWN": 4}
|
|
27
|
+
|
|
28
|
+
_ORIGIN_LABELS = {
|
|
29
|
+
PackageOrigin.OS: "os",
|
|
30
|
+
PackageOrigin.LANGUAGE: "lang",
|
|
31
|
+
PackageOrigin.UNKNOWN: "-",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def sort_by_severity(vulns: Iterable[Vulnerability]) -> list[Vulnerability]:
|
|
36
|
+
"""Severity first, then CVSS descending, then CVE id for stability."""
|
|
37
|
+
return sorted(
|
|
38
|
+
vulns,
|
|
39
|
+
key=lambda v: (
|
|
40
|
+
SEVERITY_RANK.get(v.severity.value, 4),
|
|
41
|
+
-(v.cvss_score or 0.0),
|
|
42
|
+
v.cve_id,
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def origin_label(vuln: Vulnerability) -> str:
|
|
48
|
+
return _ORIGIN_LABELS[vuln.origin]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def count_by_origin(vulns: Iterable[Vulnerability]) -> dict[PackageOrigin, int]:
|
|
52
|
+
counts = dict.fromkeys(PackageOrigin, 0)
|
|
53
|
+
for vuln in vulns:
|
|
54
|
+
counts[vuln.origin] += 1
|
|
55
|
+
return counts
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Remediações que dependem de *onde* o pacote vive. A do npm é o caso que
|
|
59
|
+
# motivou isto: as vulnerabilidades de `node:22-alpine` estão todas em
|
|
60
|
+
# `/usr/local/lib/node_modules/npm/node_modules/`, então `apk upgrade` não
|
|
61
|
+
# resolve nenhuma -- e remover o npm da imagem final resolve todas.
|
|
62
|
+
_NPM_BUNDLE_MARKERS = ("node_modules/npm", "/npm/")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def bundled_npm_findings(vulns: Iterable[Vulnerability]) -> list[Vulnerability]:
|
|
66
|
+
"""Findings that come from the npm CLI bundled into the Node image."""
|
|
67
|
+
return [
|
|
68
|
+
v
|
|
69
|
+
for v in vulns
|
|
70
|
+
if v.origin is PackageOrigin.LANGUAGE
|
|
71
|
+
and any(marker in v.target.lower() for marker in _NPM_BUNDLE_MARKERS)
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def npm_remediation_hint(vulns: Iterable[Vulnerability]) -> str:
|
|
76
|
+
"""Concrete remediation for the bundled-npm case, or "" when it does not
|
|
77
|
+
apply. Returned as text so the caller decides how to render it."""
|
|
78
|
+
findings = bundled_npm_findings(vulns)
|
|
79
|
+
if not findings:
|
|
80
|
+
return ""
|
|
81
|
+
return (
|
|
82
|
+
f"{len(findings)} finding(s) come from the npm CLI bundled in the image, "
|
|
83
|
+
"not from OS packages -- `apk upgrade` / `apt upgrade` will not touch them.\n"
|
|
84
|
+
" If npm is needed at runtime: RUN npm install -g npm@latest\n"
|
|
85
|
+
" If it is not (multi-stage build shipping only dist/):\n"
|
|
86
|
+
" RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def threat_label(vuln: Vulnerability) -> str:
|
|
91
|
+
"""Explorabilidade numa célula: KEV, Exploit-DB, os dois, ou nenhum dado.
|
|
92
|
+
|
|
93
|
+
Os dois sinais existiam no modelo e nunca chegavam à tela -- eram
|
|
94
|
+
calculados, entravam no score e eram descartados na exibição. Uma coluna
|
|
95
|
+
para cada custaria duas das poucas que a tabela tem sobrando, e são a
|
|
96
|
+
mesma pergunta vista de dois ângulos, então dividem uma:
|
|
97
|
+
|
|
98
|
+
* `KEV` -- a CISA observou exploração real desta CVE;
|
|
99
|
+
* `EDB` -- há exploit publicado no Exploit-DB, ainda não verificado;
|
|
100
|
+
* `EDB*` -- há exploit publicado **e verificado** (alguém reproduziu);
|
|
101
|
+
* `KEV+EDB` -- os dois, com `*` quando o exploit é verificado;
|
|
102
|
+
* `No` -- as fontes responderam e nenhuma lista esta CVE;
|
|
103
|
+
* `-` -- nada foi consultado. **Nunca** confundir com `No`.
|
|
104
|
+
|
|
105
|
+
O `-` é o motivo de a função existir: com a fonte fora do ar, imprimir
|
|
106
|
+
`No` afirmaria que não existe exploit conhecido, que é precisamente a
|
|
107
|
+
substituição -- ausência de medida virando resultado negativo -- que
|
|
108
|
+
esta ferramenta existe para não fazer.
|
|
109
|
+
"""
|
|
110
|
+
kev = vuln.kev_status is Tristate.TRUE
|
|
111
|
+
edb = vuln.exploitdb_status is Tristate.TRUE
|
|
112
|
+
|
|
113
|
+
if kev and edb:
|
|
114
|
+
return "KEV+EDB*" if vuln.exploitdb_verified else "KEV+EDB"
|
|
115
|
+
if kev:
|
|
116
|
+
return "KEV"
|
|
117
|
+
if edb:
|
|
118
|
+
return "EDB*" if vuln.exploitdb_verified else "EDB"
|
|
119
|
+
|
|
120
|
+
# Sem achado positivo: só dá para dizer "não" quando alguma fonte
|
|
121
|
+
# efetivamente respondeu sobre esta CVE.
|
|
122
|
+
if vuln.kev_status is Tristate.FALSE or vuln.exploitdb_status is Tristate.FALSE:
|
|
123
|
+
return "No"
|
|
124
|
+
return "-"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def threat_style(vuln: Vulnerability) -> str:
|
|
128
|
+
"""A cor acompanha a força da evidência, não o rótulo.
|
|
129
|
+
|
|
130
|
+
Exploração observada em produção (KEV) e exploit verificado são vermelho;
|
|
131
|
+
exploit publicado mas não verificado é amarelo, porque é um sinal mais
|
|
132
|
+
fraco e mostrá-lo com o mesmo peso exageraria o que se sabe.
|
|
133
|
+
"""
|
|
134
|
+
kev = vuln.kev_status is Tristate.TRUE
|
|
135
|
+
edb = vuln.exploitdb_status is Tristate.TRUE
|
|
136
|
+
if kev or (edb and vuln.exploitdb_verified):
|
|
137
|
+
return "red bold"
|
|
138
|
+
if edb:
|
|
139
|
+
return "yellow"
|
|
140
|
+
return "dim"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def exploit_urls(vuln: Vulnerability) -> list[str]:
|
|
144
|
+
"""Links para os exploits catalogados, para quem quer julgar a fonte.
|
|
145
|
+
|
|
146
|
+
Só EDB-IDs numéricos entram numa URL: o identificador vem de um CSV de
|
|
147
|
+
terceiro, e montar link com o que vier de lá é como se constrói um
|
|
148
|
+
redirecionamento para onde o atacante quiser.
|
|
149
|
+
"""
|
|
150
|
+
return [
|
|
151
|
+
EXPLOIT_URL_TEMPLATE.format(edb_id=edb_id)
|
|
152
|
+
for edb_id in vuln.exploitdb_ids
|
|
153
|
+
if edb_id.isdigit()
|
|
154
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""As entidades do domínio, resolvidas sob demanda.
|
|
2
|
+
|
|
3
|
+
Mesmo motivo do `value_objects`: são todos modelos pydantic, que o pydantic
|
|
4
|
+
compila no import, e o `__init__` do pacote roda antes de qualquer
|
|
5
|
+
`from dockerls.domain.entities.image import DockerImage`. Reexportar tudo
|
|
6
|
+
no topo cobrava 116ms de cada comando -- inclusive dos que não tocam em
|
|
7
|
+
imagem nenhuma.
|
|
8
|
+
|
|
9
|
+
`__getattr__` de módulo (PEP 562) preserva a API pública: quem importa
|
|
10
|
+
daqui continua importando daqui, e só o submódulo do nome pedido é
|
|
11
|
+
carregado.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from dockerls.domain.entities.dockerfile_analysis import (
|
|
20
|
+
DockerfileAnalysis,
|
|
21
|
+
DockerfileInfo,
|
|
22
|
+
DockerfileValidationResult,
|
|
23
|
+
HardeningRule,
|
|
24
|
+
SeverityLevel,
|
|
25
|
+
ValidationCheck,
|
|
26
|
+
ValidationStatus,
|
|
27
|
+
)
|
|
28
|
+
from dockerls.domain.entities.image import DockerImage
|
|
29
|
+
from dockerls.domain.entities.recommendation import Recommendation
|
|
30
|
+
from dockerls.domain.entities.scan_result import ScanResult
|
|
31
|
+
from dockerls.domain.entities.vulnerability import Vulnerability
|
|
32
|
+
|
|
33
|
+
#: Nome público -> submódulo que o define.
|
|
34
|
+
_EXPORTS = {
|
|
35
|
+
"DockerfileAnalysis": "dockerfile_analysis",
|
|
36
|
+
"DockerfileInfo": "dockerfile_analysis",
|
|
37
|
+
"DockerfileValidationResult": "dockerfile_analysis",
|
|
38
|
+
"HardeningRule": "dockerfile_analysis",
|
|
39
|
+
"SeverityLevel": "dockerfile_analysis",
|
|
40
|
+
"ValidationCheck": "dockerfile_analysis",
|
|
41
|
+
"ValidationStatus": "dockerfile_analysis",
|
|
42
|
+
"DockerImage": "image",
|
|
43
|
+
"Recommendation": "recommendation",
|
|
44
|
+
"ScanResult": "scan_result",
|
|
45
|
+
"Vulnerability": "vulnerability",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"DockerImage",
|
|
50
|
+
"DockerfileAnalysis",
|
|
51
|
+
"DockerfileInfo",
|
|
52
|
+
"DockerfileValidationResult",
|
|
53
|
+
"HardeningRule",
|
|
54
|
+
"Recommendation",
|
|
55
|
+
"ScanResult",
|
|
56
|
+
"SeverityLevel",
|
|
57
|
+
"ValidationCheck",
|
|
58
|
+
"ValidationStatus",
|
|
59
|
+
"Vulnerability",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def __getattr__(name: str) -> Any:
|
|
64
|
+
module = _EXPORTS.get(name)
|
|
65
|
+
if module is None:
|
|
66
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
67
|
+
from importlib import import_module
|
|
68
|
+
|
|
69
|
+
value = getattr(import_module(f"{__name__}.{module}"), name)
|
|
70
|
+
globals()[name] = value
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def __dir__() -> list[str]:
|
|
75
|
+
return sorted(__all__)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""What a catalogue *claims* about an image, kept separate from what was measured.
|
|
2
|
+
|
|
3
|
+
Docker Hardened Images publishes declarative build definitions: which
|
|
4
|
+
packages go in, which account the image runs as, when the release goes
|
|
5
|
+
end-of-life. That is genuinely useful -- it is more than any registry API
|
|
6
|
+
exposes -- but it describes the *intent* of a build, not the bytes that were
|
|
7
|
+
published under a tag. A definition can be ahead of the registry, behind it,
|
|
8
|
+
or describe a build that failed.
|
|
9
|
+
|
|
10
|
+
So catalogue claims live in their own type, are always labelled as declared,
|
|
11
|
+
and never substitute for a scan. `DockerLs` uses them to discover candidates
|
|
12
|
+
and to explain trade-offs; the verdict still comes from resolving the digest
|
|
13
|
+
and scanning the image.
|
|
14
|
+
|
|
15
|
+
The asymmetry in the derived facts below is deliberate and load-bearing: a
|
|
16
|
+
declared shell package proves a shell is present, but the *absence* of one
|
|
17
|
+
proves nothing -- a busybox-derived base ships `/bin/sh` without ever naming
|
|
18
|
+
it as a package. Presence therefore yields TRUE and absence yields UNKNOWN,
|
|
19
|
+
never FALSE.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from pydantic import BaseModel, ConfigDict
|
|
25
|
+
|
|
26
|
+
from dockerls.domain.value_objects.tristate import Tristate
|
|
27
|
+
|
|
28
|
+
#: Packages that put an interactive shell in the image. Presence is proof;
|
|
29
|
+
#: absence is not.
|
|
30
|
+
SHELL_PACKAGES = frozenset(
|
|
31
|
+
{"bash", "busybox", "dash", "zsh", "ksh", "fish", "toybox", "ash", "shadow"}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
#: Packages that put a package manager in the image, which is both an
|
|
35
|
+
#: attack-surface item and a sign the image is a `-dev` variant.
|
|
36
|
+
PACKAGE_MANAGER_PACKAGES = frozenset(
|
|
37
|
+
{"apt", "apt-get", "apk-tools", "dpkg", "rpm", "dnf", "yum", "microdnf", "zypper", "pacman"}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
#: Compilers, fetchers and inspection utilities. Each one is a capability an
|
|
41
|
+
#: attacker inherits if they reach code execution inside the container.
|
|
42
|
+
DEBUG_TOOL_PACKAGES = frozenset(
|
|
43
|
+
{
|
|
44
|
+
"curl",
|
|
45
|
+
"wget",
|
|
46
|
+
"git",
|
|
47
|
+
"gcc",
|
|
48
|
+
"g++",
|
|
49
|
+
"clang",
|
|
50
|
+
"make",
|
|
51
|
+
"binutils",
|
|
52
|
+
"strace",
|
|
53
|
+
"ltrace",
|
|
54
|
+
"gdb",
|
|
55
|
+
"netcat",
|
|
56
|
+
"netcat-openbsd",
|
|
57
|
+
"nmap",
|
|
58
|
+
"tcpdump",
|
|
59
|
+
"procps",
|
|
60
|
+
"vim",
|
|
61
|
+
"nano",
|
|
62
|
+
"less",
|
|
63
|
+
"python3",
|
|
64
|
+
"perl",
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class DeclaredImageMetadata(BaseModel):
|
|
70
|
+
"""A catalogue's declaration about one image definition.
|
|
71
|
+
|
|
72
|
+
Frozen: a declaration is a record of what a source said at fetch time.
|
|
73
|
+
Mutating it after the fact would make the evidence trail unreliable.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
model_config = ConfigDict(frozen=True)
|
|
77
|
+
|
|
78
|
+
#: Which catalogue made the claim, e.g. "Docker Hardened Images".
|
|
79
|
+
catalog: str = ""
|
|
80
|
+
#: Where the claim can be read in full, so a user can audit it.
|
|
81
|
+
definition_url: str = ""
|
|
82
|
+
#: Human name of the definition ("Node.js 22.x").
|
|
83
|
+
display_name: str = ""
|
|
84
|
+
#: Fully-qualified repository the definition publishes to
|
|
85
|
+
#: ("dhi.io/node"). This is what makes a catalogue entry resolvable
|
|
86
|
+
#: against a registry; without it there is no candidate to verify.
|
|
87
|
+
registry_repository: str = ""
|
|
88
|
+
#: "runtime", "dev", "compat", ... -- a `-dev` variant deliberately
|
|
89
|
+
#: carries a shell and a package manager and must not be ranked as if
|
|
90
|
+
#: it were the hardened runtime image.
|
|
91
|
+
variant: str = ""
|
|
92
|
+
#: Account the definition says the image runs as. Empty when unstated.
|
|
93
|
+
run_as_user: str = ""
|
|
94
|
+
#: Declared platforms ("linux/amd64"), used for architecture matching.
|
|
95
|
+
platforms: tuple[str, ...] = ()
|
|
96
|
+
#: ISO dates from the definition; empty when unstated.
|
|
97
|
+
release_date: str = ""
|
|
98
|
+
end_of_life: str = ""
|
|
99
|
+
#: Base OS the definition builds on.
|
|
100
|
+
os_id: str = ""
|
|
101
|
+
os_version: str = ""
|
|
102
|
+
#: Number of packages the definition installs. A real signal of size and
|
|
103
|
+
#: attack surface -- and one no registry API exposes.
|
|
104
|
+
declared_package_count: int | None = None
|
|
105
|
+
entrypoint: tuple[str, ...] = ()
|
|
106
|
+
cmd: tuple[str, ...] = ()
|
|
107
|
+
#: Tags the definition says it publishes, which is what makes a
|
|
108
|
+
#: catalogue entry resolvable against the registry.
|
|
109
|
+
tags: tuple[str, ...] = ()
|
|
110
|
+
#: Package names matched against the sets above, kept so the reason for
|
|
111
|
+
#: a derived fact can be shown rather than asserted.
|
|
112
|
+
shell_packages: tuple[str, ...] = ()
|
|
113
|
+
package_manager_packages: tuple[str, ...] = ()
|
|
114
|
+
debug_tool_packages: tuple[str, ...] = ()
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def declared_non_root(self) -> Tristate:
|
|
118
|
+
"""Whether the definition says the image runs as a non-root account.
|
|
119
|
+
|
|
120
|
+
`run-as: root` is an explicit FALSE. An empty `run-as` is UNKNOWN,
|
|
121
|
+
not FALSE: the definition simply did not say, and the OCI config of
|
|
122
|
+
the built image is the authority either way.
|
|
123
|
+
"""
|
|
124
|
+
if not self.run_as_user:
|
|
125
|
+
return Tristate.UNKNOWN
|
|
126
|
+
return Tristate.of(self.run_as_user.lower() not in ("root", "0"))
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def declared_has_shell(self) -> Tristate:
|
|
130
|
+
return Tristate.TRUE if self.shell_packages else Tristate.UNKNOWN
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def declared_has_package_manager(self) -> Tristate:
|
|
134
|
+
return Tristate.TRUE if self.package_manager_packages else Tristate.UNKNOWN
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def declared_has_debug_tools(self) -> Tristate:
|
|
138
|
+
return Tristate.TRUE if self.debug_tool_packages else Tristate.UNKNOWN
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def is_dev_variant(self) -> bool:
|
|
142
|
+
"""A development variant ships build tooling by design.
|
|
143
|
+
|
|
144
|
+
Ranked against runtime variants without this distinction, a `-dev`
|
|
145
|
+
image looks like just another candidate from a hardened catalogue.
|
|
146
|
+
"""
|
|
147
|
+
return "dev" in self.variant.lower().split("-") or self.variant.lower().endswith("dev")
|