gitlab-compliance 2.4.0__tar.gz → 2.5.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.4.0 → gitlab_compliance-2.5.0}/PKG-INFO +1 -1
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/pyproject.toml +1 -1
- gitlab_compliance-2.5.0/src/__init__.py +1 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/behave_support/steps/script_steps.py +113 -18
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/console.py +19 -5
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/render.py +37 -11
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/runner.py +188 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/script_analysis.py +384 -49
- gitlab_compliance-2.5.0/src/compliance/shell_lex.py +777 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/shell_render.py +34 -15
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/gitlab_compliance.py +34 -0
- gitlab_compliance-2.4.0/src/__init__.py +0 -1
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/LICENSE +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/docs/index.md +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/__init__.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/api_config.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/api_enrichment.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/behave_support/environment.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/behave_support/steps/__init__.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/behave_support/steps/given_steps.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/behave_support/steps/then_steps.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/behave_support/steps/when_steps.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/__init__.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/baseline-jobs.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/component-inputs.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/include-pinning.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/shell/shell-ci-conventions.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/shell/shell-command-substitution.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/shell/shell-conditionals.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/shell/shell-error-handling.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/shell/shell-file-operations.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/shell/shell-pinning.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/shell/shell-pipelines.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/shell/shell-portability.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/shell/shell-quoting.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/shell/shell-references.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/shell/shell-security.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/supply-chain/image-pinning.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/supply-chain/include-versions.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/supply-chain/service-pinning.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/builtin_policies/variable-allowlist.feature +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/container_fix.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/gitlab_api.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/image_versions.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/include_fetch.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/include_fix.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/include_resolution.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/include_versions.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/merge_requests.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/metadata.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/model.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/models.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/oci_registry.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/policy_doc.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/policy_fix.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/release_cache.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/secret_redact.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/stash.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/compliance/supply_chain_fix.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/gitlab_docs.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/modules/__init__.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/modules/command_reference.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/modules/common.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/modules/constants.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/modules/doc_controller.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/modules/gitlab_reference.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/modules/gitstrings.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/modules/job_composition.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/modules/logging.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/modules/output_filters.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/modules/pipeline_data.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/modules/release.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/modules/swagger_html.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/modules/swagger_markdown.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/modules/yaml_lines.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/modules/yaml_md_table.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/properties/__init__.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/properties/extract_job_attribute.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/properties/includes.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/properties/inputs.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/properties/jobs.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/properties/table_render.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/properties/variables.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/properties/workflows.py +0 -0
- {gitlab_compliance-2.4.0 → gitlab_compliance-2.5.0}/src/properties/yaml_paths.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.4.0" # x-release-please-version
|
|
@@ -6,6 +6,33 @@ from behave import given, then, when
|
|
|
6
6
|
|
|
7
7
|
from src.compliance.behave_support.environment import _skip_remaining_steps
|
|
8
8
|
from src.compliance.script_analysis import (
|
|
9
|
+
evidence_backticks,
|
|
10
|
+
evidence_bashism_in_posix_test,
|
|
11
|
+
evidence_bashisms_without_bash,
|
|
12
|
+
evidence_chmod_777,
|
|
13
|
+
evidence_curl_missing_fail,
|
|
14
|
+
evidence_dangerous_rm,
|
|
15
|
+
evidence_deprecated_ci_build,
|
|
16
|
+
evidence_download_without_checksum,
|
|
17
|
+
evidence_eval,
|
|
18
|
+
evidence_hardcoded_secrets,
|
|
19
|
+
evidence_insecure_temp_files,
|
|
20
|
+
evidence_invalid_shebang,
|
|
21
|
+
evidence_masked_failure,
|
|
22
|
+
evidence_missing_strict_mode,
|
|
23
|
+
evidence_nested_backticks,
|
|
24
|
+
evidence_pipeline_without_pipefail,
|
|
25
|
+
evidence_posix_bashisms,
|
|
26
|
+
evidence_remote_pipe,
|
|
27
|
+
evidence_unpinned_docker,
|
|
28
|
+
evidence_unpinned_manager,
|
|
29
|
+
evidence_unquoted_command_substitution,
|
|
30
|
+
evidence_unquoted_path_variables,
|
|
31
|
+
evidence_unquoted_test_variables,
|
|
32
|
+
evidence_unquoted_variables,
|
|
33
|
+
evidence_unresolved_references,
|
|
34
|
+
evidence_unsafe_array_expansion,
|
|
35
|
+
evidence_unverified_git_clone,
|
|
9
36
|
format_script_violation,
|
|
10
37
|
job_has_effective_script,
|
|
11
38
|
job_has_script_field,
|
|
@@ -49,6 +76,19 @@ from src.compliance.script_analysis import (
|
|
|
49
76
|
from src.compliance.stash import filter_entities, name_starts_with
|
|
50
77
|
|
|
51
78
|
|
|
79
|
+
def _shell_check_verbose(context) -> bool:
|
|
80
|
+
config = getattr(context, "config", None)
|
|
81
|
+
userdata = getattr(config, "userdata", None) if config is not None else None
|
|
82
|
+
if not isinstance(userdata, dict):
|
|
83
|
+
return False
|
|
84
|
+
return str(userdata.get("shell_check_verbose", "false")).lower() in {
|
|
85
|
+
"true",
|
|
86
|
+
"1",
|
|
87
|
+
"yes",
|
|
88
|
+
"on",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
52
92
|
def _set_stash(context, entities: list[dict]):
|
|
53
93
|
context.stash = list(entities)
|
|
54
94
|
context.step_mode = "given"
|
|
@@ -72,32 +112,36 @@ def _apply_filter(context, entities: list[dict], reason: str):
|
|
|
72
112
|
context.stash = entities
|
|
73
113
|
|
|
74
114
|
|
|
75
|
-
def _assert_none(context, predicate, message: str):
|
|
115
|
+
def _assert_none(context, predicate, message: str, evidence=None):
|
|
76
116
|
if context.scenario_skipped:
|
|
77
117
|
return
|
|
78
118
|
context.step_mode = "then"
|
|
79
119
|
if not context.stash:
|
|
80
120
|
raise AssertionError("No entities in stash to assert against.")
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if predicate(entity)
|
|
85
|
-
|
|
121
|
+
verbose = _shell_check_verbose(context)
|
|
122
|
+
failures = []
|
|
123
|
+
for entity in context.stash:
|
|
124
|
+
if not predicate(entity):
|
|
125
|
+
continue
|
|
126
|
+
found = evidence(entity) if verbose and evidence is not None else None
|
|
127
|
+
failures.append(format_script_violation(entity, message, found=found))
|
|
86
128
|
if failures:
|
|
87
129
|
raise AssertionError("; ".join(failures))
|
|
88
130
|
|
|
89
131
|
|
|
90
|
-
def _assert_all_true(context, predicate, message: str):
|
|
132
|
+
def _assert_all_true(context, predicate, message: str, evidence=None):
|
|
91
133
|
if context.scenario_skipped:
|
|
92
134
|
return
|
|
93
135
|
context.step_mode = "then"
|
|
94
136
|
if not context.stash:
|
|
95
137
|
raise AssertionError("No entities in stash to assert against.")
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if
|
|
100
|
-
|
|
138
|
+
verbose = _shell_check_verbose(context)
|
|
139
|
+
failures = []
|
|
140
|
+
for entity in context.stash:
|
|
141
|
+
if predicate(entity):
|
|
142
|
+
continue
|
|
143
|
+
found = evidence(entity) if verbose and evidence is not None else None
|
|
144
|
+
failures.append(format_script_violation(entity, message, found=found))
|
|
101
145
|
if failures:
|
|
102
146
|
raise AssertionError("; ".join(failures))
|
|
103
147
|
|
|
@@ -230,6 +274,7 @@ def then_variables_quoted(context):
|
|
|
230
274
|
context,
|
|
231
275
|
script_has_unquoted_variables,
|
|
232
276
|
"Unquoted variable expansions found",
|
|
277
|
+
evidence=evidence_unquoted_variables,
|
|
233
278
|
)
|
|
234
279
|
|
|
235
280
|
|
|
@@ -239,6 +284,7 @@ def then_path_variables_quoted(context):
|
|
|
239
284
|
context,
|
|
240
285
|
script_has_unquoted_path_variables,
|
|
241
286
|
"Unquoted variables used in file paths",
|
|
287
|
+
evidence=evidence_unquoted_path_variables,
|
|
242
288
|
)
|
|
243
289
|
|
|
244
290
|
|
|
@@ -248,6 +294,7 @@ def then_cmd_sub_quoted(context):
|
|
|
248
294
|
context,
|
|
249
295
|
script_has_unquoted_command_substitution,
|
|
250
296
|
"Unquoted command substitution found",
|
|
297
|
+
evidence=evidence_unquoted_command_substitution,
|
|
251
298
|
)
|
|
252
299
|
|
|
253
300
|
|
|
@@ -257,6 +304,7 @@ def then_array_at_syntax(context):
|
|
|
257
304
|
context,
|
|
258
305
|
script_has_unsafe_array_expansion,
|
|
259
306
|
"Unsafe array expansion found",
|
|
307
|
+
evidence=evidence_unsafe_array_expansion,
|
|
260
308
|
)
|
|
261
309
|
|
|
262
310
|
|
|
@@ -266,6 +314,7 @@ def then_strict_mode_table(context):
|
|
|
266
314
|
context,
|
|
267
315
|
script_enables_strict_mode,
|
|
268
316
|
"Multi-line script missing set -e/-u/pipefail",
|
|
317
|
+
evidence=evidence_missing_strict_mode,
|
|
269
318
|
)
|
|
270
319
|
|
|
271
320
|
|
|
@@ -277,7 +326,12 @@ def then_failure_handled(context):
|
|
|
277
326
|
return True
|
|
278
327
|
return not script_masks_failures(entity)
|
|
279
328
|
|
|
280
|
-
_assert_all_true(
|
|
329
|
+
_assert_all_true(
|
|
330
|
+
context,
|
|
331
|
+
ok,
|
|
332
|
+
"Command failures are not handled explicitly",
|
|
333
|
+
evidence=evidence_masked_failure,
|
|
334
|
+
)
|
|
281
335
|
|
|
282
336
|
|
|
283
337
|
@then("functions must propagate failures")
|
|
@@ -291,6 +345,7 @@ def then_functions_propagate(context):
|
|
|
291
345
|
context,
|
|
292
346
|
script_functions_mask_failures,
|
|
293
347
|
"Functions mask failures with || true",
|
|
348
|
+
evidence=evidence_masked_failure,
|
|
294
349
|
)
|
|
295
350
|
|
|
296
351
|
|
|
@@ -300,6 +355,7 @@ def then_file_paths_quoted(context):
|
|
|
300
355
|
context,
|
|
301
356
|
script_has_unquoted_path_variables,
|
|
302
357
|
"Unquoted file paths found",
|
|
358
|
+
evidence=evidence_unquoted_path_variables,
|
|
303
359
|
)
|
|
304
360
|
|
|
305
361
|
|
|
@@ -309,6 +365,7 @@ def then_mktemp_used(context):
|
|
|
309
365
|
context,
|
|
310
366
|
script_uses_insecure_temp_files,
|
|
311
367
|
"Insecure temporary file creation without mktemp",
|
|
368
|
+
evidence=evidence_insecure_temp_files,
|
|
312
369
|
)
|
|
313
370
|
|
|
314
371
|
|
|
@@ -318,12 +375,18 @@ def then_rm_protected(context):
|
|
|
318
375
|
context,
|
|
319
376
|
script_has_dangerous_rm,
|
|
320
377
|
"Dangerous recursive rm without path validation",
|
|
378
|
+
evidence=evidence_dangerous_rm,
|
|
321
379
|
)
|
|
322
380
|
|
|
323
381
|
|
|
324
382
|
@then("command substitution must use dollar parentheses")
|
|
325
383
|
def then_no_backticks(context):
|
|
326
|
-
_assert_none(
|
|
384
|
+
_assert_none(
|
|
385
|
+
context,
|
|
386
|
+
script_uses_backticks,
|
|
387
|
+
"Legacy backtick substitution found",
|
|
388
|
+
evidence=evidence_backticks,
|
|
389
|
+
)
|
|
327
390
|
|
|
328
391
|
|
|
329
392
|
@then("nested command substitution must use dollar parentheses")
|
|
@@ -332,6 +395,7 @@ def then_nested_dollar(context):
|
|
|
332
395
|
context,
|
|
333
396
|
script_has_nested_backticks,
|
|
334
397
|
"Nested backtick substitution found",
|
|
398
|
+
evidence=evidence_nested_backticks,
|
|
335
399
|
)
|
|
336
400
|
|
|
337
401
|
|
|
@@ -341,6 +405,7 @@ def then_posix_operators(context):
|
|
|
341
405
|
context,
|
|
342
406
|
script_has_bashism_in_posix_test,
|
|
343
407
|
"Non-portable operators in [ tests",
|
|
408
|
+
evidence=evidence_bashism_in_posix_test,
|
|
344
409
|
)
|
|
345
410
|
|
|
346
411
|
|
|
@@ -350,6 +415,7 @@ def then_test_vars_quoted(context):
|
|
|
350
415
|
context,
|
|
351
416
|
script_has_unquoted_test_variables,
|
|
352
417
|
"Unquoted variables in test expressions",
|
|
418
|
+
evidence=evidence_unquoted_test_variables,
|
|
353
419
|
)
|
|
354
420
|
|
|
355
421
|
|
|
@@ -359,12 +425,15 @@ def then_pipefail(context):
|
|
|
359
425
|
context,
|
|
360
426
|
lambda e: (not script_has_pipeline(e)) or script_has_pipefail(e),
|
|
361
427
|
"Pipeline used without pipefail",
|
|
428
|
+
evidence=evidence_pipeline_without_pipefail,
|
|
362
429
|
)
|
|
363
430
|
|
|
364
431
|
|
|
365
432
|
@then("eval must not be used")
|
|
366
433
|
def then_no_eval(context):
|
|
367
|
-
_assert_none(
|
|
434
|
+
_assert_none(
|
|
435
|
+
context, script_uses_eval, "eval is used in script", evidence=evidence_eval
|
|
436
|
+
)
|
|
368
437
|
|
|
369
438
|
|
|
370
439
|
@then("untrusted remote scripts must not be executed")
|
|
@@ -373,6 +442,7 @@ def then_no_remote_pipe(context):
|
|
|
373
442
|
context,
|
|
374
443
|
script_has_remote_pipe_to_shell,
|
|
375
444
|
"Remote pipe to shell (curl|sh) detected",
|
|
445
|
+
evidence=evidence_remote_pipe,
|
|
376
446
|
)
|
|
377
447
|
|
|
378
448
|
|
|
@@ -382,6 +452,7 @@ def then_user_input_safe(context):
|
|
|
382
452
|
context,
|
|
383
453
|
script_has_unquoted_variables,
|
|
384
454
|
"User/CI variables used without quoting",
|
|
455
|
+
evidence=evidence_unquoted_variables,
|
|
385
456
|
)
|
|
386
457
|
|
|
387
458
|
|
|
@@ -391,6 +462,7 @@ def then_no_secrets(context):
|
|
|
391
462
|
context,
|
|
392
463
|
script_has_hardcoded_secrets,
|
|
393
464
|
"Hardcoded secret pattern detected",
|
|
465
|
+
evidence=evidence_hardcoded_secrets,
|
|
394
466
|
)
|
|
395
467
|
|
|
396
468
|
|
|
@@ -400,6 +472,7 @@ def then_downloads_checksum(context):
|
|
|
400
472
|
context,
|
|
401
473
|
script_downloads_without_checksum,
|
|
402
474
|
"Download without nearby checksum verification",
|
|
475
|
+
evidence=evidence_download_without_checksum,
|
|
403
476
|
)
|
|
404
477
|
|
|
405
478
|
|
|
@@ -416,7 +489,12 @@ def then_packages_pinned(context, manager):
|
|
|
416
489
|
predicate = predicates.get(manager)
|
|
417
490
|
if predicate is None:
|
|
418
491
|
raise AssertionError(f"Unknown package manager '{manager}'")
|
|
419
|
-
_assert_none(
|
|
492
|
+
_assert_none(
|
|
493
|
+
context,
|
|
494
|
+
predicate,
|
|
495
|
+
f"Unpinned {manager} package install",
|
|
496
|
+
evidence=lambda entity: evidence_unpinned_manager(entity, manager),
|
|
497
|
+
)
|
|
420
498
|
|
|
421
499
|
|
|
422
500
|
@then("docker commands must pin container images to a tag or sha256 digest")
|
|
@@ -425,6 +503,7 @@ def then_docker_images_pinned(context):
|
|
|
425
503
|
context,
|
|
426
504
|
script_has_unpinned_docker_image,
|
|
427
505
|
"Docker command references an unpinned container image",
|
|
506
|
+
evidence=evidence_unpinned_docker,
|
|
428
507
|
)
|
|
429
508
|
|
|
430
509
|
|
|
@@ -434,12 +513,18 @@ def then_git_clone_verified(context):
|
|
|
434
513
|
context,
|
|
435
514
|
script_has_unverified_git_clone,
|
|
436
515
|
"git clone without commit/tag verification",
|
|
516
|
+
evidence=evidence_unverified_git_clone,
|
|
437
517
|
)
|
|
438
518
|
|
|
439
519
|
|
|
440
520
|
@then("curl must use fail flag")
|
|
441
521
|
def then_curl_fail(context):
|
|
442
|
-
_assert_none(
|
|
522
|
+
_assert_none(
|
|
523
|
+
context,
|
|
524
|
+
script_curl_missing_fail,
|
|
525
|
+
"curl without --fail/-f",
|
|
526
|
+
evidence=evidence_curl_missing_fail,
|
|
527
|
+
)
|
|
443
528
|
|
|
444
529
|
|
|
445
530
|
@then("deprecated CI_BUILD variables must not be used")
|
|
@@ -450,6 +535,7 @@ def then_no_ci_build(context):
|
|
|
450
535
|
context,
|
|
451
536
|
script_uses_deprecated_ci_build_vars,
|
|
452
537
|
"Deprecated $CI_BUILD_* variable used",
|
|
538
|
+
evidence=evidence_deprecated_ci_build,
|
|
453
539
|
)
|
|
454
540
|
|
|
455
541
|
|
|
@@ -459,6 +545,7 @@ def then_no_unresolved_refs(context):
|
|
|
459
545
|
context,
|
|
460
546
|
script_has_unresolved_references,
|
|
461
547
|
"Unresolved !reference in script blocks",
|
|
548
|
+
evidence=evidence_unresolved_references,
|
|
462
549
|
)
|
|
463
550
|
|
|
464
551
|
|
|
@@ -468,6 +555,7 @@ def then_shebang_valid(context):
|
|
|
468
555
|
context,
|
|
469
556
|
lambda e: (not script_has_shebang(e)) or script_shebang_is_valid(e),
|
|
470
557
|
"Invalid shebang in script block",
|
|
558
|
+
evidence=evidence_invalid_shebang,
|
|
471
559
|
)
|
|
472
560
|
|
|
473
561
|
|
|
@@ -477,6 +565,7 @@ def then_bash_declared(context):
|
|
|
477
565
|
context,
|
|
478
566
|
script_bashisms_without_bash_shebang,
|
|
479
567
|
"Bash-specific syntax without bash shebang",
|
|
568
|
+
evidence=evidence_bashisms_without_bash,
|
|
480
569
|
)
|
|
481
570
|
|
|
482
571
|
|
|
@@ -486,9 +575,15 @@ def then_posix_no_bashisms(context):
|
|
|
486
575
|
context,
|
|
487
576
|
script_posix_shebang_with_bashisms,
|
|
488
577
|
"Bashisms used with sh shebang",
|
|
578
|
+
evidence=evidence_posix_bashisms,
|
|
489
579
|
)
|
|
490
580
|
|
|
491
581
|
|
|
492
582
|
@then("chmod 777 must not be used")
|
|
493
583
|
def then_no_chmod_777(context):
|
|
494
|
-
_assert_none(
|
|
584
|
+
_assert_none(
|
|
585
|
+
context,
|
|
586
|
+
script_has_chmod_777,
|
|
587
|
+
"chmod 777 is used",
|
|
588
|
+
evidence=evidence_chmod_777,
|
|
589
|
+
)
|
|
@@ -183,14 +183,16 @@ def _summary_table(result: ComplianceResult, *, narrow: bool) -> Table:
|
|
|
183
183
|
return summary
|
|
184
184
|
|
|
185
185
|
|
|
186
|
-
def _scenario_table(
|
|
186
|
+
def _scenario_table(
|
|
187
|
+
result: ComplianceResult, *, width: int, failures_only: bool = False
|
|
188
|
+
) -> Table | None:
|
|
187
189
|
if not result.scenario_results:
|
|
188
190
|
return None
|
|
189
191
|
|
|
190
192
|
narrow = width < 60
|
|
191
193
|
medium = width < 100
|
|
192
194
|
scenarios = Table(
|
|
193
|
-
title="Scenarios",
|
|
195
|
+
title="Scenarios" if not failures_only else "Failed scenarios",
|
|
194
196
|
show_header=True,
|
|
195
197
|
header_style="bold",
|
|
196
198
|
expand=True,
|
|
@@ -204,7 +206,10 @@ def _scenario_table(result: ComplianceResult, *, width: int) -> Table | None:
|
|
|
204
206
|
scenarios.add_column("Policy", overflow="fold")
|
|
205
207
|
scenarios.add_column("Status", justify="center", no_wrap=True)
|
|
206
208
|
|
|
209
|
+
rows_added = 0
|
|
207
210
|
for scenario in result.scenario_results:
|
|
211
|
+
if failures_only and scenario.status != "failed":
|
|
212
|
+
continue
|
|
208
213
|
style = _STATUS_STYLES.get(scenario.status, "bold white")
|
|
209
214
|
policy = scenario.title or scenario.name
|
|
210
215
|
row: list[str | Text] = []
|
|
@@ -214,10 +219,15 @@ def _scenario_table(result: ComplianceResult, *, width: int) -> Table | None:
|
|
|
214
219
|
row.append(scenario.feature)
|
|
215
220
|
row.extend([policy, Text(scenario.status.upper(), style=style)])
|
|
216
221
|
scenarios.add_row(*row)
|
|
222
|
+
rows_added += 1
|
|
223
|
+
if rows_added == 0:
|
|
224
|
+
return None
|
|
217
225
|
return scenarios
|
|
218
226
|
|
|
219
227
|
|
|
220
|
-
def _detail_panels(
|
|
228
|
+
def _detail_panels(
|
|
229
|
+
result: ComplianceResult, *, failures_only: bool = False
|
|
230
|
+
) -> list[Panel]:
|
|
221
231
|
blocks: list[Panel] = []
|
|
222
232
|
failures = [
|
|
223
233
|
scenario for scenario in result.scenario_results if scenario.status == "failed"
|
|
@@ -248,6 +258,9 @@ def _detail_panels(result: ComplianceResult) -> list[Panel]:
|
|
|
248
258
|
)
|
|
249
259
|
)
|
|
250
260
|
|
|
261
|
+
if failures_only:
|
|
262
|
+
return blocks
|
|
263
|
+
|
|
251
264
|
skipped = [
|
|
252
265
|
scenario for scenario in result.scenario_results if scenario.status == "skipped"
|
|
253
266
|
]
|
|
@@ -285,6 +298,7 @@ def render_compliance_console(
|
|
|
285
298
|
*,
|
|
286
299
|
command_title: str = "check",
|
|
287
300
|
console: Console | None = None,
|
|
301
|
+
failures_only: bool = False,
|
|
288
302
|
) -> None:
|
|
289
303
|
"""Render a width-aware Rich compliance report."""
|
|
290
304
|
out = console or get_console()
|
|
@@ -313,10 +327,10 @@ def render_compliance_console(
|
|
|
313
327
|
)
|
|
314
328
|
out.print(_summary_table(result, narrow=narrow))
|
|
315
329
|
|
|
316
|
-
for block in _detail_panels(result):
|
|
330
|
+
for block in _detail_panels(result, failures_only=failures_only):
|
|
317
331
|
out.print(block)
|
|
318
332
|
|
|
319
|
-
scenarios = _scenario_table(result, width=width)
|
|
333
|
+
scenarios = _scenario_table(result, width=width, failures_only=failures_only)
|
|
320
334
|
if scenarios is not None:
|
|
321
335
|
out.print(scenarios)
|
|
322
336
|
|
|
@@ -8,6 +8,7 @@ import json
|
|
|
8
8
|
import os
|
|
9
9
|
import re
|
|
10
10
|
import xml.etree.ElementTree as ET # nosec B405 - XML is generated, not parsed
|
|
11
|
+
from dataclasses import replace
|
|
11
12
|
from datetime import datetime, timezone
|
|
12
13
|
|
|
13
14
|
from src.compliance.models import ComplianceResult, ScenarioResult
|
|
@@ -473,34 +474,43 @@ def render_compliance_junit(
|
|
|
473
474
|
pipeline_file: str,
|
|
474
475
|
*,
|
|
475
476
|
suite_name: str = "gitlab-compliance",
|
|
477
|
+
failures_only: bool = False,
|
|
476
478
|
) -> str:
|
|
477
479
|
"""Render compliance scenarios as JUnit XML for CI test report artifacts."""
|
|
480
|
+
scenarios = list(result.scenario_results)
|
|
481
|
+
if failures_only:
|
|
482
|
+
scenarios = [s for s in scenarios if s.status == "failed"]
|
|
483
|
+
|
|
484
|
+
failed = sum(1 for s in scenarios if s.status == "failed")
|
|
485
|
+
skipped = sum(1 for s in scenarios if s.status == "skipped")
|
|
478
486
|
root = ET.Element(
|
|
479
487
|
"testsuites",
|
|
480
488
|
{
|
|
481
489
|
"name": suite_name,
|
|
482
|
-
"tests": str(
|
|
483
|
-
"failures": str(
|
|
490
|
+
"tests": str(len(scenarios)),
|
|
491
|
+
"failures": str(failed),
|
|
484
492
|
"errors": "0",
|
|
485
|
-
"skipped": str(
|
|
493
|
+
"skipped": str(skipped),
|
|
486
494
|
},
|
|
487
495
|
)
|
|
488
496
|
|
|
489
|
-
for feature,
|
|
490
|
-
result.scenario_results
|
|
491
|
-
).items():
|
|
497
|
+
for feature, feature_scenarios in _group_scenarios_by_feature(scenarios).items():
|
|
492
498
|
suite = ET.SubElement(
|
|
493
499
|
root,
|
|
494
500
|
"testsuite",
|
|
495
501
|
{
|
|
496
502
|
"name": feature,
|
|
497
|
-
"tests": str(len(
|
|
498
|
-
"failures": str(
|
|
503
|
+
"tests": str(len(feature_scenarios)),
|
|
504
|
+
"failures": str(
|
|
505
|
+
sum(1 for s in feature_scenarios if s.status == "failed")
|
|
506
|
+
),
|
|
499
507
|
"errors": "0",
|
|
500
|
-
"skipped": str(
|
|
508
|
+
"skipped": str(
|
|
509
|
+
sum(1 for s in feature_scenarios if s.status == "skipped")
|
|
510
|
+
),
|
|
501
511
|
},
|
|
502
512
|
)
|
|
503
|
-
for scenario in
|
|
513
|
+
for scenario in feature_scenarios:
|
|
504
514
|
testcase = ET.SubElement(
|
|
505
515
|
suite,
|
|
506
516
|
"testcase",
|
|
@@ -557,6 +567,7 @@ def render_compliance_report(
|
|
|
557
567
|
output_format: str,
|
|
558
568
|
*,
|
|
559
569
|
suite_name: str = "gitlab-compliance",
|
|
570
|
+
failures_only: bool = False,
|
|
560
571
|
) -> str | None:
|
|
561
572
|
fmt = output_format.lower()
|
|
562
573
|
if fmt == "markdown":
|
|
@@ -566,7 +577,22 @@ def render_compliance_report(
|
|
|
566
577
|
if fmt == "mr-comment":
|
|
567
578
|
return render_compliance_mr_comment(result, pipeline_file, features_dir)
|
|
568
579
|
if fmt == "codequality":
|
|
580
|
+
if failures_only:
|
|
581
|
+
filtered = [s for s in result.scenario_results if s.status == "failed"]
|
|
582
|
+
result = replace(
|
|
583
|
+
result,
|
|
584
|
+
scenario_results=filtered,
|
|
585
|
+
scenarios=len(filtered),
|
|
586
|
+
passed=0,
|
|
587
|
+
failed=len(filtered),
|
|
588
|
+
skipped=0,
|
|
589
|
+
)
|
|
569
590
|
return render_compliance_code_quality(result, pipeline_file)
|
|
570
591
|
if fmt == "junit":
|
|
571
|
-
return render_compliance_junit(
|
|
592
|
+
return render_compliance_junit(
|
|
593
|
+
result,
|
|
594
|
+
pipeline_file,
|
|
595
|
+
suite_name=suite_name,
|
|
596
|
+
failures_only=failures_only,
|
|
597
|
+
)
|
|
572
598
|
return None
|