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,129 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import StrEnum
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from dockerls.domain.entities.vulnerability import Severity, Vulnerability
8
+
9
+
10
+ class ScanStatus(StrEnum):
11
+ OK = "OK"
12
+ ERROR = "ERROR"
13
+ TIMEOUT = "TIMEOUT"
14
+ PARTIAL = "PARTIAL"
15
+
16
+
17
+ class ScanErrorKind(StrEnum):
18
+ """Why a scan failed, as a stable code rather than a slice of stderr.
19
+
20
+ A terminal column can only hold a few characters, and truncating the raw
21
+ message produced things like `error in v...` -- which names no cause and
22
+ cannot be grouped, counted, or acted on. The full stderr is still kept in
23
+ `error_message` (log file and `--format json`); this is what the table
24
+ shows instead of a cut-off prefix.
25
+
26
+ It also decides *retryability*: a broken vulnerability database is the
27
+ scanner's problem and another scanner may well succeed, while a tag that
28
+ does not exist will fail identically no matter who is asked.
29
+ """
30
+
31
+ NONE = "NONE"
32
+ DB_INIT_FAILED = "DB_INIT_FAILED"
33
+ TIMEOUT = "TIMEOUT"
34
+ AUTH_REQUIRED = "AUTH_REQUIRED"
35
+ NOT_FOUND = "NOT_FOUND"
36
+ RATE_LIMITED = "RATE_LIMITED"
37
+ INVALID_OUTPUT = "INVALID_OUTPUT"
38
+ SCANNER_MISSING = "SCANNER_MISSING"
39
+ #: The reference named a host the network policy refuses to contact.
40
+ #: The scan never ran, so this is an absence of measurement like any
41
+ #: other error -- and it is *not* a scanner fault: a second tool would
42
+ #: pull from exactly the same refused host.
43
+ BLOCKED_BY_POLICY = "BLOCKED_BY_POLICY"
44
+ UNKNOWN = "UNKNOWN"
45
+
46
+ @property
47
+ def is_scanner_fault(self) -> bool:
48
+ """True when a *different* scanner has a real chance of succeeding.
49
+
50
+ `NOT_FOUND` and `AUTH_REQUIRED` are facts about the image, so retrying
51
+ them with another tool just doubles the wait for the same answer.
52
+ """
53
+ return self in (
54
+ ScanErrorKind.DB_INIT_FAILED,
55
+ ScanErrorKind.TIMEOUT,
56
+ ScanErrorKind.RATE_LIMITED,
57
+ ScanErrorKind.INVALID_OUTPUT,
58
+ ScanErrorKind.SCANNER_MISSING,
59
+ ScanErrorKind.UNKNOWN,
60
+ )
61
+
62
+
63
+ class ScanResult(BaseModel):
64
+ image_reference: str
65
+ scanner: str = "trivy"
66
+ vulnerabilities: list[Vulnerability] = []
67
+ scan_timestamp: str = ""
68
+ status: ScanStatus = ScanStatus.OK
69
+ error_message: str = ""
70
+ # Classified reason for a failure. `error_message` keeps the full stderr;
71
+ # this is the part that fits in a table and can be grouped across tags.
72
+ error_kind: ScanErrorKind = ScanErrorKind.NONE
73
+ # Path to the raw scanner JSON this result was parsed from, so a score
74
+ # shown to the user can always be traced back to its source evidence.
75
+ evidence_path: str = ""
76
+ # Base distribution the scanner identified ("debian", "alpine", "wolfi")
77
+ # and its version. This is the only *measured* source of the image's
78
+ # libc family, which is what decides whether a migration between two
79
+ # images can work at all -- an Alpine-to-Debian move swaps musl for
80
+ # glibc, and native modules compiled against one do not load under the
81
+ # other. Empty when the scanner did not report it.
82
+ os_family: str = ""
83
+ os_version: str = ""
84
+
85
+ @property
86
+ def is_verified(self) -> bool:
87
+ """A scan that actually completed and produced a parsed result.
88
+
89
+ This is the gate for recommending an image: only a scanner run that
90
+ exited cleanly (`OK`) and carries a timestamp counts as proof. A
91
+ `PARTIAL` scan is usable for reporting but is deliberately *not*
92
+ verified -- it means some targets could not be inspected, so its
93
+ vulnerability counts are a lower bound, not a measurement.
94
+ """
95
+ return self.status is ScanStatus.OK and bool(self.scan_timestamp)
96
+
97
+ @property
98
+ def critical_count(self) -> int:
99
+ return sum(1 for v in self.vulnerabilities if v.severity == Severity.CRITICAL)
100
+
101
+ @property
102
+ def high_count(self) -> int:
103
+ return sum(1 for v in self.vulnerabilities if v.severity == Severity.HIGH)
104
+
105
+ @property
106
+ def medium_count(self) -> int:
107
+ return sum(1 for v in self.vulnerabilities if v.severity == Severity.MEDIUM)
108
+
109
+ @property
110
+ def low_count(self) -> int:
111
+ return sum(1 for v in self.vulnerabilities if v.severity == Severity.LOW)
112
+
113
+ @property
114
+ def total_count(self) -> int:
115
+ return len(self.vulnerabilities)
116
+
117
+ @property
118
+ def fixable_count(self) -> int:
119
+ return sum(1 for v in self.vulnerabilities if v.is_fixable)
120
+
121
+ @property
122
+ def fixable_high_count(self) -> int:
123
+ return sum(1 for v in self.vulnerabilities if v.severity == Severity.HIGH and v.is_fixable)
124
+
125
+ @property
126
+ def fixable_critical_count(self) -> int:
127
+ return sum(
128
+ 1 for v in self.vulnerabilities if v.severity == Severity.CRITICAL and v.is_fixable
129
+ )
@@ -0,0 +1,232 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from enum import StrEnum
5
+ from typing import Any
6
+
7
+ from loguru import logger
8
+ from pydantic import BaseModel, Field, field_validator
9
+
10
+ from dockerls.domain.value_objects.tristate import Tristate
11
+
12
+
13
+ def _probability(value: Any, field: str) -> float:
14
+ """Coerce an externally supplied probability into 0.0-1.0, or to zero.
15
+
16
+ EPSS arrives as JSON from a third party, and `float()` happily accepts
17
+ `"nan"`, `"inf"` and `"-1"`. None of the three is a probability, and all
18
+ three used to reach the scoring engine, where the damage was not
19
+ cosmetic:
20
+
21
+ * `NaN` propagates through the penalty sum, and the final
22
+ `max(0.0, min(100.0, score))` answers **100.0** for a NaN input --
23
+ every comparison against NaN is False, so the clamp keeps its own
24
+ bound. A malformed feed therefore certified an image as perfect.
25
+ * A negative probability *subtracts* from the penalty, so a feed
26
+ answering `-5` raises the score of an image with more findings.
27
+
28
+ Both are the same failure this codebase is built to refuse: a source
29
+ that did not give a usable answer being spent as evidence of safety. An
30
+ unusable value becomes 0.0, which contributes nothing in either
31
+ direction, and is loud in the log rather than silent.
32
+ """
33
+ try:
34
+ number = float(value)
35
+ except (TypeError, ValueError):
36
+ number = math.nan
37
+ if not math.isfinite(number) or not (0.0 <= number <= 1.0):
38
+ logger.warning(f"Discarding out-of-range {field} value {value!r}; treated as no signal")
39
+ return 0.0
40
+ return number
41
+
42
+
43
+ class Severity(StrEnum):
44
+ CRITICAL = "CRITICAL"
45
+ HIGH = "HIGH"
46
+ MEDIUM = "MEDIUM"
47
+ LOW = "LOW"
48
+ UNKNOWN = "UNKNOWN"
49
+
50
+
51
+ class FixStatus(StrEnum):
52
+ FIX_AVAILABLE = "FIX_AVAILABLE"
53
+ NO_FIX = "NO_FIX"
54
+ UNDER_INVESTIGATION = "UNDER_INVESTIGATION"
55
+
56
+
57
+ class PackageOrigin(StrEnum):
58
+ """De onde vem o pacote afetado -- o que determina como se remedia."""
59
+
60
+ OS = "os"
61
+ LANGUAGE = "lang-pkg"
62
+ UNKNOWN = "unknown"
63
+
64
+
65
+ # Marcadores vindos do `Class`/`Type`/`Target` do Trivy e do `artifact.type`
66
+ # do Grype. Os de linguagem são checados primeiro: um `Target` como
67
+ # "/usr/local/lib/node_modules/npm/package-lock.json" contém tanto "npm"
68
+ # quanto, potencialmente, o nome da distro.
69
+ _LANG_MARKERS = (
70
+ "lang-pkg",
71
+ "node-pkg",
72
+ "npm",
73
+ "yarn",
74
+ "python-pkg",
75
+ "pip",
76
+ "poetry",
77
+ "gemspec",
78
+ "gem",
79
+ "cargo",
80
+ "composer",
81
+ "go-module",
82
+ "gobinary",
83
+ "jar",
84
+ "pom",
85
+ "conan",
86
+ "nuget",
87
+ "swift",
88
+ "pub",
89
+ "hex",
90
+ "site-packages",
91
+ "node_modules",
92
+ )
93
+ _OS_MARKERS = (
94
+ "os-pkg",
95
+ "apk",
96
+ "deb",
97
+ "rpm",
98
+ "alpine",
99
+ "debian",
100
+ "ubuntu",
101
+ "redhat",
102
+ "centos",
103
+ "amazon",
104
+ "suse",
105
+ "photon",
106
+ "oracle",
107
+ "rocky",
108
+ "alma",
109
+ "wolfi",
110
+ "chainguard",
111
+ )
112
+
113
+
114
+ class Vulnerability(BaseModel):
115
+ cve_id: str
116
+ severity: Severity
117
+ cvss_score: float = 0.0
118
+ # Qual base publicou o `cvss_score` acima ("nvd", "redhat", "ghsa", ...).
119
+ # Severidade e score podem vir de bases diferentes -- é assim que um
120
+ # CRITICAL aparecia ao lado de um CVSS 7.5 e parecia contradição. Dizer a
121
+ # origem é o que torna o número verificável em vez de suspeito.
122
+ cvss_source: str = ""
123
+ # Origem do pacote: "os" (apk/apt/yum) ou "lang-pkg" (npm, pip, gem...).
124
+ # Decide a remediação: `apk upgrade` não resolve um CVE de dependência do
125
+ # npm embutido na imagem.
126
+ package_type: str = ""
127
+ target: str = ""
128
+ package_name: str = ""
129
+ installed_version: str = ""
130
+ fixed_version: str = ""
131
+ description: str = ""
132
+ published_date: str = ""
133
+ # Confirmed exploited in the wild (CISA KEV). Kept as the boolean the
134
+ # scoring engine and the exporters have always read; `kev_status` is the
135
+ # field that distinguishes "checked, not listed" from "never checked".
136
+ exploit_known: bool = False
137
+ # Three-valued, because a threat-intel feed that did not answer must not
138
+ # look like a feed that answered "no". With the KEV catalogue
139
+ # unreachable every CVE came back `exploit_known=False`, and the report
140
+ # then stated -- affirmatively -- that the image had no known-exploited
141
+ # vulnerabilities. That sentence is the strongest claim this tool makes
142
+ # about real-world exploitation, and it was being emitted precisely when
143
+ # nothing had been consulted.
144
+ kev_status: Tristate = Tristate.UNKNOWN
145
+ # EPSS probability, 0.0-1.0. `epss_known` says whether the 0.0 means
146
+ # "FIRST.org scored it at zero" or "FIRST.org was not reachable".
147
+ epss_score: float = 0.0
148
+ epss_known: bool = False
149
+ #: EPSS percentile when the source provides it: 0.9 probability is
150
+ #: extreme, while the percentile says how extreme relative to everything
151
+ #: else scored that day.
152
+ epss_percentile: float = 0.0
153
+ #: When the intelligence above was retrieved, so a stale enrichment is
154
+ #: visible rather than assumed current.
155
+ threat_intel_timestamp: str = ""
156
+ # Public exploit catalogued in Exploit-DB. Three-valued for the same
157
+ # reason `kev_status` is: a catalogue that was never consulted must not
158
+ # read as a catalogue that answered "no exploit". UNKNOWN is the default
159
+ # precisely so an absent lookup can never be spent as reassurance.
160
+ #
161
+ # KEV and this answer different questions and neither replaces the
162
+ # other: KEV means "observed being exploited in the wild", while an
163
+ # Exploit-DB entry means "working exploit code is published". A CVE can
164
+ # sit in one and not the other.
165
+ exploitdb_status: Tristate = Tristate.UNKNOWN
166
+ #: The EDB-ID(s) matched, so a reader can open
167
+ #: https://www.exploit-db.com/exploits/<id> and judge the exploit itself.
168
+ #: Matching is strictly CVE-ID to CVE-ID -- see the client for why a
169
+ #: product-name fallback would manufacture false positives.
170
+ exploitdb_ids: list[str] = Field(default_factory=list)
171
+ #: Exploit-DB flags entries as verified (someone reproduced it) or not.
172
+ #: An unverified entry is a weaker signal than a verified one, and
173
+ #: collapsing the two would overstate what is known. Only meaningful
174
+ #: when `exploitdb_status` is TRUE.
175
+ exploitdb_verified: bool = False
176
+
177
+ @field_validator("epss_score", "epss_percentile", mode="before")
178
+ @classmethod
179
+ def _bound_probabilities(cls, value: Any, info: Any) -> float:
180
+ """Enforced on the entity, not only where the feed is parsed.
181
+
182
+ The same field is rebuilt from the SQLite cache and from scanner
183
+ JSON, and a row written before this check existed is still on disk.
184
+ The invariant belongs to the type.
185
+ """
186
+ return _probability(value, info.field_name)
187
+
188
+ @property
189
+ def origin(self) -> PackageOrigin:
190
+ """Whether this finding lives in an OS package or a language package.
191
+
192
+ Decide a remediação, e a diferença é total: as vulnerabilidades de
193
+ `node:22-alpine` estão todas em
194
+ `/usr/local/lib/node_modules/npm/node_modules/`, isto é, nas
195
+ dependências do npm que a imagem embute. `apk upgrade` não resolve
196
+ nenhuma delas. Sem essa distinção, a recomendação aponta para o
197
+ gerenciador errado.
198
+ """
199
+ haystack = f"{self.package_type} {self.target}".lower()
200
+ if any(marker in haystack for marker in _LANG_MARKERS):
201
+ return PackageOrigin.LANGUAGE
202
+ if any(marker in haystack for marker in _OS_MARKERS):
203
+ return PackageOrigin.OS
204
+ return PackageOrigin.UNKNOWN
205
+
206
+ @property
207
+ def fix_status(self) -> FixStatus:
208
+ if self.fixed_version:
209
+ return FixStatus.FIX_AVAILABLE
210
+ return FixStatus.NO_FIX
211
+
212
+ @property
213
+ def is_fixable(self) -> bool:
214
+ return self.fix_status == FixStatus.FIX_AVAILABLE
215
+
216
+
217
+ def finding_identity(vuln: Vulnerability) -> str:
218
+ """Uma identidade estável para um achado, comparável entre medições.
219
+
220
+ Id da vulnerabilidade **e** pacote, porque o mesmo CVE afetando dois
221
+ pacotes de uma imagem são dois problemas a resolver, e o mesmo pacote
222
+ carregando dois CVEs também. A versão instalada fica de fora de propósito:
223
+ dois scanners -- e a mesma imagem antes e depois de um build -- reportam o
224
+ mesmo pacote com strings de versão normalizadas de formas diferentes, e
225
+ comparar isso fabricaria diferença a partir de formatação.
226
+
227
+ Mora aqui, e não no serviço que a inventou, porque duas coisas distintas
228
+ dependem dela: a validação cruzada entre scanners e a atribuição de cada
229
+ achado à base ou às suas camadas. Se cada uma tivesse a sua, as duas
230
+ divergiriam na primeira correção.
231
+ """
232
+ return f"{vuln.cve_id.strip().upper()}|{vuln.package_name.strip().lower()}"
@@ -0,0 +1,17 @@
1
+ from dockerls.domain.interfaces.cache_store import CacheStoreInterface
2
+ from dockerls.domain.interfaces.dockerfile_validator import (
3
+ DockerfileValidatorInterface,
4
+ HardeningTemplateProvider,
5
+ )
6
+ from dockerls.domain.interfaces.eol_checker import EOLCheckerInterface
7
+ from dockerls.domain.interfaces.image_repository import ImageRepositoryInterface
8
+ from dockerls.domain.interfaces.scanner import ScannerInterface
9
+
10
+ __all__ = [
11
+ "ImageRepositoryInterface",
12
+ "ScannerInterface",
13
+ "EOLCheckerInterface",
14
+ "CacheStoreInterface",
15
+ "DockerfileValidatorInterface",
16
+ "HardeningTemplateProvider",
17
+ ]
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+
6
+
7
+ class CacheStoreInterface(ABC):
8
+ @abstractmethod
9
+ async def get(self, key: str) -> Any | None: ...
10
+
11
+ @abstractmethod
12
+ async def set(self, key: str, value: Any, ttl_seconds: int = 86400) -> None: ...
13
+
14
+ @abstractmethod
15
+ async def delete(self, key: str) -> None: ...
16
+
17
+ @abstractmethod
18
+ async def clear(self) -> None: ...
@@ -0,0 +1,99 @@
1
+ """Interfaces para validação de Dockerfiles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from pathlib import Path
10
+
11
+ from dockerls.domain.entities.dockerfile_analysis import (
12
+ DockerfileAnalysis,
13
+ DockerfileValidationResult,
14
+ HardeningRule,
15
+ )
16
+
17
+
18
+ class DockerfileValidatorInterface(ABC):
19
+ """Interface para validadores de Dockerfile."""
20
+
21
+ @abstractmethod
22
+ def validate(self, dockerfile_path: str | Path) -> DockerfileValidationResult:
23
+ """Valida um Dockerfile contra regras OWASP.
24
+
25
+ Args:
26
+ dockerfile_path: Caminho para o Dockerfile ou diretório contendo Dockerfile.
27
+
28
+ Returns:
29
+ Resultado da validação com checks, warnings e errors.
30
+ """
31
+ pass
32
+
33
+ @abstractmethod
34
+ def analyze(self, dockerfile_path: str | Path) -> DockerfileAnalysis:
35
+ """Analisa um Dockerfile e retorna análise completa.
36
+
37
+ Args:
38
+ dockerfile_path: Caminho para o Dockerfile ou diretório contendo Dockerfile.
39
+
40
+ Returns:
41
+ Análise completa incluindo info, validação e score de segurança.
42
+ """
43
+ pass
44
+
45
+ @abstractmethod
46
+ def suggest_hardening(self, dockerfile_path: str | Path) -> list[HardeningRule]:
47
+ """Sugere melhorias de hardening para um Dockerfile.
48
+
49
+ Args:
50
+ dockerfile_path: Caminho para o Dockerfile ou diretório contendo Dockerfile.
51
+
52
+ Returns:
53
+ Lista de regras de hardening sugeridas.
54
+ """
55
+ pass
56
+
57
+
58
+ class HardeningTemplateProvider(ABC):
59
+ """Interface para provedores de templates hardened."""
60
+
61
+ @abstractmethod
62
+ def get_template(self, base_image: str) -> str:
63
+ """Retorna template hardened para um tipo de base image.
64
+
65
+ Args:
66
+ base_image: Nome da imagem base (ex: node, python, go).
67
+
68
+ Returns:
69
+ Conteúdo do template Dockerfile hardened.
70
+ """
71
+ pass
72
+
73
+ @abstractmethod
74
+ def list_templates(self) -> list[str]:
75
+ """Lista todos os templates disponíveis.
76
+
77
+ Returns:
78
+ Lista de nomes de templates disponíveis.
79
+ """
80
+ pass
81
+
82
+ @abstractmethod
83
+ def generate_hardened_dockerfile(
84
+ self,
85
+ dockerfile_path: str | Path,
86
+ base_image: str | None = None,
87
+ output_path: str | Path | None = None,
88
+ ) -> str:
89
+ """Gera um Dockerfile hardened baseado no original ou template.
90
+
91
+ Args:
92
+ dockerfile_path: Caminho para o Dockerfile original.
93
+ base_image: Imagem base para usar no template (opcional).
94
+ output_path: Caminho para salvar o novo Dockerfile (opcional).
95
+
96
+ Returns:
97
+ Conteúdo do Dockerfile hardened gerado.
98
+ """
99
+ pass
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+ class EOLCheckerInterface(ABC):
7
+ @abstractmethod
8
+ async def is_eol(self, product: str, version: str) -> bool: ...
9
+
10
+ @abstractmethod
11
+ async def is_lts(self, product: str, version: str) -> bool: ...
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from dockerls.domain.entities.image import DockerImage
8
+
9
+
10
+ class ImageRepositoryInterface(ABC):
11
+ @abstractmethod
12
+ async def search_tags(self, image_name: str, limit: int = 100) -> list[DockerImage]: ...
13
+
14
+ @abstractmethod
15
+ async def get_image_metadata(self, image_name: str, tag: str) -> DockerImage | None: ...
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from dockerls.domain.entities.scan_result import ScanResult
8
+
9
+
10
+ class ScannerInterface(ABC):
11
+ @abstractmethod
12
+ async def scan(self, image_reference: str) -> ScanResult: ...
13
+
14
+ @abstractmethod
15
+ async def is_available(self) -> bool: ...