conclear 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 (137) hide show
  1. conclear/__init__.py +5 -0
  2. conclear/__main__.py +6 -0
  3. conclear/_embedded_identity.py +3 -0
  4. conclear/adapters/__init__.py +1 -0
  5. conclear/adapters/base.py +107 -0
  6. conclear/adapters/buildah.py +128 -0
  7. conclear/adapters/ci.py +198 -0
  8. conclear/adapters/cosign.py +475 -0
  9. conclear/adapters/git.py +127 -0
  10. conclear/adapters/hadolint.py +84 -0
  11. conclear/adapters/podman.py +661 -0
  12. conclear/adapters/quay.py +492 -0
  13. conclear/adapters/registry_backends.py +100 -0
  14. conclear/adapters/skopeo.py +193 -0
  15. conclear/adapters/trivy.py +402 -0
  16. conclear/archive.py +340 -0
  17. conclear/archive_source.py +113 -0
  18. conclear/artifacts.py +710 -0
  19. conclear/attestations.py +111 -0
  20. conclear/build_identity.py +18 -0
  21. conclear/catalog.py +415 -0
  22. conclear/checks.py +687 -0
  23. conclear/cli.py +229 -0
  24. conclear/commands/__init__.py +1 -0
  25. conclear/commands/adopt.py +64 -0
  26. conclear/commands/archive.py +143 -0
  27. conclear/commands/common.py +209 -0
  28. conclear/commands/configuration.py +39 -0
  29. conclear/commands/local.py +540 -0
  30. conclear/commands/maintenance.py +836 -0
  31. conclear/commands/remote.py +462 -0
  32. conclear/commands/transport.py +88 -0
  33. conclear/commands/version.py +32 -0
  34. conclear/compatibility_inventory.py +260 -0
  35. conclear/config.py +1611 -0
  36. conclear/config_decisions.py +53 -0
  37. conclear/conformance.py +37 -0
  38. conclear/containerfile.py +325 -0
  39. conclear/context.py +381 -0
  40. conclear/data/__init__.py +1 -0
  41. conclear/data/checks.json +76 -0
  42. conclear/data/guide-options.json +133 -0
  43. conclear/data/guide-requirements.json +3045 -0
  44. conclear/data/implementation.json +408 -0
  45. conclear/data/requirement-coverage.json +244 -0
  46. conclear/database.py +129 -0
  47. conclear/dependencies.py +267 -0
  48. conclear/emulation.py +138 -0
  49. conclear/errors.py +119 -0
  50. conclear/fileio.py +119 -0
  51. conclear/freshness.py +146 -0
  52. conclear/guide_options.py +200 -0
  53. conclear/guide_requirements.py +567 -0
  54. conclear/hook_scratch.py +132 -0
  55. conclear/hooks.py +139 -0
  56. conclear/identity.py +77 -0
  57. conclear/implementation.py +367 -0
  58. conclear/jsonutil.py +158 -0
  59. conclear/layout_assembly.py +269 -0
  60. conclear/oci.py +480 -0
  61. conclear/parsing.py +97 -0
  62. conclear/path_safety.py +135 -0
  63. conclear/pin_application.py +393 -0
  64. conclear/pin_occurrences.py +292 -0
  65. conclear/pin_updates.py +572 -0
  66. conclear/pins.py +327 -0
  67. conclear/presentation.py +101 -0
  68. conclear/process.py +532 -0
  69. conclear/provenance.py +129 -0
  70. conclear/records.py +223 -0
  71. conclear/registry_control.py +101 -0
  72. conclear/registry_policy.py +165 -0
  73. conclear/release_check.py +737 -0
  74. conclear/release_profile.py +362 -0
  75. conclear/rescan_history.py +337 -0
  76. conclear/runtime.py +204 -0
  77. conclear/runtime_directory.py +197 -0
  78. conclear/scan_identity.py +86 -0
  79. conclear/scan_policy.py +508 -0
  80. conclear/schema.py +125 -0
  81. conclear/schemas/__init__.py +1 -0
  82. conclear/schemas/archive.schema.json +40 -0
  83. conclear/schemas/config.schema.json +398 -0
  84. conclear/schemas/profile.schema.json +128 -0
  85. conclear/schemas/proposal.schema.json +113 -0
  86. conclear/schemas/provenance.schema.json +99 -0
  87. conclear/schemas/record.schema.json +869 -0
  88. conclear/schemas/result.schema.json +2482 -0
  89. conclear/schemas/triage.schema.json +43 -0
  90. conclear/secrets.py +127 -0
  91. conclear/services/__init__.py +1 -0
  92. conclear/services/adoption.py +126 -0
  93. conclear/services/adoption_draft.py +443 -0
  94. conclear/services/adoption_observation.py +459 -0
  95. conclear/services/archive_rescan.py +130 -0
  96. conclear/services/archives.py +519 -0
  97. conclear/services/assembly.py +788 -0
  98. conclear/services/attestation.py +567 -0
  99. conclear/services/attestation_reads.py +41 -0
  100. conclear/services/checking.py +89 -0
  101. conclear/services/ci_context.py +135 -0
  102. conclear/services/cleanup.py +462 -0
  103. conclear/services/configuration_view.py +309 -0
  104. conclear/services/doctor.py +159 -0
  105. conclear/services/local_phases.py +105 -0
  106. conclear/services/preflight.py +178 -0
  107. conclear/services/privilege_tests.py +392 -0
  108. conclear/services/promotion.py +335 -0
  109. conclear/services/publication.py +428 -0
  110. conclear/services/qualification.py +724 -0
  111. conclear/services/qualification_inputs.py +128 -0
  112. conclear/services/registry_diagnostics.py +210 -0
  113. conclear/services/release.py +672 -0
  114. conclear/services/rescan.py +675 -0
  115. conclear/services/rescan_evidence.py +145 -0
  116. conclear/services/run_context.py +340 -0
  117. conclear/services/runtime_controls.py +133 -0
  118. conclear/services/runtime_lifecycle.py +804 -0
  119. conclear/services/runtime_tests.py +795 -0
  120. conclear/services/verification.py +363 -0
  121. conclear/setid_inventory.py +303 -0
  122. conclear/source_integrity.py +161 -0
  123. conclear/spdx.py +174 -0
  124. conclear/test_inputs.py +309 -0
  125. conclear/test_output_archive.py +92 -0
  126. conclear/tool_matrix.py +93 -0
  127. conclear/tools.py +324 -0
  128. conclear/transport.py +981 -0
  129. conclear/triage.py +110 -0
  130. conclear/values.py +264 -0
  131. conclear/version_sources.py +148 -0
  132. conclear/workspace.py +628 -0
  133. conclear-1.0.0.dist-info/METADATA +735 -0
  134. conclear-1.0.0.dist-info/RECORD +137 -0
  135. conclear-1.0.0.dist-info/WHEEL +4 -0
  136. conclear-1.0.0.dist-info/entry_points.txt +3 -0
  137. conclear-1.0.0.dist-info/licenses/LICENSES/GPL-3.0-or-later.txt +232 -0
conclear/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """ConClear OCI image release application."""
2
+
3
+ from conclear.identity import VERSION
4
+
5
+ __version__ = VERSION
conclear/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Run ConClear as a Python module."""
2
+
3
+ from conclear.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,3 @@
1
+ """Generated source identity for this distribution build."""
2
+
3
+ SOURCE_REVISION = "93692472fdb99cc2d3ea0e9c8aa8332d92d6ac63"
@@ -0,0 +1 @@
1
+ """Typed adapters for release tools and external services."""
@@ -0,0 +1,107 @@
1
+ """Shared command construction for external tool adapters."""
2
+
3
+ from collections.abc import Mapping, Sequence
4
+ from pathlib import Path
5
+ from typing import Protocol
6
+
7
+ from conclear.errors import InvalidInvocationError, OperationalError
8
+ from conclear.process import (
9
+ CommandRequest,
10
+ OperationKind,
11
+ ProcessResult,
12
+ )
13
+ from conclear.tools import ResolvedTool
14
+
15
+
16
+ def prepare_new_layout_path(layout_path: Path) -> None:
17
+ """Require an absent OCI-layout destination below an existing regular directory.
18
+
19
+ Tools that write `oci:` layouts neither create missing parents nor refuse an
20
+ existing layout, so every adapter that exports one applies the same
21
+ fail-closed preparation before the tool runs.
22
+ """
23
+ try:
24
+ layout_path.lstat()
25
+ except FileNotFoundError:
26
+ pass
27
+ except OSError as exc:
28
+ raise OperationalError(
29
+ f"Unable to inspect output layout {layout_path}"
30
+ ) from exc
31
+ else:
32
+ raise InvalidInvocationError(f"Output layout already exists: {layout_path}")
33
+ try:
34
+ layout_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
35
+ if not layout_path.parent.is_dir() or layout_path.parent.is_symlink():
36
+ raise InvalidInvocationError(
37
+ f"Output layout parent is not a regular directory: {layout_path.parent}"
38
+ )
39
+ except OSError as exc:
40
+ raise OperationalError(
41
+ f"Unable to create output layout parent {layout_path.parent}"
42
+ ) from exc
43
+
44
+
45
+ class Runner(Protocol):
46
+ """Narrow process runner boundary used by tool adapters."""
47
+
48
+ def run(self, request: CommandRequest) -> ProcessResult:
49
+ """Execute one fully specified external process."""
50
+ ...
51
+
52
+
53
+ class ToolAdapter:
54
+ """Execute one immutable resolved tool through the central runner."""
55
+
56
+ def __init__(
57
+ self,
58
+ *,
59
+ tool: ResolvedTool,
60
+ runner: Runner,
61
+ environment: Mapping[str, str],
62
+ log_directory: Path,
63
+ ) -> None:
64
+ """Bind the adapter to a run-owned environment and log directory."""
65
+ self._tool = tool
66
+ self._runner = runner
67
+ self._environment = dict(environment)
68
+ self._log_directory = log_directory
69
+ self._sequence = 0
70
+
71
+ def _run(
72
+ self,
73
+ arguments: Sequence[str],
74
+ *,
75
+ operation: OperationKind = OperationKind.READ,
76
+ timeout_seconds: float,
77
+ retries: int = 0,
78
+ extra_environment: Mapping[str, str] | None = None,
79
+ secret_values: tuple[str, ...] = (),
80
+ secret_paths: tuple[Path, ...] = (),
81
+ cwd: Path | None = None,
82
+ stdout_artifact: Path | None = None,
83
+ max_artifact_bytes: int = 128 * 1024 * 1024,
84
+ ) -> ProcessResult:
85
+ self._tool.assert_unchanged()
86
+ environment = dict(self._environment)
87
+ if extra_environment is not None:
88
+ environment.update(extra_environment)
89
+ self._sequence += 1
90
+ return self._runner.run(
91
+ CommandRequest(
92
+ argv=(str(self._tool.path), *arguments),
93
+ environment=environment,
94
+ timeout_seconds=timeout_seconds,
95
+ operation=operation,
96
+ retries=retries,
97
+ log_path=(
98
+ self._log_directory
99
+ / f"{self._tool.name.value}-{self._sequence:04d}.json"
100
+ ),
101
+ secret_values=secret_values,
102
+ secret_paths=secret_paths,
103
+ cwd=cwd,
104
+ stdout_artifact=stdout_artifact,
105
+ max_artifact_bytes=max_artifact_bytes,
106
+ )
107
+ )
@@ -0,0 +1,128 @@
1
+ """Rootless Buildah OCI-layout adapter."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from conclear.adapters.base import ToolAdapter, prepare_new_layout_path
7
+ from conclear.oci import OCIGraph, validate_layout
8
+ from conclear.parsing import json_value, object_value
9
+ from conclear.process import OperationKind
10
+ from conclear.values import Platform
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class BuildObservation:
15
+ """Observed result of one platform build."""
16
+
17
+ image_name: str
18
+ layout_path: Path
19
+ graph: OCIGraph
20
+ build_arguments: tuple[tuple[str, str], ...]
21
+
22
+
23
+ class BuildahAdapter(ToolAdapter):
24
+ """Build one platform using run-specific containers storage."""
25
+
26
+ def info(self, *, root: Path, runroot: Path) -> dict[str, object]:
27
+ """Validate access to one isolated rootless Buildah storage."""
28
+ output = self._run(
29
+ (
30
+ "--root",
31
+ str(root),
32
+ "--runroot",
33
+ str(runroot),
34
+ "info",
35
+ "--format",
36
+ "{{json .}}",
37
+ ),
38
+ timeout_seconds=120,
39
+ ).stdout
40
+ return object_value(
41
+ json_value(output, label="Buildah info"), label="Buildah info"
42
+ )
43
+
44
+ def build(
45
+ self,
46
+ *,
47
+ root: Path,
48
+ runroot: Path,
49
+ containerfile: Path,
50
+ context: Path,
51
+ platform: Platform,
52
+ image_name: str,
53
+ layout_path: Path,
54
+ layout_reference: str,
55
+ source_epoch: int,
56
+ build_arguments: dict[str, str],
57
+ auth_file: Path | None,
58
+ ) -> BuildObservation:
59
+ """Build and export a digest-preserving OCI image layout."""
60
+ prepare_new_layout_path(layout_path)
61
+ arguments: list[str] = [
62
+ "--root",
63
+ str(root),
64
+ "--runroot",
65
+ str(runroot),
66
+ "build",
67
+ "--format",
68
+ "oci",
69
+ "--platform",
70
+ str(platform),
71
+ "--source-date-epoch",
72
+ str(source_epoch),
73
+ "--rewrite-timestamp",
74
+ "--pull=always",
75
+ # Labels describe this image, never its base (IG0431). Annotation
76
+ # inheritance stays on: Buildah writes the base annotations through it.
77
+ "--inherit-labels=false",
78
+ "--file",
79
+ str(containerfile),
80
+ "--tag",
81
+ image_name,
82
+ ]
83
+ if auth_file is not None:
84
+ arguments.extend(("--authfile", str(auth_file)))
85
+ for name, value in sorted(build_arguments.items()):
86
+ arguments.extend(("--build-arg", f"{name}={value}"))
87
+ arguments.append(str(context))
88
+ self._run(
89
+ arguments,
90
+ timeout_seconds=3600,
91
+ operation=OperationKind.WRITE,
92
+ secret_paths=(() if auth_file is None else (auth_file,)),
93
+ )
94
+ self._run(
95
+ (
96
+ "--root",
97
+ str(root),
98
+ "--runroot",
99
+ str(runroot),
100
+ "push",
101
+ "--format",
102
+ "oci",
103
+ image_name,
104
+ f"oci:{layout_path}:{layout_reference}",
105
+ ),
106
+ timeout_seconds=1800,
107
+ operation=OperationKind.WRITE,
108
+ )
109
+ graph = validate_layout(layout_path, reference=layout_reference)
110
+ return BuildObservation(
111
+ image_name=image_name,
112
+ layout_path=layout_path,
113
+ graph=graph,
114
+ build_arguments=tuple(sorted(build_arguments.items())),
115
+ )
116
+
117
+ def remove_storage(self, *, root: Path, runroot: Path) -> None:
118
+ """Remove every image in one run-owned Buildah storage root."""
119
+ self._run(
120
+ ("--root", str(root), "--runroot", str(runroot), "rm", "--all"),
121
+ timeout_seconds=300,
122
+ operation=OperationKind.WRITE,
123
+ )
124
+ self._run(
125
+ ("--root", str(root), "--runroot", str(runroot), "rmi", "--all"),
126
+ timeout_seconds=300,
127
+ operation=OperationKind.WRITE,
128
+ )
@@ -0,0 +1,198 @@
1
+ """Typed observations from supported CI provider environments."""
2
+
3
+ import re
4
+ from collections.abc import Mapping
5
+ from dataclasses import dataclass
6
+ from urllib.parse import urlsplit, urlunsplit
7
+
8
+ from conclear.errors import InvalidInvocationError
9
+ from conclear.values import validate_source_revision
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class CIContextAbsent:
14
+ """No supported CI provider marker was observed."""
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class CIContextInvalid:
19
+ """A provider marker was observed without a usable complete context."""
20
+
21
+ provider: str | None
22
+ diagnostic: str
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class ObservedCIContext:
27
+ """Narrow provider metadata observed from the current process environment."""
28
+
29
+ provider: str
30
+ server: str
31
+ repository: str
32
+ revision: str
33
+ run_id: str
34
+
35
+
36
+ type CIContextObservation = CIContextAbsent | CIContextInvalid | ObservedCIContext
37
+
38
+ _REPOSITORY_COMPONENT = re.compile(r"^[A-Za-z0-9._~-]+$")
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class _ProviderSpec:
43
+ provider: str
44
+ server_variable: str
45
+ repository_variable: str
46
+ revision_variable: str
47
+ run_variable: str
48
+
49
+
50
+ _PROVIDERS = {
51
+ "forgejo-actions": _ProviderSpec(
52
+ "forgejo-actions",
53
+ "FORGEJO_SERVER_URL",
54
+ "FORGEJO_REPOSITORY",
55
+ "FORGEJO_SHA",
56
+ "FORGEJO_RUN_ID",
57
+ ),
58
+ # Gitea's native gitea.* values are expression contexts; its documented
59
+ # process environment retains GITHUB_* for these identity fields.
60
+ "gitea-actions": _ProviderSpec(
61
+ "gitea-actions",
62
+ "GITHUB_SERVER_URL",
63
+ "GITHUB_REPOSITORY",
64
+ "GITHUB_SHA",
65
+ "GITHUB_RUN_ID",
66
+ ),
67
+ "github-actions": _ProviderSpec(
68
+ "github-actions",
69
+ "GITHUB_SERVER_URL",
70
+ "GITHUB_REPOSITORY",
71
+ "GITHUB_SHA",
72
+ "GITHUB_RUN_ID",
73
+ ),
74
+ "gitlab-ci": _ProviderSpec(
75
+ "gitlab-ci",
76
+ "CI_SERVER_URL",
77
+ "CI_PROJECT_PATH",
78
+ "CI_COMMIT_SHA",
79
+ "CI_PIPELINE_ID",
80
+ ),
81
+ "woodpecker-ci": _ProviderSpec(
82
+ "woodpecker-ci",
83
+ "CI_FORGE_URL",
84
+ "CI_REPO",
85
+ "CI_COMMIT_SHA",
86
+ "CI_PIPELINE_NUMBER",
87
+ ),
88
+ }
89
+
90
+
91
+ def observe_ci_context(environment: Mapping[str, str]) -> CIContextObservation:
92
+ """Observe and validate a supported provider's ordinary environment metadata."""
93
+ providers = list(_detected_providers(environment))
94
+ github_compatible = (
95
+ environment.get("GITHUB_ACTIONS") == "true"
96
+ and environment.get("FORGEJO_ACTIONS") != "true"
97
+ and environment.get("GITEA_ACTIONS") != "true"
98
+ )
99
+ if github_compatible:
100
+ if not _is_github_environment(environment):
101
+ return CIContextInvalid(
102
+ None,
103
+ "GitHub-compatible CI variables lack a provider-specific marker",
104
+ )
105
+ providers.append("github-actions")
106
+ if not providers:
107
+ return CIContextAbsent()
108
+ if len(providers) != 1:
109
+ return CIContextInvalid(None, "Multiple CI provider markers were observed")
110
+ provider = providers[0]
111
+ spec = _PROVIDERS[provider]
112
+ try:
113
+ return ObservedCIContext(
114
+ provider=provider,
115
+ server=_https_url(environment.get(spec.server_variable), "CI server"),
116
+ repository=_repository(
117
+ environment.get(spec.repository_variable), "CI repository"
118
+ ),
119
+ revision=validate_source_revision(
120
+ _text(environment.get(spec.revision_variable), "CI revision")
121
+ ),
122
+ run_id=_text(environment.get(spec.run_variable), "CI run identifier"),
123
+ )
124
+ except (InvalidInvocationError, ValueError) as exc:
125
+ return CIContextInvalid(provider, str(exc))
126
+
127
+
128
+ def _detected_providers(environment: Mapping[str, str]) -> tuple[str, ...]:
129
+ providers: list[str] = []
130
+ forgejo = environment.get("FORGEJO_ACTIONS") == "true"
131
+ gitea = environment.get("GITEA_ACTIONS") == "true"
132
+ if forgejo:
133
+ providers.append("forgejo-actions")
134
+ if gitea:
135
+ providers.append("gitea-actions")
136
+ if environment.get("GITLAB_CI") == "true":
137
+ providers.append("gitlab-ci")
138
+ if (
139
+ environment.get("CI") == "woodpecker"
140
+ or environment.get("CI_SYSTEM_NAME") == "woodpecker"
141
+ ):
142
+ providers.append("woodpecker-ci")
143
+ return tuple(providers)
144
+
145
+
146
+ def _is_github_environment(environment: Mapping[str, str]) -> bool:
147
+ server = environment.get("GITHUB_SERVER_URL", "").rstrip("/")
148
+ api = environment.get("GITHUB_API_URL", "").rstrip("/")
149
+ if server == "https://github.com":
150
+ return api == "https://api.github.com"
151
+ return bool(server) and api == f"{server}/api/v3"
152
+
153
+
154
+ def _text(value: str | None, label: str) -> str:
155
+ if (
156
+ value is None
157
+ or not value
158
+ or len(value) > 1024
159
+ or any(character.isspace() for character in value)
160
+ ):
161
+ raise ValueError(f"{label} is missing or malformed")
162
+ return value
163
+
164
+
165
+ def _repository(value: str | None, label: str) -> str:
166
+ result = _text(value, label)
167
+ components = result.split("/")
168
+ if (
169
+ len(components) < 2
170
+ or result.endswith(".git")
171
+ or any(
172
+ component in {"", ".", ".."}
173
+ or _REPOSITORY_COMPONENT.fullmatch(component) is None
174
+ for component in components
175
+ )
176
+ ):
177
+ raise ValueError(f"{label} is malformed")
178
+ return result
179
+
180
+
181
+ def _https_url(value: str | None, label: str) -> str:
182
+ result = _text(value, label)
183
+ try:
184
+ parsed = urlsplit(result)
185
+ port = parsed.port
186
+ except ValueError as exc:
187
+ raise ValueError(f"{label} is malformed") from exc
188
+ if (
189
+ parsed.scheme != "https"
190
+ or parsed.hostname is None
191
+ or parsed.username is not None
192
+ or parsed.password is not None
193
+ or parsed.query
194
+ or parsed.fragment
195
+ ):
196
+ raise ValueError(f"{label} must be a credential-free HTTPS URL")
197
+ authority = parsed.hostname.lower() + ("" if port is None else f":{port}")
198
+ return urlunsplit(("https", authority, parsed.path.rstrip("/"), "", ""))