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,79 @@
1
+ """A private registry, reachable the same way any OCI Distribution V2
2
+ registry is: host, optional namespace, and Basic credentials exchanged for
3
+ a scoped bearer token at whatever realm the registry's 401 challenge names.
4
+
5
+ This is deliberately one implementation, not one per vendor. ECR, Harbor,
6
+ GHCR's container registry, Artifactory, and a self-hosted `registry:2` all
7
+ speak the same protocol -- the only per-registry difference is the host,
8
+ namespace, and how the operator obtained a password (`aws ecr
9
+ get-login-password` for ECR, a personal access token for GHCR, a robot
10
+ account for Harbor). None of that changes what this client sends over the
11
+ wire, so none of it needs its own class.
12
+
13
+ Configured through `Settings` (`DOCKERLS_PRIVATE_REGISTRY_HOST` and
14
+ friends) and registered with `SourceRegistry` like every other source --
15
+ `recommend`/`search`/`analyze`/`compare` never learn this exists as a
16
+ distinct case.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ from dockerls.integrations.registry.hardened import HardenedRepository
24
+
25
+ if TYPE_CHECKING:
26
+ from dockerls.domain.entities.image import DockerImage
27
+ from dockerls.infrastructure.network.host_guard import HostGuard
28
+
29
+ PRIVATE_REGISTRY = "Private Registry"
30
+
31
+
32
+ class PrivateRegistryRepository(HardenedRepository):
33
+ """One organization's own registry, authenticated with Basic
34
+ credentials against the standard Docker Registry HTTP API V2 token
35
+ endpoint.
36
+
37
+ Unlike Chainguard/Distroless, whose `repositories` map is a small,
38
+ curated alias table for a handful of well-known ecosystem names, a
39
+ private registry's repository names *are* the query: `team/app`,
40
+ `internal/base-images/python`, whatever the organization's own naming
41
+ convention is. Multi-segment paths are accepted rather than refused --
42
+ that refusal in the base class exists to keep Chainguard/Distroless
43
+ from being asked about a reference that already names a different
44
+ registry, which does not apply here since this repository *is* the
45
+ registry the reference is being asked about.
46
+ """
47
+
48
+ source = PRIVATE_REGISTRY
49
+
50
+ def __init__(
51
+ self,
52
+ host: str,
53
+ namespace: str = "",
54
+ timeout: int = 30,
55
+ guard: HostGuard | None = None,
56
+ *,
57
+ username: str = "",
58
+ password: str = "",
59
+ ):
60
+ self.host = host
61
+ self.namespace = namespace
62
+ super().__init__(timeout=timeout, guard=guard, username=username, password=password)
63
+
64
+ def repositories_for(self, image_name: str) -> list[str]:
65
+ name = image_name.strip().strip("/")
66
+ if not name or ":" in name:
67
+ return []
68
+ if self.namespace:
69
+ return [f"{self.namespace}/{name}"]
70
+ return [name]
71
+
72
+ def _build_image(self, repository: str, tag: str, payload: dict[str, Any]) -> DockerImage:
73
+ # `is_official=False`: this is an organization's own image, not a
74
+ # vetted upstream catalogue like Docker Hub official images or
75
+ # Chainguard's -- it must not collect the scoring bonus those get.
76
+ # The base class defaults to `is_official=True`, so it is
77
+ # overridden explicitly rather than left to that default.
78
+ image = super()._build_image(repository, tag, payload)
79
+ return image.model_copy(update={"is_official": False})
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from urllib.parse import quote
4
+
5
+ from dockerls.integrations.dockerhub.urls import build_dockerhub_url
6
+
7
+ # Browsable catalogue pages for the hardened sources. Neither registry
8
+ # exposes a per-tag web page, so these link to the image's catalogue entry.
9
+ _HARDENED_HOSTS = {
10
+ "cgr.dev": "https://images.chainguard.dev/directory/image/{repo}/versions",
11
+ "gcr.io": "https://console.cloud.google.com/gcr/images/{namespace}/global/{repo}",
12
+ }
13
+
14
+
15
+ def source_url(image_name: str, tag: str) -> str:
16
+ """Return a human-browsable URL for `image_name`:`tag` at its source.
17
+
18
+ Docker Hub images get their exact tag-filtered Hub URL; hardened
19
+ registries get their catalogue page. Returns "" when the source has no
20
+ known web presence.
21
+ """
22
+ hub = build_dockerhub_url(image_name, tag)
23
+ if hub:
24
+ return hub
25
+
26
+ parts = image_name.split("/")
27
+ if len(parts) < 3:
28
+ return ""
29
+ host, namespace, repo = parts[0], parts[1], "/".join(parts[2:])
30
+ template = _HARDENED_HOSTS.get(host)
31
+ if not template:
32
+ return ""
33
+ return template.format(
34
+ namespace=quote(namespace, safe=""),
35
+ repo=quote(repo, safe=""),
36
+ )
@@ -0,0 +1,67 @@
1
+ """Classificação do stderr dos scanners em causas estáveis.
2
+
3
+ Trivy e Grype relatam falhas como texto livre em stderr. Guardar só um
4
+ prefixo cortado desse texto -- que era o que a tabela mostrava, produzindo
5
+ coisas como `error in v...` -- não nomeia causa nenhuma e não permite agrupar
6
+ 93 falhas para descobrir que todas são a mesma.
7
+
8
+ Aqui o texto vira um `ScanErrorKind`. A mensagem completa continua em
9
+ `ScanResult.error_message`, indo para o arquivo de log e para `--format json`;
10
+ esta classificação é o que aparece no terminal, e é o que decide se vale a
11
+ pena tentar o mesmo alvo com o outro scanner.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+
18
+ from dockerls.domain.entities.scan_result import ScanErrorKind
19
+
20
+ # Ordem importa: a primeira regra que casar vence, então as causas mais
21
+ # específicas vêm antes das genéricas. "db error" aparece dentro de mensagens
22
+ # que também mencionam "failed to download", por exemplo.
23
+ _RULES: tuple[tuple[ScanErrorKind, re.Pattern[str]], ...] = (
24
+ (
25
+ ScanErrorKind.RATE_LIMITED,
26
+ re.compile(r"rate limit|too many requests|429|toomanyrequests", re.IGNORECASE),
27
+ ),
28
+ (
29
+ ScanErrorKind.AUTH_REQUIRED,
30
+ re.compile(
31
+ r"unauthorized|authentication required|forbidden|401|403|denied: requested access",
32
+ re.IGNORECASE,
33
+ ),
34
+ ),
35
+ (
36
+ ScanErrorKind.NOT_FOUND,
37
+ re.compile(
38
+ r"manifest unknown|not found|no such image|repository does not exist"
39
+ r"|name unknown|could not find the image|unable to find the specified image",
40
+ re.IGNORECASE,
41
+ ),
42
+ ),
43
+ (
44
+ ScanErrorKind.DB_INIT_FAILED,
45
+ re.compile(
46
+ r"db error|database error|init error|failed to download (?:vulnerability )?db"
47
+ r"|failed to initialize|unable to open database|bad database|db\.metadata"
48
+ r"|cache may be in use|database is locked|failed to open db",
49
+ re.IGNORECASE,
50
+ ),
51
+ ),
52
+ (
53
+ ScanErrorKind.TIMEOUT,
54
+ re.compile(r"timeout|timed out|deadline exceeded|context deadline", re.IGNORECASE),
55
+ ),
56
+ )
57
+
58
+
59
+ def classify_scanner_error(message: str) -> ScanErrorKind:
60
+ """Map a scanner's stderr onto a stable, groupable cause code."""
61
+ text = (message or "").strip()
62
+ if not text:
63
+ return ScanErrorKind.UNKNOWN
64
+ for kind, pattern in _RULES:
65
+ if pattern.search(text):
66
+ return kind
67
+ return ScanErrorKind.UNKNOWN
@@ -0,0 +1,57 @@
1
+ """Refuse a scan target the network policy would not let us contact.
2
+
3
+ `trivy image X` and `grype X` open their own sockets. The SSRF guard that
4
+ protects the registry inspector never saw those pulls, so a reference like
5
+ `169.254.169.254/latest:v1` -- syntactically valid, and arriving from a CI
6
+ variable, a config file or a pull request -- aimed the scanner's connection
7
+ at the cloud metadata endpoint while the guarded door stayed shut.
8
+
9
+ The check runs *before* the binary is invoked, and its refusal is an
10
+ `ERROR` result with `BLOCKED_BY_POLICY`, never an empty finding list: an
11
+ image nobody was allowed to measure has not been measured, and the whole
12
+ pipeline already treats an unmeasured image as unverified rather than clean.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from datetime import UTC, datetime
18
+ from typing import TYPE_CHECKING
19
+
20
+ from loguru import logger
21
+
22
+ from dockerls.domain.entities.scan_result import ScanErrorKind, ScanResult, ScanStatus
23
+ from dockerls.domain.value_objects.image_reference import registry_host_of
24
+
25
+ if TYPE_CHECKING:
26
+ from dockerls.infrastructure.network.host_guard import HostGuard
27
+
28
+
29
+ def blocked_target_reason(reference: str, guard: HostGuard | None) -> str:
30
+ """Why `reference` may not be scanned, or "" when it may.
31
+
32
+ No guard means no policy was configured for this scanner, and the answer
33
+ is "" -- the caller decides whether to run unguarded. Docker Hub
34
+ references carry no host and are never judged: contacting Docker Hub is
35
+ what this tool is for.
36
+ """
37
+ if guard is None:
38
+ return ""
39
+ host = registry_host_of(reference)
40
+ if not host:
41
+ return ""
42
+ if guard.allows(host):
43
+ return ""
44
+ return guard.explain(host)
45
+
46
+
47
+ def blocked_scan_result(reference: str, scanner: str, reason: str) -> ScanResult:
48
+ """The result a refused target produces: an error, not an empty scan."""
49
+ logger.warning(f"Refusing to scan {reference} with {scanner}: {reason}")
50
+ return ScanResult(
51
+ image_reference=reference,
52
+ scanner=scanner,
53
+ scan_timestamp=datetime.now(tz=UTC).isoformat(),
54
+ status=ScanStatus.ERROR,
55
+ error_message=reason,
56
+ error_kind=ScanErrorKind.BLOCKED_BY_POLICY,
57
+ )
File without changes