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
|
@@ -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,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", []))
|