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,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitlab-compliance
3
+ Version: 2.0.0
4
+ Summary: Gitlab automatically generates gitlab documentation from yaml files
5
+ License-File: LICENSE
6
+ Author: Charlie Smith
7
+ Author-email: me@charlieasmith.co.uk
8
+ Requires-Python: >=3.12,<3.13
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Requires-Dist: behave (>=1.2.6,<2.0.0)
12
+ Requires-Dist: click (>=8.1.8,<9.0.0)
13
+ Requires-Dist: idna (==3.15)
14
+ Requires-Dist: jq (==1.8.0)
15
+ Requires-Dist: loguru (==0.7.0)
16
+ Requires-Dist: markdown-analysis (==0.1.5)
17
+ Requires-Dist: oras (==0.2.42)
18
+ Requires-Dist: oyaml
19
+ Requires-Dist: prettytable
20
+ Requires-Dist: pytablewriter (==1.2.0)
21
+ Requires-Dist: python-gitlab (==7.0.0)
22
+ Requires-Dist: pyyaml (==6.0.2)
23
+ Requires-Dist: requests (==2.34.2)
24
+ Requires-Dist: rich (==14.2.0)
25
+ Requires-Dist: semver (==3.0.2)
26
+ Requires-Dist: typer (==0.10.0)
27
+ Requires-Dist: urllib3 (==2.7.0)
28
+ Project-URL: Documentation, https://maturitybuilder.github.io/gitlab-compliance/
29
+ Project-URL: Home Page, https://maturitybuilder.github.io/gitlab-compliance/
30
+ Project-URL: Repository, https://github.com/MaturityBuilder/gitlab-compliance
31
+ Description-Content-Type: text/markdown
32
+
33
+ # gitlab-compliance
34
+
35
+ `gitlab-compliance` (PyPI package [`gitlab-compliance`](https://pypi.org/project/gitlab-compliance/)) is a lightweight, security and compliance focused test framework for GitLab CI/CD. It runs Gherkin policies against `.gitlab-ci.yml` and optional GitLab API settings — the same BDD model as [terraform-compliance](https://terraform-compliance.com/) uses for Terraform plans.
36
+
37
+ Source code: [MaturityBuilder/gitlab-compliance](https://github.com/MaturityBuilder/gitlab-compliance).
38
+
39
+ - **compliance:** Ensure pipeline YAML and project settings follow your security standards and custom policies
40
+ - **behaviour driven development:** Policies are readable Gherkin scenarios that developers and security teams share
41
+ - **portable:** Install from `pip`. See [Installation](installation/index.md)
42
+ - **pre-merge:** Validate configuration before changes land on protected branches
43
+ - **YAML and API:** Offline checks against pipeline files; optional GitLab API checks for project settings and CI variables
44
+ - **easy to integrate:** Run in GitLab CI or local git hooks
45
+ - **segregation of duty:** Keep policy packs in a separate repository or OCI registry
46
+ - **documentation:** Generate Markdown or HTML reference docs from `.gitlab-ci.yml`
47
+
48
+ ## Idea
49
+
50
+ `gitlab-compliance` focuses on [negative testing](https://en.wikipedia.org/wiki/Negative_testing) — catching misconfigurations and policy violations — rather than proving that a job runs successfully end to end.
51
+
52
+ GitLab CI pipelines are defined in YAML that composes jobs, includes, variables, and workflow rules. What was missing is a lightweight way to assert that this configuration follows organizational standards before merge. GitLab offers native compliance features in higher tiers; `gitlab-compliance` provides an open, portable alternative inspired by [terraform-compliance](https://terraform-compliance.com/) and [Conftest](https://www.conftest.dev/).
53
+
54
+ For example, a policy might require that no job uses a floating `latest` image tag:
55
+
56
+ ```
57
+ if a job defines an image, it must not use the :latest tag
58
+ ```
59
+
60
+ translates into:
61
+
62
+ ```gherkin
63
+ Given I have any job defined
64
+ When it has image
65
+ Then its image must not match ":latest$"
66
+ ```
67
+
68
+ The `image` value comes from your pipeline YAML:
69
+
70
+ ```yaml
71
+ scan:
72
+ image: python:3.12
73
+ build:
74
+ image: docker:latest # violates the policy above
75
+ ```
76
+
77
+ In CI, this scenario runs against `.gitlab-ci.yml` (and resolved local includes) so merge requests cannot introduce violations.
78
+
79
+ See [Examples](examples/index.md) for more sample use cases.
80
+
81
+ ## Supporting / Requirements
82
+
83
+ - **Python:** 3.12 (see [Installing via pip](installation/pip.md))
84
+ - **Pipeline file:** `.gitlab-ci.yml` or another path passed with `-p`
85
+ - **API checks (optional):** GitLab token plus `--project` or `--group` for settings and CI variable policies
86
+
87
+ Full CLI options: [Usage](usage/index.md). Step grammar: [BDD Reference](bdd-reference/index.md).
88
+
89
+ ## How can you support the project?
90
+
91
+ Contributions are welcome — see [Contributing](contributing.md).
92
+
@@ -0,0 +1,50 @@
1
+ src/__init__.py,sha256=mqMuQB3aqJVPrHHqJMLjqiMKUiJjozc7EPLcX5DpKHg,22
2
+ src/compliance/__init__.py,sha256=ZtqcTSIFqri7ZTe5GJyUzR3mZZI3BoxXqcAlEHITz3o,52
3
+ src/compliance/api_config.py,sha256=ZEc-kh1Apcwv_3nvP4oUF4-bggcMtgwi_GCZDTeIAJs,3010
4
+ src/compliance/api_enrichment.py,sha256=HEr8K702WQ4y28vK_Ve1q7ZROh1S8cC7jo48bjUcYIY,1424
5
+ src/compliance/behave_support/environment.py,sha256=sIXVQ6u9np1Znr85BCUGQ4n6oOMsS7mRfVAPkTwC3A8,1531
6
+ src/compliance/behave_support/steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ src/compliance/behave_support/steps/given_steps.py,sha256=sel8u3UD0D3v6YLDV1z-VPMXygQs1HZbWlHiN0EgiZY,4838
8
+ src/compliance/behave_support/steps/then_steps.py,sha256=TESTwgzEV3YXojyjEBzSKhsGl47agW8dkcvLJnzgzaQ,6951
9
+ src/compliance/behave_support/steps/when_steps.py,sha256=Pasdn2wb7I-9bxuapWdk_v37EFqQY3brfstazIwWxvQ,5234
10
+ src/compliance/console.py,sha256=oy5mjUGWr47u3zZ3J8zk8gnV0_cZX--Ht0leu6baaEQ,3311
11
+ src/compliance/container_fix.py,sha256=Pw14gpEwE2q0AkZkUcBxbG6ACP7VO2YZFaZz5_dd9Hw,4576
12
+ src/compliance/gitlab_api.py,sha256=_vt9r39Um_tUsVu_UviKPXv8EbZ3_WzgQyghnLMMlCw,2717
13
+ src/compliance/image_versions.py,sha256=t6VJ1r5HO9D1Qas4XgMsasyzZOa8SuDreQyfRTWVk8I,10193
14
+ src/compliance/include_fix.py,sha256=kRrYbu1p02_DsSrZMcF6g14n-qNIganRljzTkMjNuSA,3665
15
+ src/compliance/include_versions.py,sha256=Ju6XvvU4dk8YW0XKyJ96_o41NnZUYfsHVIBcjb0ABFY,7751
16
+ src/compliance/metadata.py,sha256=tWQoDoauUob2NDxyRn2WaI6aWPzeBrMtETDsBdV-pbc,7217
17
+ src/compliance/model.py,sha256=6sWbYpBnzJnjo1PR72VYfQW4VpRk_7Bbh_0mXpISdxk,9655
18
+ src/compliance/models.py,sha256=Rfm-aKCqLad9QySyar9xrXwICA0e1EZewkm7aACDIzs,560
19
+ src/compliance/oci_registry.py,sha256=s-IXIGpLRH9_90W73liyoBZnKk51C9F_iTNiIKM1-T0,4890
20
+ src/compliance/policy_doc.py,sha256=mYv08eC2KmXGLpIWWEn3PfL7qKyNaa_ltHTdPjIanMM,6328
21
+ src/compliance/release_cache.py,sha256=LQxCioYRgrGBXMfzWUfnk6Rc65qG1goMK1oqMjW4qYc,1903
22
+ src/compliance/render.py,sha256=fC68PZe2gmpQySMxeocaY7JeMW1SsOqs9vOeIq6BCU4,12537
23
+ src/compliance/runner.py,sha256=erDcZhQfDGRiiuAsSYDdc7w_Os9wFZo-hwr5U68apUQ,10791
24
+ src/compliance/stash.py,sha256=Nb15Wwjt6AsOoazKxYYGAQgD2VqFns4vjqZYCxPa9EA,6311
25
+ src/compliance/supply_chain_fix.py,sha256=1yHdsEyFTxBR108V63S40O6mLpstqgs1p203YQcMEPc,2046
26
+ src/gitlab_compliance.py,sha256=iLwi4kHmbXSRCB0HA5dPhs7JXSaSW6wI6oUx52LhSw8,17549
27
+ src/gitlab_docs.py,sha256=3OZ7w29IP72HtZxDxgEpBu6B_ER7B5bjmqy5sMaAfQY,994
28
+ src/modules/__init__.py,sha256=oSKhQHo_8dYEVv3A19nCmQysoh4TbOzHl508xX9iHoo,23
29
+ src/modules/command_reference.py,sha256=IwV733I67WnoUGGQsklhPeAXPEA_oJK3vJ3n_RurGP0,6452
30
+ src/modules/common.py,sha256=LAXF15up9zOOa3kiKlPIfB_OqAPryg6wdhuTQPQXIeA,3706
31
+ src/modules/constants.py,sha256=Td-hzTzUYuSvSRMvgqUojIZPgTiWSZ2V7UbyCG9qZ5c,617
32
+ src/modules/doc_controller.py,sha256=hhkPbvKKyLKmxjsL3tXWHc9fOWdrCozdC-z303i5HNM,6281
33
+ src/modules/logging.py,sha256=lKLi2r1Ma29bnFGc7VEw_VGVPnB5i2zCoJiKVC0VDOU,932
34
+ src/modules/pipeline_data.py,sha256=ysLFIZXeEt2V5Y1PcyXFN3lWmqtuIuaN1DFKOTJkGzM,12308
35
+ src/modules/release.py,sha256=7Gdms_KU4HhweXFAAS9x9-UjB-wauXDlMBiKFVgJj8s,10681
36
+ src/modules/swagger_html.py,sha256=BwKemuQ12T8UGVokRNQQhZQVGHlicEkfOuyiAH5DuDc,12195
37
+ src/modules/yaml_lines.py,sha256=FsQFKiusi_dDZO6ZLSzfOvvPg7c1SBFQmw433k5GCV4,2402
38
+ src/modules/yaml_md_table.py,sha256=jcBBJupTQkD6ZXvTk_uiI3TS3zXFlTvKBPbeTW2jI8k,1155
39
+ src/properties/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
+ src/properties/extract_job_attribute.py,sha256=ZydTsP_ENE5n-U7hQEdUGJQtwa38cXBoHZuudYsEILI,2172
41
+ src/properties/includes.py,sha256=gvRrJONvUN-AuKNyd-ZLemDxi_SGA1eRy-rJWOVeeQo,7188
42
+ src/properties/inputs.py,sha256=csRcl62eXDD2BVYSUoxBL0QcJMCo5ptSXnaNsKY6ATg,3513
43
+ src/properties/jobs.py,sha256=w_jfGkXobQ_DzZoHwbjsDPmv1mzyhDAcY440aJW6QKs,7870
44
+ src/properties/variables.py,sha256=7GZThGowq3aHIotrnRNfna47pNvxK4ZhMsaKD6pSK2A,3911
45
+ src/properties/workflows.py,sha256=Qv_iXag5RXsFjFoBVAX7tEohhB1v4fAuAazSWxlE4MU,2089
46
+ gitlab_compliance-2.0.0.dist-info/METADATA,sha256=Se9O7G5ZjGMQbqCQwbsw6gKIjzHt1KkLsULI_ZQyxWs,4211
47
+ gitlab_compliance-2.0.0.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
48
+ gitlab_compliance-2.0.0.dist-info/entry_points.txt,sha256=_ZeapNtwblG_VkP-YrYRzFzK51Gq-mauZGf9P5HBAiI,129
49
+ gitlab_compliance-2.0.0.dist-info/licenses/LICENSE,sha256=Vs5PryRiGYf-ga-K4faB-K2Xn2cUr0z6UMUEkA7oOW4,1103
50
+ gitlab_compliance-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.4.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ gitlab-compliance=src.gitlab_compliance:gitlab_compliance
3
+ gitlab-docs=src.gitlab_compliance:gitlab_compliance
4
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2024] [Charles Smith] [DevOps Contracting Limited]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
src/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.6"
@@ -0,0 +1 @@
1
+ """GitLab CI/CD configuration compliance engine."""
@@ -0,0 +1,94 @@
1
+ """Resolve GitLab API connection settings for compliance checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any, Literal
7
+
8
+ ApiScope = Literal["project", "group"]
9
+
10
+
11
+ def _userdata_value(userdata: dict[str, Any], key: str) -> str | None:
12
+ value = userdata.get(key)
13
+ if value:
14
+ return str(value)
15
+ return None
16
+
17
+
18
+ def resolve_token(userdata: dict[str, Any] | None = None) -> str | None:
19
+ # Tokens must not be passed through Behave userdata (may appear in logs/reports).
20
+ return os.getenv("GITLAB_TOKEN") or os.getenv("CI_JOB_TOKEN")
21
+
22
+
23
+ def resolve_project(userdata: dict[str, Any] | None = None) -> str | None:
24
+ userdata = userdata or {}
25
+ return _userdata_value(userdata, "project") or os.getenv("CI_PROJECT_PATH")
26
+
27
+
28
+ def resolve_group(userdata: dict[str, Any] | None = None) -> str | None:
29
+ userdata = userdata or {}
30
+ return _userdata_value(userdata, "group") or os.getenv("GITLAB_GROUP_PATH")
31
+
32
+
33
+ def project_api_ready(userdata: dict[str, Any] | None = None) -> bool:
34
+ return bool(resolve_token(userdata) and resolve_project(userdata))
35
+
36
+
37
+ def group_api_ready(userdata: dict[str, Any] | None = None) -> bool:
38
+ return bool(resolve_token(userdata) and resolve_group(userdata))
39
+
40
+
41
+ def gitlab_api_ready(userdata: dict[str, Any] | None = None) -> bool:
42
+ return bool(resolve_token(userdata))
43
+
44
+
45
+ def _missing_connection_message(scope: ApiScope, label: str) -> str:
46
+ if scope == "project":
47
+ return (
48
+ f"GitLab API connection info not provided for {label}. "
49
+ "Set --token and --project (or GITLAB_TOKEN/CI_JOB_TOKEN and CI_PROJECT_PATH)."
50
+ )
51
+ return (
52
+ f"GitLab API connection info not provided for {label}. "
53
+ "Set --token and --group (or GITLAB_TOKEN/CI_JOB_TOKEN)."
54
+ )
55
+
56
+
57
+ def _missing_gitlab_api_message(label: str) -> str:
58
+ return (
59
+ f"GitLab API token not provided for {label}. "
60
+ "Set --token or GITLAB_TOKEN/CI_JOB_TOKEN."
61
+ )
62
+
63
+
64
+ def require_gitlab_api(context, label: str) -> bool:
65
+ userdata = context.config.userdata
66
+ if gitlab_api_ready(userdata):
67
+ return True
68
+
69
+ message = _missing_gitlab_api_message(label)
70
+ if userdata.get("strict") == "true":
71
+ raise AssertionError(message)
72
+
73
+ from src.compliance.behave_support.environment import _skip_remaining_steps
74
+
75
+ _skip_remaining_steps(context, message)
76
+ return False
77
+
78
+
79
+ def require_api_connection(context, scope: ApiScope, label: str) -> bool:
80
+ userdata = context.config.userdata
81
+ ready = (
82
+ project_api_ready(userdata) if scope == "project" else group_api_ready(userdata)
83
+ )
84
+ if ready:
85
+ return True
86
+
87
+ message = _missing_connection_message(scope, label)
88
+ if userdata.get("strict") == "true":
89
+ raise AssertionError(message)
90
+
91
+ from src.compliance.behave_support.environment import _skip_remaining_steps
92
+
93
+ _skip_remaining_steps(context, message)
94
+ return False
@@ -0,0 +1,52 @@
1
+ """Detect which API-backed enrichment steps a policy pack requires."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass
10
+ class ApiEnrichmentRequirements:
11
+ enrich_includes: bool = False
12
+ enrich_images: bool = False
13
+ load_api_entities: bool = False
14
+
15
+
16
+ INCLUDE_MARKERS = (
17
+ "include with release metadata",
18
+ "newer release",
19
+ "within the latest",
20
+ )
21
+
22
+ IMAGE_MARKERS = (
23
+ "container image with release metadata",
24
+ "registry latest",
25
+ )
26
+
27
+ API_ENTITY_MARKERS = (
28
+ "project setting",
29
+ "project ci variable",
30
+ "group setting",
31
+ )
32
+
33
+
34
+ def policies_require_api_enrichment(features_dir: str) -> ApiEnrichmentRequirements:
35
+ reqs = ApiEnrichmentRequirements()
36
+ if not os.path.isdir(features_dir):
37
+ return reqs
38
+
39
+ combined: list[str] = []
40
+ for root, _dirs, files in os.walk(features_dir):
41
+ for filename in files:
42
+ if not filename.endswith(".feature"):
43
+ continue
44
+ feature_path = os.path.join(root, filename)
45
+ with open(feature_path, encoding="utf-8") as handle:
46
+ combined.append(handle.read().lower())
47
+
48
+ text = "\n".join(combined)
49
+ reqs.enrich_includes = any(marker in text for marker in INCLUDE_MARKERS)
50
+ reqs.enrich_images = any(marker in text for marker in IMAGE_MARKERS)
51
+ reqs.load_api_entities = any(marker in text for marker in API_ENTITY_MARKERS)
52
+ return reqs
@@ -0,0 +1,43 @@
1
+ """Behave environment for GitLab compliance policies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from src.compliance.model import load_pipeline_entities
6
+
7
+
8
+ def before_all(context):
9
+ context.compliance_entities = {}
10
+ context.stash = []
11
+ context.step_mode = None
12
+ context.scenario_skipped = False
13
+
14
+
15
+ def before_scenario(context, scenario):
16
+ context.stash = []
17
+ context.step_mode = None
18
+ context.scenario_skipped = False
19
+
20
+ pipeline = context.config.userdata.get("pipeline")
21
+ if not pipeline:
22
+ raise RuntimeError("Pipeline file not configured for compliance run.")
23
+
24
+ if not context.compliance_entities:
25
+ context.compliance_entities = load_pipeline_entities(
26
+ pipeline_file=pipeline,
27
+ include_nested=context.config.userdata.get("include_nested", "true")
28
+ == "true",
29
+ gitlab_url=context.config.userdata.get("gitlab_url") or None,
30
+ project=context.config.userdata.get("project") or None,
31
+ group=context.config.userdata.get("group") or None,
32
+ enrich_includes=context.config.userdata.get("enrich_includes", "true")
33
+ == "true",
34
+ enrich_images=context.config.userdata.get("enrich_images", "true")
35
+ == "true",
36
+ load_api_entities=context.config.userdata.get("load_api_entities", "true")
37
+ == "true",
38
+ )
39
+
40
+
41
+ def _skip_remaining_steps(context, reason: str):
42
+ context.scenario_skipped = True
43
+ context.scenario.skip(reason)
File without changes
@@ -0,0 +1,154 @@
1
+ """GIVEN steps for GitLab compliance policies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from behave import given
6
+
7
+ from src.compliance.api_config import require_api_connection, require_gitlab_api
8
+ from src.compliance.behave_support.environment import _skip_remaining_steps
9
+ from src.compliance.stash import (
10
+ container_image_release_metadata_resolved,
11
+ filter_entities,
12
+ include_release_metadata_resolved,
13
+ )
14
+
15
+
16
+ def _set_stash(context, entities: list[dict]):
17
+ context.stash = list(entities)
18
+ context.step_mode = "given"
19
+ context.scenario_skipped = False
20
+
21
+
22
+ def _set_stash_or_skip(context, entities: list[dict], *, skip_reason: str):
23
+ if not entities:
24
+ _skip_remaining_steps(context, skip_reason)
25
+ return
26
+ _set_stash(context, entities)
27
+
28
+
29
+ @given("I have any job defined")
30
+ def given_any_job(context):
31
+ _set_stash(context, context.compliance_entities.get("jobs", []))
32
+
33
+
34
+ @given('I have job "{name}" defined')
35
+ def given_named_job(context, name):
36
+ jobs = [
37
+ job
38
+ for job in context.compliance_entities.get("jobs", [])
39
+ if job.get("name") == name
40
+ ]
41
+ _set_stash_or_skip(
42
+ context, jobs, skip_reason=f'No job named "{name}" found in pipeline'
43
+ )
44
+
45
+
46
+ @given("I have any include defined")
47
+ def given_any_include(context):
48
+ _set_stash(context, context.compliance_entities.get("includes", []))
49
+
50
+
51
+ @given('I have include type "{include_type}" defined')
52
+ def given_include_type(context, include_type):
53
+ includes = filter_entities(
54
+ context.compliance_entities.get("includes", []),
55
+ lambda entity: entity.get("include_type") == include_type,
56
+ )
57
+ _set_stash_or_skip(
58
+ context,
59
+ includes,
60
+ skip_reason=f'No includes of type "{include_type}" found in pipeline',
61
+ )
62
+
63
+
64
+ @given("I have any include with release metadata defined")
65
+ def given_any_include_with_release_metadata(context):
66
+ if not require_gitlab_api(context, "include release checks"):
67
+ return
68
+ includes = filter_entities(
69
+ context.compliance_entities.get("includes", []),
70
+ include_release_metadata_resolved,
71
+ )
72
+ _set_stash_or_skip(
73
+ context,
74
+ includes,
75
+ skip_reason="No includes with release metadata resolved",
76
+ )
77
+
78
+
79
+ @given("I have any container image defined")
80
+ def given_any_container_image(context):
81
+ _set_stash(context, context.compliance_entities.get("container_images", []))
82
+
83
+
84
+ @given('I have container image from "{image_source}" defined')
85
+ def given_container_image_from_source(context, image_source):
86
+ images = filter_entities(
87
+ context.compliance_entities.get("container_images", []),
88
+ lambda entity: entity.get("image_source") == image_source,
89
+ )
90
+ _set_stash_or_skip(
91
+ context,
92
+ images,
93
+ skip_reason=f'No container images from "{image_source}" found in pipeline',
94
+ )
95
+
96
+
97
+ @given("I have any container image with release metadata defined")
98
+ def given_any_container_image_with_release_metadata(context):
99
+ images = filter_entities(
100
+ context.compliance_entities.get("container_images", []),
101
+ container_image_release_metadata_resolved,
102
+ )
103
+ _set_stash_or_skip(
104
+ context,
105
+ images,
106
+ skip_reason="No container images with release metadata resolved",
107
+ )
108
+
109
+
110
+ @given("I have any variable defined")
111
+ def given_any_variable(context):
112
+ _set_stash(context, context.compliance_entities.get("variables", []))
113
+
114
+
115
+ @given("I have any workflow rule defined")
116
+ def given_any_workflow_rule(context):
117
+ _set_stash(context, context.compliance_entities.get("workflow_rules", []))
118
+
119
+
120
+ @given("I have any project setting defined")
121
+ def given_any_project_setting(context):
122
+ if not require_api_connection(context, "project", "project setting checks"):
123
+ return
124
+ _set_stash(context, context.compliance_entities.get("project_settings", []))
125
+
126
+
127
+ @given('I have project setting "{name}" defined')
128
+ def given_project_setting(context, name):
129
+ if not require_api_connection(context, "project", "project setting checks"):
130
+ return
131
+ settings = [
132
+ setting
133
+ for setting in context.compliance_entities.get("project_settings", [])
134
+ if setting.get("name") == name
135
+ ]
136
+ _set_stash_or_skip(
137
+ context,
138
+ settings,
139
+ skip_reason=f'No project setting "{name}" found',
140
+ )
141
+
142
+
143
+ @given("I have any project ci variable defined")
144
+ def given_any_project_ci_variable(context):
145
+ if not require_api_connection(context, "project", "project CI variable checks"):
146
+ return
147
+ _set_stash(context, context.compliance_entities.get("project_ci_variables", []))
148
+
149
+
150
+ @given("I have any group setting defined")
151
+ def given_any_group_setting(context):
152
+ if not require_api_connection(context, "group", "group setting checks"):
153
+ return
154
+ _set_stash(context, context.compliance_entities.get("group_settings", []))