gitlab-compliance 2.1.1__tar.gz → 2.2.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.
Files changed (81) hide show
  1. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/PKG-INFO +1 -1
  2. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/pyproject.toml +2 -2
  3. gitlab_compliance-2.2.0/src/compliance/behave_support/steps/script_steps.py +494 -0
  4. gitlab_compliance-2.2.0/src/compliance/builtin_policies/__init__.py +15 -0
  5. gitlab_compliance-2.2.0/src/compliance/builtin_policies/shell/shell-ci-conventions.feature +34 -0
  6. gitlab_compliance-2.2.0/src/compliance/builtin_policies/shell/shell-command-substitution.feature +25 -0
  7. gitlab_compliance-2.2.0/src/compliance/builtin_policies/shell/shell-conditionals.feature +25 -0
  8. gitlab_compliance-2.2.0/src/compliance/builtin_policies/shell/shell-error-handling.feature +35 -0
  9. gitlab_compliance-2.2.0/src/compliance/builtin_policies/shell/shell-file-operations.feature +36 -0
  10. gitlab_compliance-2.2.0/src/compliance/builtin_policies/shell/shell-pinning.feature +100 -0
  11. gitlab_compliance-2.2.0/src/compliance/builtin_policies/shell/shell-pipelines.feature +27 -0
  12. gitlab_compliance-2.2.0/src/compliance/builtin_policies/shell/shell-portability.feature +34 -0
  13. gitlab_compliance-2.2.0/src/compliance/builtin_policies/shell/shell-quoting.feature +47 -0
  14. gitlab_compliance-2.2.0/src/compliance/builtin_policies/shell/shell-references.feature +16 -0
  15. gitlab_compliance-2.2.0/src/compliance/builtin_policies/shell/shell-security.feature +52 -0
  16. gitlab_compliance-2.2.0/src/compliance/builtin_policies/supply-chain/image-pinning.feature +51 -0
  17. gitlab_compliance-2.2.0/src/compliance/builtin_policies/supply-chain/include-versions.feature +67 -0
  18. gitlab_compliance-2.2.0/src/compliance/builtin_policies/supply-chain/service-pinning.feature +7 -0
  19. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/console.py +10 -3
  20. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/merge_requests.py +1 -1
  21. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/metadata.py +39 -9
  22. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/model.py +31 -0
  23. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/runner.py +111 -23
  24. gitlab_compliance-2.2.0/src/compliance/script_analysis.py +791 -0
  25. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/gitlab_compliance.py +360 -6
  26. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/modules/common.py +4 -5
  27. gitlab_compliance-2.2.0/src/modules/gitlab_reference.py +54 -0
  28. gitlab_compliance-2.2.0/src/modules/job_composition.py +230 -0
  29. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/modules/pipeline_data.py +70 -3
  30. gitlab_compliance-2.1.1/src/compliance/builtin_policies/__init__.py +0 -9
  31. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/LICENSE +0 -0
  32. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/docs/index.md +0 -0
  33. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/__init__.py +0 -0
  34. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/__init__.py +0 -0
  35. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/api_config.py +0 -0
  36. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/api_enrichment.py +0 -0
  37. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/behave_support/environment.py +0 -0
  38. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/behave_support/steps/__init__.py +0 -0
  39. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/behave_support/steps/given_steps.py +0 -0
  40. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/behave_support/steps/then_steps.py +0 -0
  41. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/behave_support/steps/when_steps.py +0 -0
  42. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/builtin_policies/baseline-jobs.feature +0 -0
  43. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/builtin_policies/component-inputs.feature +0 -0
  44. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/builtin_policies/include-pinning.feature +0 -0
  45. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/builtin_policies/variable-allowlist.feature +0 -0
  46. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/container_fix.py +0 -0
  47. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/gitlab_api.py +0 -0
  48. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/image_versions.py +0 -0
  49. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/include_fix.py +0 -0
  50. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/include_versions.py +0 -0
  51. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/models.py +0 -0
  52. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/oci_registry.py +0 -0
  53. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/policy_doc.py +0 -0
  54. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/policy_fix.py +0 -0
  55. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/release_cache.py +0 -0
  56. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/render.py +0 -0
  57. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/secret_redact.py +0 -0
  58. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/stash.py +0 -0
  59. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/compliance/supply_chain_fix.py +0 -0
  60. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/gitlab_docs.py +0 -0
  61. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/modules/__init__.py +0 -0
  62. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/modules/command_reference.py +0 -0
  63. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/modules/constants.py +0 -0
  64. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/modules/doc_controller.py +0 -0
  65. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/modules/gitstrings.py +0 -0
  66. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/modules/logging.py +0 -0
  67. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/modules/output_filters.py +0 -0
  68. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/modules/release.py +0 -0
  69. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/modules/swagger_html.py +0 -0
  70. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/modules/swagger_markdown.py +0 -0
  71. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/modules/yaml_lines.py +0 -0
  72. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/modules/yaml_md_table.py +0 -0
  73. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/properties/__init__.py +0 -0
  74. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/properties/extract_job_attribute.py +0 -0
  75. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/properties/includes.py +0 -0
  76. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/properties/inputs.py +0 -0
  77. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/properties/jobs.py +0 -0
  78. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/properties/table_render.py +0 -0
  79. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/properties/variables.py +0 -0
  80. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/properties/workflows.py +0 -0
  81. {gitlab_compliance-2.1.1 → gitlab_compliance-2.2.0}/src/properties/yaml_paths.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitlab-compliance
3
- Version: 2.1.1
3
+ Version: 2.2.0
4
4
  Summary: BDD compliance testing for GitLab CI/CD pipelines and project settings — by MaturityBuilder
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gitlab-compliance"
3
- version = "2.1.1"
3
+ version = "2.2.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.2.0"
65
+ pre-commit = "^4.6.1"
66
66
  pyyaml = "^6.0.3"
67
67
  coverage = "^7.9.2"
68
68
 
@@ -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
@@ -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
@@ -0,0 +1,36 @@
1
+ # METADATA
2
+ # title: Safe file operations
3
+ # description: Quoted paths, secure temps, and protected recursive rm.
4
+ # custom:
5
+ # id: GLCI-SHELL-FILE
6
+ # severity: HIGH
7
+ Feature: Safe file operations
8
+
9
+ # METADATA
10
+ # title: File paths are quoted
11
+ # custom:
12
+ # id: GLCI-SHELL-FILE-001
13
+ # severity: HIGH
14
+ Scenario: File paths are quoted
15
+ Given I have any job with effective script defined
16
+ Then file paths must be quoted
17
+
18
+ # METADATA
19
+ # title: Temporary files are securely created
20
+ # custom:
21
+ # id: GLCI-SHELL-FILE-002
22
+ # severity: HIGH
23
+ Scenario: Temporary files are securely created
24
+ Given I have any job with effective script defined
25
+ When temporary files are required
26
+ Then mktemp must be used for temporary files
27
+
28
+ # METADATA
29
+ # title: Dangerous rm operations are protected
30
+ # custom:
31
+ # id: GLCI-SHELL-FILE-003
32
+ # severity: HIGH
33
+ Scenario: Dangerous rm operations are protected
34
+ Given I have any job with effective script defined
35
+ When rm is used recursively
36
+ Then path validation must be performed for recursive rm