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