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.
Files changed (50) hide show
  1. gitlab_compliance-2.0.0.dist-info/METADATA +92 -0
  2. gitlab_compliance-2.0.0.dist-info/RECORD +50 -0
  3. gitlab_compliance-2.0.0.dist-info/WHEEL +4 -0
  4. gitlab_compliance-2.0.0.dist-info/entry_points.txt +4 -0
  5. gitlab_compliance-2.0.0.dist-info/licenses/LICENSE +21 -0
  6. src/__init__.py +1 -0
  7. src/compliance/__init__.py +1 -0
  8. src/compliance/api_config.py +94 -0
  9. src/compliance/api_enrichment.py +52 -0
  10. src/compliance/behave_support/environment.py +43 -0
  11. src/compliance/behave_support/steps/__init__.py +0 -0
  12. src/compliance/behave_support/steps/given_steps.py +154 -0
  13. src/compliance/behave_support/steps/then_steps.py +234 -0
  14. src/compliance/behave_support/steps/when_steps.py +168 -0
  15. src/compliance/console.py +95 -0
  16. src/compliance/container_fix.py +135 -0
  17. src/compliance/gitlab_api.py +94 -0
  18. src/compliance/image_versions.py +316 -0
  19. src/compliance/include_fix.py +112 -0
  20. src/compliance/include_versions.py +250 -0
  21. src/compliance/metadata.py +245 -0
  22. src/compliance/model.py +277 -0
  23. src/compliance/models.py +29 -0
  24. src/compliance/oci_registry.py +147 -0
  25. src/compliance/policy_doc.py +177 -0
  26. src/compliance/release_cache.py +51 -0
  27. src/compliance/render.py +360 -0
  28. src/compliance/runner.py +310 -0
  29. src/compliance/stash.py +203 -0
  30. src/compliance/supply_chain_fix.py +69 -0
  31. src/gitlab_compliance.py +636 -0
  32. src/gitlab_docs.py +40 -0
  33. src/modules/__init__.py +1 -0
  34. src/modules/command_reference.py +220 -0
  35. src/modules/common.py +144 -0
  36. src/modules/constants.py +22 -0
  37. src/modules/doc_controller.py +178 -0
  38. src/modules/logging.py +36 -0
  39. src/modules/pipeline_data.py +364 -0
  40. src/modules/release.py +343 -0
  41. src/modules/swagger_html.py +408 -0
  42. src/modules/yaml_lines.py +70 -0
  43. src/modules/yaml_md_table.py +29 -0
  44. src/properties/__init__.py +0 -0
  45. src/properties/extract_job_attribute.py +76 -0
  46. src/properties/includes.py +178 -0
  47. src/properties/inputs.py +86 -0
  48. src/properties/jobs.py +167 -0
  49. src/properties/variables.py +91 -0
  50. src/properties/workflows.py +54 -0
@@ -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