gitlab-compliance 2.1.1__tar.gz → 2.3.0__tar.gz
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.1.1 → gitlab_compliance-2.3.0}/PKG-INFO +1 -1
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/pyproject.toml +2 -2
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/behave_support/environment.py +12 -0
- gitlab_compliance-2.3.0/src/compliance/behave_support/steps/script_steps.py +494 -0
- gitlab_compliance-2.3.0/src/compliance/builtin_policies/__init__.py +15 -0
- gitlab_compliance-2.3.0/src/compliance/builtin_policies/shell/shell-ci-conventions.feature +34 -0
- gitlab_compliance-2.3.0/src/compliance/builtin_policies/shell/shell-command-substitution.feature +25 -0
- gitlab_compliance-2.3.0/src/compliance/builtin_policies/shell/shell-conditionals.feature +25 -0
- gitlab_compliance-2.3.0/src/compliance/builtin_policies/shell/shell-error-handling.feature +35 -0
- gitlab_compliance-2.3.0/src/compliance/builtin_policies/shell/shell-file-operations.feature +36 -0
- gitlab_compliance-2.3.0/src/compliance/builtin_policies/shell/shell-pinning.feature +100 -0
- gitlab_compliance-2.3.0/src/compliance/builtin_policies/shell/shell-pipelines.feature +27 -0
- gitlab_compliance-2.3.0/src/compliance/builtin_policies/shell/shell-portability.feature +34 -0
- gitlab_compliance-2.3.0/src/compliance/builtin_policies/shell/shell-quoting.feature +47 -0
- gitlab_compliance-2.3.0/src/compliance/builtin_policies/shell/shell-references.feature +16 -0
- gitlab_compliance-2.3.0/src/compliance/builtin_policies/shell/shell-security.feature +52 -0
- gitlab_compliance-2.3.0/src/compliance/builtin_policies/supply-chain/image-pinning.feature +51 -0
- gitlab_compliance-2.3.0/src/compliance/builtin_policies/supply-chain/include-versions.feature +67 -0
- gitlab_compliance-2.3.0/src/compliance/builtin_policies/supply-chain/service-pinning.feature +7 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/console.py +10 -3
- gitlab_compliance-2.3.0/src/compliance/include_fetch.py +340 -0
- gitlab_compliance-2.3.0/src/compliance/include_resolution.py +116 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/merge_requests.py +1 -1
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/metadata.py +39 -9
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/model.py +51 -3
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/models.py +1 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/render.py +217 -12
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/runner.py +140 -24
- gitlab_compliance-2.3.0/src/compliance/script_analysis.py +791 -0
- gitlab_compliance-2.3.0/src/compliance/shell_render.py +632 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/gitlab_compliance.py +447 -10
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/modules/common.py +4 -5
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/modules/constants.py +17 -1
- gitlab_compliance-2.3.0/src/modules/gitlab_reference.py +54 -0
- gitlab_compliance-2.3.0/src/modules/job_composition.py +281 -0
- gitlab_compliance-2.3.0/src/modules/pipeline_data.py +868 -0
- gitlab_compliance-2.1.1/src/compliance/builtin_policies/__init__.py +0 -9
- gitlab_compliance-2.1.1/src/modules/pipeline_data.py +0 -426
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/LICENSE +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/docs/index.md +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/__init__.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/__init__.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/api_config.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/api_enrichment.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/behave_support/steps/__init__.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/behave_support/steps/given_steps.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/behave_support/steps/then_steps.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/behave_support/steps/when_steps.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/builtin_policies/baseline-jobs.feature +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/builtin_policies/component-inputs.feature +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/builtin_policies/include-pinning.feature +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/builtin_policies/variable-allowlist.feature +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/container_fix.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/gitlab_api.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/image_versions.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/include_fix.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/include_versions.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/oci_registry.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/policy_doc.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/policy_fix.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/release_cache.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/secret_redact.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/stash.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/supply_chain_fix.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/gitlab_docs.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/modules/__init__.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/modules/command_reference.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/modules/doc_controller.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/modules/gitstrings.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/modules/logging.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/modules/output_filters.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/modules/release.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/modules/swagger_html.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/modules/swagger_markdown.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/modules/yaml_lines.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/modules/yaml_md_table.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/properties/__init__.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/properties/extract_job_attribute.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/properties/includes.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/properties/inputs.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/properties/jobs.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/properties/table_render.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/properties/variables.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/properties/workflows.py +0 -0
- {gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/properties/yaml_paths.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "gitlab-compliance"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.3.0"
|
|
4
4
|
dynamic = [ "readme" ]
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -62,7 +62,7 @@ gitlab-compliance = 'src.gitlab_compliance:gitlab_compliance'
|
|
|
62
62
|
[tool.poetry.group.dev.dependencies]
|
|
63
63
|
setuptools = "83.0.0"
|
|
64
64
|
pytest = "^9.0.3"
|
|
65
|
-
pre-commit = "^4.
|
|
65
|
+
pre-commit = "^4.6.1"
|
|
66
66
|
pyyaml = "^6.0.3"
|
|
67
67
|
coverage = "^7.9.2"
|
|
68
68
|
|
{gitlab_compliance-2.1.1 → gitlab_compliance-2.3.0}/src/compliance/behave_support/environment.py
RENAMED
|
@@ -6,6 +6,15 @@ from src.compliance.model import load_pipeline_entities
|
|
|
6
6
|
from src.compliance.release_cache import ReleaseMetadataCache
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
def _resolve_external_flag(value: str | None) -> bool | None:
|
|
10
|
+
normalized = str(value or "auto").strip().lower()
|
|
11
|
+
if normalized in {"true", "yes", "on", "1"}:
|
|
12
|
+
return True
|
|
13
|
+
if normalized in {"false", "no", "off", "0"}:
|
|
14
|
+
return False
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
|
|
9
18
|
def before_all(context):
|
|
10
19
|
context.compliance_entities = {}
|
|
11
20
|
context.stash = []
|
|
@@ -42,6 +51,9 @@ def before_scenario(context, scenario):
|
|
|
42
51
|
== "true",
|
|
43
52
|
load_api_entities=context.config.userdata.get("load_api_entities", "true")
|
|
44
53
|
== "true",
|
|
54
|
+
resolve_external_includes=_resolve_external_flag(
|
|
55
|
+
context.config.userdata.get("resolve_external_includes", "auto")
|
|
56
|
+
),
|
|
45
57
|
cache=getattr(context, "release_cache", None),
|
|
46
58
|
)
|
|
47
59
|
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"""Behave steps for GitLab CI embedded shell script policies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from behave import given, then, when
|
|
6
|
+
|
|
7
|
+
from src.compliance.behave_support.environment import _skip_remaining_steps
|
|
8
|
+
from src.compliance.script_analysis import (
|
|
9
|
+
format_script_violation,
|
|
10
|
+
job_has_effective_script,
|
|
11
|
+
job_has_script_field,
|
|
12
|
+
script_bashisms_without_bash_shebang,
|
|
13
|
+
script_curl_missing_fail,
|
|
14
|
+
script_defines_functions,
|
|
15
|
+
script_downloads_without_checksum,
|
|
16
|
+
script_enables_strict_mode,
|
|
17
|
+
script_functions_mask_failures,
|
|
18
|
+
script_has_bashism_in_posix_test,
|
|
19
|
+
script_has_chmod_777,
|
|
20
|
+
script_has_dangerous_rm,
|
|
21
|
+
script_has_hardcoded_secrets,
|
|
22
|
+
script_has_nested_backticks,
|
|
23
|
+
script_has_pipefail,
|
|
24
|
+
script_has_pipeline,
|
|
25
|
+
script_has_remote_pipe_to_shell,
|
|
26
|
+
script_has_shebang,
|
|
27
|
+
script_has_unpinned_apk,
|
|
28
|
+
script_has_unpinned_apt,
|
|
29
|
+
script_has_unpinned_docker_image,
|
|
30
|
+
script_has_unpinned_go_install,
|
|
31
|
+
script_has_unpinned_npm,
|
|
32
|
+
script_has_unpinned_pip,
|
|
33
|
+
script_has_unpinned_yum,
|
|
34
|
+
script_has_unquoted_command_substitution,
|
|
35
|
+
script_has_unquoted_path_variables,
|
|
36
|
+
script_has_unquoted_test_variables,
|
|
37
|
+
script_has_unquoted_variables,
|
|
38
|
+
script_has_unresolved_references,
|
|
39
|
+
script_has_unsafe_array_expansion,
|
|
40
|
+
script_has_unverified_git_clone,
|
|
41
|
+
script_is_multiline,
|
|
42
|
+
script_masks_failures,
|
|
43
|
+
script_posix_shebang_with_bashisms,
|
|
44
|
+
script_shebang_is_valid,
|
|
45
|
+
script_uses_backticks,
|
|
46
|
+
script_uses_eval,
|
|
47
|
+
script_uses_insecure_temp_files,
|
|
48
|
+
)
|
|
49
|
+
from src.compliance.stash import filter_entities, name_starts_with
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _set_stash(context, entities: list[dict]):
|
|
53
|
+
context.stash = list(entities)
|
|
54
|
+
context.step_mode = "given"
|
|
55
|
+
context.scenario_skipped = False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _set_stash_or_skip(context, entities: list[dict], *, skip_reason: str):
|
|
59
|
+
if not entities:
|
|
60
|
+
_skip_remaining_steps(context, skip_reason)
|
|
61
|
+
return
|
|
62
|
+
_set_stash(context, entities)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _apply_filter(context, entities: list[dict], reason: str):
|
|
66
|
+
if context.scenario_skipped:
|
|
67
|
+
return
|
|
68
|
+
context.step_mode = "when"
|
|
69
|
+
if not entities:
|
|
70
|
+
_skip_remaining_steps(context, reason)
|
|
71
|
+
return
|
|
72
|
+
context.stash = entities
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _assert_none(context, predicate, message: str):
|
|
76
|
+
if context.scenario_skipped:
|
|
77
|
+
return
|
|
78
|
+
context.step_mode = "then"
|
|
79
|
+
if not context.stash:
|
|
80
|
+
raise AssertionError("No entities in stash to assert against.")
|
|
81
|
+
failures = [
|
|
82
|
+
format_script_violation(entity, message)
|
|
83
|
+
for entity in context.stash
|
|
84
|
+
if predicate(entity)
|
|
85
|
+
]
|
|
86
|
+
if failures:
|
|
87
|
+
raise AssertionError("; ".join(failures))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _assert_all_true(context, predicate, message: str):
|
|
91
|
+
if context.scenario_skipped:
|
|
92
|
+
return
|
|
93
|
+
context.step_mode = "then"
|
|
94
|
+
if not context.stash:
|
|
95
|
+
raise AssertionError("No entities in stash to assert against.")
|
|
96
|
+
failures = [
|
|
97
|
+
format_script_violation(entity, message)
|
|
98
|
+
for entity in context.stash
|
|
99
|
+
if not predicate(entity)
|
|
100
|
+
]
|
|
101
|
+
if failures:
|
|
102
|
+
raise AssertionError("; ".join(failures))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@given("I have any job with effective script defined")
|
|
106
|
+
def given_any_job_with_effective_script(context):
|
|
107
|
+
jobs = filter_entities(
|
|
108
|
+
context.compliance_entities.get("jobs", []),
|
|
109
|
+
job_has_effective_script,
|
|
110
|
+
)
|
|
111
|
+
_set_stash_or_skip(
|
|
112
|
+
context, jobs, skip_reason="No jobs with effective script found in pipeline"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@given("I have any job with before_script defined")
|
|
117
|
+
def given_any_job_with_before_script(context):
|
|
118
|
+
jobs = filter_entities(
|
|
119
|
+
context.compliance_entities.get("jobs", []),
|
|
120
|
+
lambda entity: job_has_script_field(entity, "before_script"),
|
|
121
|
+
)
|
|
122
|
+
_set_stash_or_skip(
|
|
123
|
+
context, jobs, skip_reason="No jobs with before_script found in pipeline"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@given("I have any job with script defined")
|
|
128
|
+
def given_any_job_with_script(context):
|
|
129
|
+
jobs = filter_entities(
|
|
130
|
+
context.compliance_entities.get("jobs", []),
|
|
131
|
+
lambda entity: job_has_script_field(entity, "script"),
|
|
132
|
+
)
|
|
133
|
+
_set_stash_or_skip(
|
|
134
|
+
context, jobs, skip_reason="No jobs with script found in pipeline"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@when("a variable is expanded within a command")
|
|
139
|
+
def when_variable_expanded(context):
|
|
140
|
+
filtered = filter_entities(context.stash, script_has_unquoted_variables)
|
|
141
|
+
# Keep jobs that have expansions to validate; if none, keep stash for positive pass.
|
|
142
|
+
if filtered:
|
|
143
|
+
_apply_filter(context, filtered, "No jobs with variable expansions")
|
|
144
|
+
else:
|
|
145
|
+
context.step_mode = "when"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@when("variables are used as part of a path")
|
|
149
|
+
def when_variables_in_path(context):
|
|
150
|
+
filtered = filter_entities(context.stash, script_has_unquoted_path_variables)
|
|
151
|
+
if filtered:
|
|
152
|
+
_apply_filter(context, filtered, "No jobs with path variable usage")
|
|
153
|
+
else:
|
|
154
|
+
context.step_mode = "when"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@when("command substitution is used")
|
|
158
|
+
def when_command_substitution(context):
|
|
159
|
+
filtered = filter_entities(
|
|
160
|
+
context.stash,
|
|
161
|
+
lambda e: script_has_unquoted_command_substitution(e)
|
|
162
|
+
or "$(" in str(e.get("values", {}).get("effective_script", "")),
|
|
163
|
+
)
|
|
164
|
+
if not filtered:
|
|
165
|
+
filtered = filter_entities(
|
|
166
|
+
context.stash,
|
|
167
|
+
lambda e: "$("
|
|
168
|
+
in "\n".join(
|
|
169
|
+
map(str, e.get("values", {}).get("effective_script", []) or [])
|
|
170
|
+
)
|
|
171
|
+
or "`"
|
|
172
|
+
in "\n".join(
|
|
173
|
+
map(str, e.get("values", {}).get("effective_script", []) or [])
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
if filtered:
|
|
177
|
+
_apply_filter(context, filtered, "No jobs with command substitution")
|
|
178
|
+
else:
|
|
179
|
+
context.step_mode = "when"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@when("an array is expanded")
|
|
183
|
+
def when_array_expanded(context):
|
|
184
|
+
filtered = filter_entities(context.stash, script_has_unsafe_array_expansion)
|
|
185
|
+
if filtered:
|
|
186
|
+
_apply_filter(context, filtered, "No jobs with array expansions")
|
|
187
|
+
else:
|
|
188
|
+
context.step_mode = "when"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@when("the effective script has more than 1 line")
|
|
192
|
+
def when_multiline_script(context):
|
|
193
|
+
filtered = filter_entities(context.stash, script_is_multiline)
|
|
194
|
+
_apply_filter(context, filtered, "No multi-line effective scripts")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@when("rm is used recursively")
|
|
198
|
+
def when_rm_recursive(context):
|
|
199
|
+
filtered = filter_entities(context.stash, script_has_dangerous_rm)
|
|
200
|
+
if filtered:
|
|
201
|
+
_apply_filter(context, filtered, "No recursive rm usage")
|
|
202
|
+
else:
|
|
203
|
+
context.step_mode = "when"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@when("a pipeline is used")
|
|
207
|
+
def when_pipeline_used(context):
|
|
208
|
+
filtered = filter_entities(context.stash, script_has_pipeline)
|
|
209
|
+
_apply_filter(context, filtered, "No pipelines found in scripts")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@when("temporary files are required")
|
|
213
|
+
def when_temp_files_required(context):
|
|
214
|
+
filtered = filter_entities(context.stash, script_uses_insecure_temp_files)
|
|
215
|
+
if filtered:
|
|
216
|
+
_apply_filter(context, filtered, "No temporary file usage detected")
|
|
217
|
+
else:
|
|
218
|
+
context.step_mode = "when"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@when('its name starts with "{prefix}"')
|
|
222
|
+
def when_name_starts_with_prefix(context, prefix):
|
|
223
|
+
filtered = filter_entities(context.stash, lambda e: name_starts_with(e, prefix))
|
|
224
|
+
_apply_filter(context, filtered, f"No entity names start with '{prefix}'")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@then("the variable must be wrapped in double quotes")
|
|
228
|
+
def then_variables_quoted(context):
|
|
229
|
+
_assert_none(
|
|
230
|
+
context,
|
|
231
|
+
script_has_unquoted_variables,
|
|
232
|
+
"Unquoted variable expansions found",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@then("all variables must be safely quoted")
|
|
237
|
+
def then_path_variables_quoted(context):
|
|
238
|
+
_assert_none(
|
|
239
|
+
context,
|
|
240
|
+
script_has_unquoted_path_variables,
|
|
241
|
+
"Unquoted variables used in file paths",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@then("the result must be quoted unless word splitting is intended")
|
|
246
|
+
def then_cmd_sub_quoted(context):
|
|
247
|
+
_assert_none(
|
|
248
|
+
context,
|
|
249
|
+
script_has_unquoted_command_substitution,
|
|
250
|
+
"Unquoted command substitution found",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@then('array "@" syntax must be used where appropriate')
|
|
255
|
+
def then_array_at_syntax(context):
|
|
256
|
+
_assert_none(
|
|
257
|
+
context,
|
|
258
|
+
script_has_unsafe_array_expansion,
|
|
259
|
+
"Unsafe array expansion found",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@then("the script must enable strict mode options")
|
|
264
|
+
def then_strict_mode_table(context):
|
|
265
|
+
_assert_all_true(
|
|
266
|
+
context,
|
|
267
|
+
script_enables_strict_mode,
|
|
268
|
+
"Multi-line script missing set -e/-u/pipefail",
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@then("failure must be handled explicitly")
|
|
273
|
+
def then_failure_handled(context):
|
|
274
|
+
# With strict mode, failures are handled; otherwise flag || true masking.
|
|
275
|
+
def ok(entity):
|
|
276
|
+
if script_enables_strict_mode(entity):
|
|
277
|
+
return True
|
|
278
|
+
return not script_masks_failures(entity)
|
|
279
|
+
|
|
280
|
+
_assert_all_true(context, ok, "Command failures are not handled explicitly")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@then("functions must propagate failures")
|
|
284
|
+
def then_functions_propagate(context):
|
|
285
|
+
filtered = filter_entities(context.stash, script_defines_functions)
|
|
286
|
+
if not filtered:
|
|
287
|
+
context.step_mode = "then"
|
|
288
|
+
return
|
|
289
|
+
context.stash = filtered
|
|
290
|
+
_assert_none(
|
|
291
|
+
context,
|
|
292
|
+
script_functions_mask_failures,
|
|
293
|
+
"Functions mask failures with || true",
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@then("file paths must be quoted")
|
|
298
|
+
def then_file_paths_quoted(context):
|
|
299
|
+
_assert_none(
|
|
300
|
+
context,
|
|
301
|
+
script_has_unquoted_path_variables,
|
|
302
|
+
"Unquoted file paths found",
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@then("mktemp must be used for temporary files")
|
|
307
|
+
def then_mktemp_used(context):
|
|
308
|
+
_assert_none(
|
|
309
|
+
context,
|
|
310
|
+
script_uses_insecure_temp_files,
|
|
311
|
+
"Insecure temporary file creation without mktemp",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@then("path validation must be performed for recursive rm")
|
|
316
|
+
def then_rm_protected(context):
|
|
317
|
+
_assert_none(
|
|
318
|
+
context,
|
|
319
|
+
script_has_dangerous_rm,
|
|
320
|
+
"Dangerous recursive rm without path validation",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@then("command substitution must use dollar parentheses")
|
|
325
|
+
def then_no_backticks(context):
|
|
326
|
+
_assert_none(context, script_uses_backticks, "Legacy backtick substitution found")
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@then("nested command substitution must use dollar parentheses")
|
|
330
|
+
def then_nested_dollar(context):
|
|
331
|
+
_assert_none(
|
|
332
|
+
context,
|
|
333
|
+
script_has_nested_backticks,
|
|
334
|
+
"Nested backtick substitution found",
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@then("POSIX compliant operators must be used")
|
|
339
|
+
def then_posix_operators(context):
|
|
340
|
+
_assert_none(
|
|
341
|
+
context,
|
|
342
|
+
script_has_bashism_in_posix_test,
|
|
343
|
+
"Non-portable operators in [ tests",
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@then("variables in test expressions must be quoted")
|
|
348
|
+
def then_test_vars_quoted(context):
|
|
349
|
+
_assert_none(
|
|
350
|
+
context,
|
|
351
|
+
script_has_unquoted_test_variables,
|
|
352
|
+
"Unquoted variables in test expressions",
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@then("pipefail must be enabled")
|
|
357
|
+
def then_pipefail(context):
|
|
358
|
+
_assert_all_true(
|
|
359
|
+
context,
|
|
360
|
+
lambda e: (not script_has_pipeline(e)) or script_has_pipefail(e),
|
|
361
|
+
"Pipeline used without pipefail",
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@then("eval must not be used")
|
|
366
|
+
def then_no_eval(context):
|
|
367
|
+
_assert_none(context, script_uses_eval, "eval is used in script")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@then("untrusted remote scripts must not be executed")
|
|
371
|
+
def then_no_remote_pipe(context):
|
|
372
|
+
_assert_none(
|
|
373
|
+
context,
|
|
374
|
+
script_has_remote_pipe_to_shell,
|
|
375
|
+
"Remote pipe to shell (curl|sh) detected",
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@then("user supplied variables must be quoted or validated")
|
|
380
|
+
def then_user_input_safe(context):
|
|
381
|
+
_assert_none(
|
|
382
|
+
context,
|
|
383
|
+
script_has_unquoted_variables,
|
|
384
|
+
"User/CI variables used without quoting",
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@then("hardcoded secrets must not be present")
|
|
389
|
+
def then_no_secrets(context):
|
|
390
|
+
_assert_none(
|
|
391
|
+
context,
|
|
392
|
+
script_has_hardcoded_secrets,
|
|
393
|
+
"Hardcoded secret pattern detected",
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@then("script downloads must verify checksums")
|
|
398
|
+
def then_downloads_checksum(context):
|
|
399
|
+
_assert_none(
|
|
400
|
+
context,
|
|
401
|
+
script_downloads_without_checksum,
|
|
402
|
+
"Download without nearby checksum verification",
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@then('package installs of type "{manager}" must use pinned versions')
|
|
407
|
+
def then_packages_pinned(context, manager):
|
|
408
|
+
predicates = {
|
|
409
|
+
"apk": script_has_unpinned_apk,
|
|
410
|
+
"pip": script_has_unpinned_pip,
|
|
411
|
+
"apt": script_has_unpinned_apt,
|
|
412
|
+
"yum": script_has_unpinned_yum,
|
|
413
|
+
"npm": script_has_unpinned_npm,
|
|
414
|
+
"go": script_has_unpinned_go_install,
|
|
415
|
+
}
|
|
416
|
+
predicate = predicates.get(manager)
|
|
417
|
+
if predicate is None:
|
|
418
|
+
raise AssertionError(f"Unknown package manager '{manager}'")
|
|
419
|
+
_assert_none(context, predicate, f"Unpinned {manager} package install")
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@then("docker commands must pin container images to a tag or sha256 digest")
|
|
423
|
+
def then_docker_images_pinned(context):
|
|
424
|
+
_assert_none(
|
|
425
|
+
context,
|
|
426
|
+
script_has_unpinned_docker_image,
|
|
427
|
+
"Docker command references an unpinned container image",
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@then("git clone must verify commit or tag")
|
|
432
|
+
def then_git_clone_verified(context):
|
|
433
|
+
_assert_none(
|
|
434
|
+
context,
|
|
435
|
+
script_has_unverified_git_clone,
|
|
436
|
+
"git clone without commit/tag verification",
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@then("curl must use fail flag")
|
|
441
|
+
def then_curl_fail(context):
|
|
442
|
+
_assert_none(context, script_curl_missing_fail, "curl without --fail/-f")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@then("deprecated CI_BUILD variables must not be used")
|
|
446
|
+
def then_no_ci_build(context):
|
|
447
|
+
from src.compliance.script_analysis import script_uses_deprecated_ci_build_vars
|
|
448
|
+
|
|
449
|
+
_assert_none(
|
|
450
|
+
context,
|
|
451
|
+
script_uses_deprecated_ci_build_vars,
|
|
452
|
+
"Deprecated $CI_BUILD_* variable used",
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@then("unresolved script references must not be present")
|
|
457
|
+
def then_no_unresolved_refs(context):
|
|
458
|
+
_assert_none(
|
|
459
|
+
context,
|
|
460
|
+
script_has_unresolved_references,
|
|
461
|
+
"Unresolved !reference in script blocks",
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@then("shebang must be valid when present")
|
|
466
|
+
def then_shebang_valid(context):
|
|
467
|
+
_assert_all_true(
|
|
468
|
+
context,
|
|
469
|
+
lambda e: (not script_has_shebang(e)) or script_shebang_is_valid(e),
|
|
470
|
+
"Invalid shebang in script block",
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@then("bash specific features must declare bash")
|
|
475
|
+
def then_bash_declared(context):
|
|
476
|
+
_assert_none(
|
|
477
|
+
context,
|
|
478
|
+
script_bashisms_without_bash_shebang,
|
|
479
|
+
"Bash-specific syntax without bash shebang",
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
@then("POSIX shebang scripts must not use bashisms")
|
|
484
|
+
def then_posix_no_bashisms(context):
|
|
485
|
+
_assert_none(
|
|
486
|
+
context,
|
|
487
|
+
script_posix_shebang_with_bashisms,
|
|
488
|
+
"Bashisms used with sh shebang",
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@then("chmod 777 must not be used")
|
|
493
|
+
def then_no_chmod_777(context):
|
|
494
|
+
_assert_none(context, script_has_chmod_777, "chmod 777 is used")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Bundled compliance policies shipped with gitlab-compliance."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
BUILTIN_POLICIES_DIR = os.path.join(os.path.dirname(__file__))
|
|
8
|
+
BUILTIN_SHELL_POLICIES_DIR = os.path.join(BUILTIN_POLICIES_DIR, "shell")
|
|
9
|
+
BUILTIN_SUPPLY_CHAIN_POLICIES_DIR = os.path.join(BUILTIN_POLICIES_DIR, "supply-chain")
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"BUILTIN_POLICIES_DIR",
|
|
13
|
+
"BUILTIN_SHELL_POLICIES_DIR",
|
|
14
|
+
"BUILTIN_SUPPLY_CHAIN_POLICIES_DIR",
|
|
15
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# METADATA
|
|
2
|
+
# title: GitLab CI script conventions
|
|
3
|
+
# description: CI-specific hygiene for curl and deprecated variables.
|
|
4
|
+
# custom:
|
|
5
|
+
# id: GLCI-SHELL-CI
|
|
6
|
+
# severity: MEDIUM
|
|
7
|
+
Feature: GitLab CI script conventions
|
|
8
|
+
|
|
9
|
+
# METADATA
|
|
10
|
+
# title: curl uses fail flag
|
|
11
|
+
# custom:
|
|
12
|
+
# id: GLCI-SHELL-CI-001
|
|
13
|
+
# severity: MEDIUM
|
|
14
|
+
Scenario: curl must use fail flag
|
|
15
|
+
Given I have any job with effective script defined
|
|
16
|
+
Then curl must use fail flag
|
|
17
|
+
|
|
18
|
+
# METADATA
|
|
19
|
+
# title: Deprecated CI_BUILD variables are not used
|
|
20
|
+
# custom:
|
|
21
|
+
# id: GLCI-SHELL-CI-002
|
|
22
|
+
# severity: MEDIUM
|
|
23
|
+
Scenario: Deprecated CI_BUILD variables must not be used
|
|
24
|
+
Given I have any job with effective script defined
|
|
25
|
+
Then deprecated CI_BUILD variables must not be used
|
|
26
|
+
|
|
27
|
+
# METADATA
|
|
28
|
+
# title: CI variables should be quoted
|
|
29
|
+
# custom:
|
|
30
|
+
# id: GLCI-SHELL-CI-003
|
|
31
|
+
# severity: MEDIUM
|
|
32
|
+
Scenario: CI variables should be quoted
|
|
33
|
+
Given I have any job with effective script defined
|
|
34
|
+
Then user supplied variables must be quoted or validated
|
gitlab_compliance-2.3.0/src/compliance/builtin_policies/shell/shell-command-substitution.feature
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# METADATA
|
|
2
|
+
# title: Command substitution standards
|
|
3
|
+
# description: Prefer modern dollar-parentheses substitution over backticks.
|
|
4
|
+
# custom:
|
|
5
|
+
# id: GLCI-SHELL-SUB
|
|
6
|
+
# severity: MEDIUM
|
|
7
|
+
Feature: Command substitution
|
|
8
|
+
|
|
9
|
+
# METADATA
|
|
10
|
+
# title: Legacy backticks are not used
|
|
11
|
+
# custom:
|
|
12
|
+
# id: GLCI-SHELL-SUB-001
|
|
13
|
+
# severity: MEDIUM
|
|
14
|
+
Scenario: Legacy backticks are not used
|
|
15
|
+
Given I have any job with effective script defined
|
|
16
|
+
Then command substitution must use dollar parentheses
|
|
17
|
+
|
|
18
|
+
# METADATA
|
|
19
|
+
# title: Nested command substitution is readable
|
|
20
|
+
# custom:
|
|
21
|
+
# id: GLCI-SHELL-SUB-002
|
|
22
|
+
# severity: MEDIUM
|
|
23
|
+
Scenario: Nested command substitution is readable
|
|
24
|
+
Given I have any job with effective script defined
|
|
25
|
+
Then nested command substitution must use dollar parentheses
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# METADATA
|
|
2
|
+
# title: Conditional expression standards
|
|
3
|
+
# description: Portable and safely quoted test expressions.
|
|
4
|
+
# custom:
|
|
5
|
+
# id: GLCI-SHELL-TEST
|
|
6
|
+
# severity: MEDIUM
|
|
7
|
+
Feature: Conditional expressions
|
|
8
|
+
|
|
9
|
+
# METADATA
|
|
10
|
+
# title: Test operators are portable
|
|
11
|
+
# custom:
|
|
12
|
+
# id: GLCI-SHELL-TEST-001
|
|
13
|
+
# severity: MEDIUM
|
|
14
|
+
Scenario: Test operators are portable
|
|
15
|
+
Given I have any job with effective script defined
|
|
16
|
+
Then POSIX compliant operators must be used
|
|
17
|
+
|
|
18
|
+
# METADATA
|
|
19
|
+
# title: Variables are quoted in test statements
|
|
20
|
+
# custom:
|
|
21
|
+
# id: GLCI-SHELL-TEST-002
|
|
22
|
+
# severity: HIGH
|
|
23
|
+
Scenario: Variables are quoted in test statements
|
|
24
|
+
Given I have any job with effective script defined
|
|
25
|
+
Then variables in test expressions must be quoted
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# METADATA
|
|
2
|
+
# title: Error handling
|
|
3
|
+
# description: Strict mode and exit-code handling for GitLab CI job scripts.
|
|
4
|
+
# custom:
|
|
5
|
+
# id: GLCI-SHELL-ERR
|
|
6
|
+
# severity: HIGH
|
|
7
|
+
Feature: Error handling
|
|
8
|
+
|
|
9
|
+
# METADATA
|
|
10
|
+
# title: Scripts use strict mode
|
|
11
|
+
# custom:
|
|
12
|
+
# id: GLCI-SHELL-ERR-001
|
|
13
|
+
# severity: MEDIUM
|
|
14
|
+
Scenario: Multi-line scripts use strict mode
|
|
15
|
+
Given I have any job with effective script defined
|
|
16
|
+
When the effective script has more than 1 line
|
|
17
|
+
Then the script must enable strict mode options
|
|
18
|
+
|
|
19
|
+
# METADATA
|
|
20
|
+
# title: Commands return meaningful exit codes
|
|
21
|
+
# custom:
|
|
22
|
+
# id: GLCI-SHELL-ERR-002
|
|
23
|
+
# severity: MEDIUM
|
|
24
|
+
Scenario: Commands return meaningful exit codes
|
|
25
|
+
Given I have any job with effective script defined
|
|
26
|
+
Then failure must be handled explicitly
|
|
27
|
+
|
|
28
|
+
# METADATA
|
|
29
|
+
# title: Functions propagate failures
|
|
30
|
+
# custom:
|
|
31
|
+
# id: GLCI-SHELL-ERR-003
|
|
32
|
+
# severity: MEDIUM
|
|
33
|
+
Scenario: Functions propagate failures
|
|
34
|
+
Given I have any job with effective script defined
|
|
35
|
+
Then functions must propagate failures
|