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