gitlab-compliance 2.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.
- gitlab_compliance-2.0.0.dist-info/METADATA +92 -0
- gitlab_compliance-2.0.0.dist-info/RECORD +50 -0
- gitlab_compliance-2.0.0.dist-info/WHEEL +4 -0
- gitlab_compliance-2.0.0.dist-info/entry_points.txt +4 -0
- gitlab_compliance-2.0.0.dist-info/licenses/LICENSE +21 -0
- src/__init__.py +1 -0
- src/compliance/__init__.py +1 -0
- src/compliance/api_config.py +94 -0
- src/compliance/api_enrichment.py +52 -0
- src/compliance/behave_support/environment.py +43 -0
- src/compliance/behave_support/steps/__init__.py +0 -0
- src/compliance/behave_support/steps/given_steps.py +154 -0
- src/compliance/behave_support/steps/then_steps.py +234 -0
- src/compliance/behave_support/steps/when_steps.py +168 -0
- src/compliance/console.py +95 -0
- src/compliance/container_fix.py +135 -0
- src/compliance/gitlab_api.py +94 -0
- src/compliance/image_versions.py +316 -0
- src/compliance/include_fix.py +112 -0
- src/compliance/include_versions.py +250 -0
- src/compliance/metadata.py +245 -0
- src/compliance/model.py +277 -0
- src/compliance/models.py +29 -0
- src/compliance/oci_registry.py +147 -0
- src/compliance/policy_doc.py +177 -0
- src/compliance/release_cache.py +51 -0
- src/compliance/render.py +360 -0
- src/compliance/runner.py +310 -0
- src/compliance/stash.py +203 -0
- src/compliance/supply_chain_fix.py +69 -0
- src/gitlab_compliance.py +636 -0
- src/gitlab_docs.py +40 -0
- src/modules/__init__.py +1 -0
- src/modules/command_reference.py +220 -0
- src/modules/common.py +144 -0
- src/modules/constants.py +22 -0
- src/modules/doc_controller.py +178 -0
- src/modules/logging.py +36 -0
- src/modules/pipeline_data.py +364 -0
- src/modules/release.py +343 -0
- src/modules/swagger_html.py +408 -0
- src/modules/yaml_lines.py +70 -0
- src/modules/yaml_md_table.py +29 -0
- src/properties/__init__.py +0 -0
- src/properties/extract_job_attribute.py +76 -0
- src/properties/includes.py +178 -0
- src/properties/inputs.py +86 -0
- src/properties/jobs.py +167 -0
- src/properties/variables.py +91 -0
- src/properties/workflows.py +54 -0
src/compliance/stash.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Entity stash helpers for compliance scenarios."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def normalize_value(value: Any) -> str:
|
|
11
|
+
if value is None:
|
|
12
|
+
return ""
|
|
13
|
+
if isinstance(value, bool):
|
|
14
|
+
return str(value).lower()
|
|
15
|
+
if isinstance(value, (list, dict)):
|
|
16
|
+
return str(value)
|
|
17
|
+
return str(value)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_property(entity: dict, property_name: str) -> Any:
|
|
21
|
+
if property_name in entity and property_name not in ("values", "type", "address"):
|
|
22
|
+
return entity[property_name]
|
|
23
|
+
|
|
24
|
+
values = entity.get("values", {})
|
|
25
|
+
if not isinstance(values, dict):
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
value = values.get(property_name)
|
|
29
|
+
if property_name == "image" and isinstance(value, dict):
|
|
30
|
+
return value.get("name", str(value))
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def entity_has_property(entity: dict, property_name: str) -> bool:
|
|
35
|
+
value = get_property(entity, property_name)
|
|
36
|
+
if value is None:
|
|
37
|
+
return False
|
|
38
|
+
if isinstance(value, (list, dict)):
|
|
39
|
+
return len(value) > 0
|
|
40
|
+
return str(value).strip() != ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def property_matches(entity: dict, property_name: str, expected: str) -> bool:
|
|
44
|
+
return normalize_value(get_property(entity, property_name)) == expected
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def property_matches_regex(entity: dict, property_name: str, pattern: str) -> bool:
|
|
48
|
+
value = normalize_value(get_property(entity, property_name))
|
|
49
|
+
try:
|
|
50
|
+
return re.search(pattern, value) is not None
|
|
51
|
+
except re.error as exc:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"Invalid regex pattern for property '{property_name}': {pattern}"
|
|
54
|
+
) from exc
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def property_not_matches_regex(entity: dict, property_name: str, pattern: str) -> bool:
|
|
58
|
+
return not property_matches_regex(entity, property_name, pattern)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def name_starts_with(entity: dict, prefix: str) -> bool:
|
|
62
|
+
name = entity.get("name", "")
|
|
63
|
+
return str(name).startswith(prefix)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def extends_includes(entity: dict, template: str) -> bool:
|
|
67
|
+
extends = get_property(entity, "extends")
|
|
68
|
+
if isinstance(extends, str):
|
|
69
|
+
return extends == template
|
|
70
|
+
if isinstance(extends, list):
|
|
71
|
+
return template in extends
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def include_has_valid_semver(entity: dict) -> bool:
|
|
76
|
+
return bool(entity.get("valid_version"))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def include_has_newer_release(entity: dict) -> bool:
|
|
80
|
+
return bool(entity.get("update_available"))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def include_version_is_latest(entity: dict) -> bool:
|
|
84
|
+
if not entity.get("release_metadata_resolved"):
|
|
85
|
+
return False
|
|
86
|
+
version = normalize_value(entity.get("version", ""))
|
|
87
|
+
latest = normalize_value(entity.get("latest_version", ""))
|
|
88
|
+
return bool(version and latest and version == latest)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def include_release_metadata_resolved(entity: dict) -> bool:
|
|
92
|
+
return bool(entity.get("release_metadata_resolved"))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def include_newer_release_older_than_days(entity: dict, days: int) -> bool:
|
|
96
|
+
if not entity.get("update_available"):
|
|
97
|
+
return False
|
|
98
|
+
age_days = entity.get("latest_release_age_days")
|
|
99
|
+
if age_days is None:
|
|
100
|
+
return False
|
|
101
|
+
return int(age_days) > days
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def include_release_lag_exceeds_days(entity: dict, days: int) -> bool:
|
|
105
|
+
if not entity.get("update_available"):
|
|
106
|
+
return False
|
|
107
|
+
lag_days = entity.get("release_lag_days")
|
|
108
|
+
if lag_days is None:
|
|
109
|
+
return False
|
|
110
|
+
return int(lag_days) > days
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def include_within_latest_tags(entity: dict, count: int) -> bool:
|
|
114
|
+
if not entity.get("release_metadata_resolved"):
|
|
115
|
+
return False
|
|
116
|
+
rank = entity.get("version_tag_rank")
|
|
117
|
+
if rank is None:
|
|
118
|
+
return False
|
|
119
|
+
return int(rank) <= count
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def include_not_within_latest_tags(entity: dict, count: int) -> bool:
|
|
123
|
+
if not entity.get("release_metadata_resolved"):
|
|
124
|
+
return False
|
|
125
|
+
rank = entity.get("version_tag_rank")
|
|
126
|
+
if rank is None:
|
|
127
|
+
return True
|
|
128
|
+
return int(rank) > count
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def container_image_uses_sha256(entity: dict) -> bool:
|
|
132
|
+
image = str(entity.get("image", entity.get("project", "")))
|
|
133
|
+
return "@sha256:" in image
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def container_image_has_newer_release(entity: dict) -> bool:
|
|
137
|
+
return bool(entity.get("update_available"))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def container_image_newer_release_older_than_days(entity: dict, days: int) -> bool:
|
|
141
|
+
if not entity.get("update_available"):
|
|
142
|
+
return False
|
|
143
|
+
age_days = entity.get("latest_release_age_days")
|
|
144
|
+
if age_days is None:
|
|
145
|
+
return False
|
|
146
|
+
return int(age_days) > days
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def container_image_release_lag_exceeds_days(entity: dict, days: int) -> bool:
|
|
150
|
+
if not entity.get("update_available"):
|
|
151
|
+
return False
|
|
152
|
+
lag_days = entity.get("release_lag_days")
|
|
153
|
+
if lag_days is None:
|
|
154
|
+
return False
|
|
155
|
+
return int(lag_days) > days
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def container_image_within_latest_tags(entity: dict, count: int) -> bool:
|
|
159
|
+
if not entity.get("release_metadata_resolved"):
|
|
160
|
+
return False
|
|
161
|
+
rank = entity.get("version_tag_rank")
|
|
162
|
+
if rank is None:
|
|
163
|
+
return False
|
|
164
|
+
return int(rank) <= count
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def container_image_not_within_latest_tags(entity: dict, count: int) -> bool:
|
|
168
|
+
if not entity.get("release_metadata_resolved"):
|
|
169
|
+
return False
|
|
170
|
+
rank = entity.get("version_tag_rank")
|
|
171
|
+
if rank is None:
|
|
172
|
+
return True
|
|
173
|
+
return int(rank) > count
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def container_image_release_metadata_resolved(entity: dict) -> bool:
|
|
177
|
+
return bool(entity.get("release_metadata_resolved"))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def filter_entities(
|
|
181
|
+
entities: list[dict], predicate: Callable[[dict], bool]
|
|
182
|
+
) -> list[dict]:
|
|
183
|
+
return [entity for entity in entities if predicate(entity)]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def format_entity_ref(entity: dict) -> str:
|
|
187
|
+
address = entity.get("address", entity.get("name", "unknown"))
|
|
188
|
+
source_file = entity.get("source_file", "")
|
|
189
|
+
line = entity.get("line", 0)
|
|
190
|
+
if source_file and line:
|
|
191
|
+
display_path = os.path.relpath(source_file)
|
|
192
|
+
return f"{address} ({display_path}:{line})"
|
|
193
|
+
return address
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def assert_all(
|
|
197
|
+
entities: list[dict], predicate: Callable[[dict], bool], message: str
|
|
198
|
+
) -> None:
|
|
199
|
+
failures = [
|
|
200
|
+
format_entity_ref(entity) for entity in entities if not predicate(entity)
|
|
201
|
+
]
|
|
202
|
+
if failures:
|
|
203
|
+
raise AssertionError(f"{message}: {', '.join(failures)}")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Apply supply-chain auto-fixes before compliance runs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from src.compliance.container_fix import (
|
|
6
|
+
apply_container_image_fixes,
|
|
7
|
+
collect_container_image_fixes,
|
|
8
|
+
)
|
|
9
|
+
from src.compliance.include_fix import (
|
|
10
|
+
apply_include_version_fixes,
|
|
11
|
+
collect_include_version_fixes,
|
|
12
|
+
)
|
|
13
|
+
from src.compliance.model import load_pipeline_entities
|
|
14
|
+
from src.modules.logging import logger
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def apply_supply_chain_fixes(
|
|
18
|
+
*,
|
|
19
|
+
pipeline_file: str,
|
|
20
|
+
include_nested: bool,
|
|
21
|
+
gitlab_url: str | None,
|
|
22
|
+
token: str,
|
|
23
|
+
project: str | None,
|
|
24
|
+
group: str | None,
|
|
25
|
+
) -> list[str]:
|
|
26
|
+
entities = load_pipeline_entities(
|
|
27
|
+
pipeline_file=pipeline_file,
|
|
28
|
+
include_nested=include_nested,
|
|
29
|
+
gitlab_url=gitlab_url,
|
|
30
|
+
token=token,
|
|
31
|
+
project=project,
|
|
32
|
+
group=group,
|
|
33
|
+
enrich_includes=True,
|
|
34
|
+
enrich_images=True,
|
|
35
|
+
load_api_entities=False,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
messages: list[str] = []
|
|
39
|
+
include_fixes = collect_include_version_fixes(entities)
|
|
40
|
+
applied_includes = apply_include_version_fixes(include_fixes)
|
|
41
|
+
for fix in applied_includes:
|
|
42
|
+
messages.append(
|
|
43
|
+
f"Fixed include {fix.project}: {fix.current_version} -> {fix.latest_version} "
|
|
44
|
+
f"({fix.source_file}:{fix.line})"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if applied_includes:
|
|
48
|
+
entities = load_pipeline_entities(
|
|
49
|
+
pipeline_file=pipeline_file,
|
|
50
|
+
include_nested=include_nested,
|
|
51
|
+
gitlab_url=gitlab_url,
|
|
52
|
+
token=token,
|
|
53
|
+
project=project,
|
|
54
|
+
group=group,
|
|
55
|
+
enrich_includes=True,
|
|
56
|
+
enrich_images=True,
|
|
57
|
+
load_api_entities=False,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
image_fixes = collect_container_image_fixes(entities)
|
|
61
|
+
for fix in apply_container_image_fixes(image_fixes):
|
|
62
|
+
messages.append(
|
|
63
|
+
f"Fixed image {fix.current_image} -> {fix.fixed_image} "
|
|
64
|
+
f"({fix.image_source} {fix.parent_job}, {fix.source_file}:{fix.line})"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
for message in messages:
|
|
68
|
+
logger.info(message)
|
|
69
|
+
return messages
|