gitlabcis 1.3.2__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.
- gitlabcis/__init__.py +12 -0
- gitlabcis/__main__.py +7 -0
- gitlabcis/benchmarks/__init__.py +8 -0
- gitlabcis/benchmarks/artifacts_4/__init__.py +4 -0
- gitlabcis/benchmarks/artifacts_4/access_to_artifacts_4_2.py +139 -0
- gitlabcis/benchmarks/artifacts_4/origin_traceability_4_4.py +11 -0
- gitlabcis/benchmarks/artifacts_4/package_registries_4_3.py +105 -0
- gitlabcis/benchmarks/artifacts_4/verification_4_1.py +83 -0
- gitlabcis/benchmarks/build_pipelines_2/__init__.py +4 -0
- gitlabcis/benchmarks/build_pipelines_2/build_environment_2_1.py +268 -0
- gitlabcis/benchmarks/build_pipelines_2/build_worker_2_2.py +129 -0
- gitlabcis/benchmarks/build_pipelines_2/pipeline_instructions_2_3.py +444 -0
- gitlabcis/benchmarks/build_pipelines_2/pipeline_integrity_2_4.py +146 -0
- gitlabcis/benchmarks/dependencies_3/__init__.py +2 -0
- gitlabcis/benchmarks/dependencies_3/third_party_packages_3_1.py +171 -0
- gitlabcis/benchmarks/dependencies_3/validate_packages_3_2.py +182 -0
- gitlabcis/benchmarks/deployment_5/__init__.py +2 -0
- gitlabcis/benchmarks/deployment_5/deployment_configuration_5_1.py +165 -0
- gitlabcis/benchmarks/deployment_5/deployment_environment_5_2.py +66 -0
- gitlabcis/benchmarks/source_code_1/__init__.py +6 -0
- gitlabcis/benchmarks/source_code_1/code_changes_1_1.py +665 -0
- gitlabcis/benchmarks/source_code_1/code_risks_1_5.py +506 -0
- gitlabcis/benchmarks/source_code_1/contribution_access_1_3.py +334 -0
- gitlabcis/benchmarks/source_code_1/repository_management_1_2.py +168 -0
- gitlabcis/benchmarks/source_code_1/third_party_1_4.py +139 -0
- gitlabcis/cli/__init__.py +0 -0
- gitlabcis/cli/log.py +30 -0
- gitlabcis/cli/main.py +541 -0
- gitlabcis/cli/output.py +151 -0
- gitlabcis/recommendations/artifacts_4/access_to_artifacts_4_2/external_auth_server.yml +51 -0
- gitlabcis/recommendations/artifacts_4/access_to_artifacts_4_2/limit_artifact_uploaders.yml +57 -0
- gitlabcis/recommendations/artifacts_4/access_to_artifacts_4_2/limit_certifying_artifacts.yml +53 -0
- gitlabcis/recommendations/artifacts_4/access_to_artifacts_4_2/minimum_package_registry_admins.yml +54 -0
- gitlabcis/recommendations/artifacts_4/access_to_artifacts_4_2/readme.md +14 -0
- gitlabcis/recommendations/artifacts_4/access_to_artifacts_4_2/require_mfa_to_package_registry.yml +52 -0
- gitlabcis/recommendations/artifacts_4/access_to_artifacts_4_2/restrict_anonymous_access.yml +67 -0
- gitlabcis/recommendations/artifacts_4/origin_traceability_4_4/artifact_origin_info.yml +56 -0
- gitlabcis/recommendations/artifacts_4/origin_traceability_4_4/readme.md +7 -0
- gitlabcis/recommendations/artifacts_4/package_registries_4_3/all_artifact_versions_signed.yml +70 -0
- gitlabcis/recommendations/artifacts_4/package_registries_4_3/audit_package_registry_config.yml +46 -0
- gitlabcis/recommendations/artifacts_4/package_registries_4_3/readme.md +12 -0
- gitlabcis/recommendations/artifacts_4/package_registries_4_3/secure_repo_webhooks.yml +50 -0
- gitlabcis/recommendations/artifacts_4/package_registries_4_3/validate_signed_artifacts_on_upload.yml +72 -0
- gitlabcis/recommendations/artifacts_4/readme.md +12 -0
- gitlabcis/recommendations/artifacts_4/verification_4_1/encrypt_artifacts_before_distribution.yml +47 -0
- gitlabcis/recommendations/artifacts_4/verification_4_1/only_authorized_platforms_can_decrypt_artifacts.yml +59 -0
- gitlabcis/recommendations/artifacts_4/verification_4_1/readme.md +11 -0
- gitlabcis/recommendations/artifacts_4/verification_4_1/sign_artifacts_in_build_pipeline.yml +40 -0
- gitlabcis/recommendations/build_pipelines_2/build_environment_2_1/authenticate_build_access.yml +55 -0
- gitlabcis/recommendations/build_pipelines_2/build_environment_2_1/build_automation.yml +54 -0
- gitlabcis/recommendations/build_pipelines_2/build_environment_2_1/build_env_admins.yml +55 -0
- gitlabcis/recommendations/build_pipelines_2/build_environment_2_1/build_logging.yml +49 -0
- gitlabcis/recommendations/build_pipelines_2/build_environment_2_1/disable_build_tools_default_passwords.yml +54 -0
- gitlabcis/recommendations/build_pipelines_2/build_environment_2_1/immutable_pipeline_infrastructure.yml +60 -0
- gitlabcis/recommendations/build_pipelines_2/build_environment_2_1/limit_build_access.yml +64 -0
- gitlabcis/recommendations/build_pipelines_2/build_environment_2_1/limit_build_secrets_scope.yml +56 -0
- gitlabcis/recommendations/build_pipelines_2/build_environment_2_1/readme.md +19 -0
- gitlabcis/recommendations/build_pipelines_2/build_environment_2_1/secure_build_env_webhooks.yml +43 -0
- gitlabcis/recommendations/build_pipelines_2/build_environment_2_1/single_responsibility_pipeline.yml +58 -0
- gitlabcis/recommendations/build_pipelines_2/build_environment_2_1/vuln_scanning.yml +64 -0
- gitlabcis/recommendations/build_pipelines_2/build_worker_2_2/build_worker_vuln_scanning.yml +58 -0
- gitlabcis/recommendations/build_pipelines_2/build_worker_2_2/monitor_worker_resource_consumption.yml +59 -0
- gitlabcis/recommendations/build_pipelines_2/build_worker_2_2/pass_worker_envs_and_commands.yml +48 -0
- gitlabcis/recommendations/build_pipelines_2/build_worker_2_2/readme.md +16 -0
- gitlabcis/recommendations/build_pipelines_2/build_worker_2_2/restrict_worker_connectivity.yml +61 -0
- gitlabcis/recommendations/build_pipelines_2/build_worker_2_2/segregate_worker_duties.yml +78 -0
- gitlabcis/recommendations/build_pipelines_2/build_worker_2_2/single_use_workers.yml +47 -0
- gitlabcis/recommendations/build_pipelines_2/build_worker_2_2/store_worker_config.yml +62 -0
- gitlabcis/recommendations/build_pipelines_2/build_worker_2_2/worker_runtime_security.yml +37 -0
- gitlabcis/recommendations/build_pipelines_2/pipeline_instructions_2_3/build_stage_io.yml +49 -0
- gitlabcis/recommendations/build_pipelines_2/pipeline_instructions_2_3/build_steps_as_code.yml +42 -0
- gitlabcis/recommendations/build_pipelines_2/pipeline_instructions_2_3/limit_pipeline_triggers.yml +76 -0
- gitlabcis/recommendations/build_pipelines_2/pipeline_instructions_2_3/pipeline_misconfiguration_scanning.yml +48 -0
- gitlabcis/recommendations/build_pipelines_2/pipeline_instructions_2_3/pipeline_secret_scanning.yml +56 -0
- gitlabcis/recommendations/build_pipelines_2/pipeline_instructions_2_3/pipeline_vuln_scanning.yml +44 -0
- gitlabcis/recommendations/build_pipelines_2/pipeline_instructions_2_3/readme.md +16 -0
- gitlabcis/recommendations/build_pipelines_2/pipeline_instructions_2_3/secure_pipeline_output.yml +52 -0
- gitlabcis/recommendations/build_pipelines_2/pipeline_instructions_2_3/track_pipeline_files.yml +48 -0
- gitlabcis/recommendations/build_pipelines_2/pipeline_integrity_2_4/create_reproducible_artifacts.yml +52 -0
- gitlabcis/recommendations/build_pipelines_2/pipeline_integrity_2_4/lock_dependencies.yml +59 -0
- gitlabcis/recommendations/build_pipelines_2/pipeline_integrity_2_4/pipeline_produces_sbom.yml +81 -0
- gitlabcis/recommendations/build_pipelines_2/pipeline_integrity_2_4/pipeline_signs_sbom.yml +38 -0
- gitlabcis/recommendations/build_pipelines_2/pipeline_integrity_2_4/readme.md +14 -0
- gitlabcis/recommendations/build_pipelines_2/pipeline_integrity_2_4/sign_artifacts.yml +35 -0
- gitlabcis/recommendations/build_pipelines_2/pipeline_integrity_2_4/validate_dependencies.yml +63 -0
- gitlabcis/recommendations/build_pipelines_2/readme.md +12 -0
- gitlabcis/recommendations/dependencies_3/readme.md +10 -0
- gitlabcis/recommendations/dependencies_3/third_party_packages_3_1/define_package_managers.yml +84 -0
- gitlabcis/recommendations/dependencies_3/third_party_packages_3_1/dependency_sbom.yml +84 -0
- gitlabcis/recommendations/dependencies_3/third_party_packages_3_1/monitor_dependencies.yml +61 -0
- gitlabcis/recommendations/dependencies_3/third_party_packages_3_1/packages_over_60_days_old.yml +95 -0
- gitlabcis/recommendations/dependencies_3/third_party_packages_3_1/pin_dependency_version.yml +48 -0
- gitlabcis/recommendations/dependencies_3/third_party_packages_3_1/readme.md +14 -0
- gitlabcis/recommendations/dependencies_3/third_party_packages_3_1/third_party_sbom_required.yml +70 -0
- gitlabcis/recommendations/dependencies_3/third_party_packages_3_1/verify_artifacts.yml +45 -0
- gitlabcis/recommendations/dependencies_3/third_party_packages_3_1/verify_signed_metadata.yml +41 -0
- gitlabcis/recommendations/dependencies_3/validate_packages_3_2/org_wide_dependency_policy.yml +47 -0
- gitlabcis/recommendations/dependencies_3/validate_packages_3_2/package_license_scanning.yml +47 -0
- gitlabcis/recommendations/dependencies_3/validate_packages_3_2/package_ownership_change.yml +42 -0
- gitlabcis/recommendations/dependencies_3/validate_packages_3_2/package_vuln_scanning.yml +62 -0
- gitlabcis/recommendations/dependencies_3/validate_packages_3_2/readme.md +10 -0
- gitlabcis/recommendations/deployment_5/deployment_configuration_5_1/audit_deployment_config.yml +46 -0
- gitlabcis/recommendations/deployment_5/deployment_configuration_5_1/limit_deployment_config_access.yml +51 -0
- gitlabcis/recommendations/deployment_5/deployment_configuration_5_1/pin_deployment_config_manifests.yml +59 -0
- gitlabcis/recommendations/deployment_5/deployment_configuration_5_1/readme.md +13 -0
- gitlabcis/recommendations/deployment_5/deployment_configuration_5_1/scan_iac.yml +72 -0
- gitlabcis/recommendations/deployment_5/deployment_configuration_5_1/secret_scan_deployment_config.yml +45 -0
- gitlabcis/recommendations/deployment_5/deployment_configuration_5_1/separate_deployment_config.yml +50 -0
- gitlabcis/recommendations/deployment_5/deployment_configuration_5_1/verify_deployment_config.yml +49 -0
- gitlabcis/recommendations/deployment_5/deployment_environment_5_2/automate_deployment.yml +47 -0
- gitlabcis/recommendations/deployment_5/deployment_environment_5_2/disable_default_passwords.yml +63 -0
- gitlabcis/recommendations/deployment_5/deployment_environment_5_2/limit_prod_access.yml +45 -0
- gitlabcis/recommendations/deployment_5/deployment_environment_5_2/readme.md +12 -0
- gitlabcis/recommendations/deployment_5/deployment_environment_5_2/reproducible_deployment.yml +50 -0
- gitlabcis/recommendations/deployment_5/readme.md +10 -0
- gitlabcis/recommendations/readme.md +24 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/audit_branch_protections.yml +56 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/auto_risk_scan_merges.yml +62 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/branch_protections_for_admins.yml +60 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/branches_updated_before_merging.yml +56 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/checks_pass_before_merging.yml +57 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/code_approval_dismissals.yml +62 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/code_approvals.yml +65 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/code_changes_require_code_owners.yml +68 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/code_dismissal_restrictions.yml +69 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/code_owners.yml +61 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/code_tracing.yml +52 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/comments_resolved_before_merging.yml +59 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/commits_must_be_signed_before_merging.yml +63 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/default_branch_protected.yml +85 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/deny_branch_deletions.yml +76 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/ensure_force_push_is_denied.yml +59 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/linear_history_required.yml +56 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/merging_restrictions.yml +65 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/readme.md +26 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/stale_branch_reviews.yml +72 -0
- gitlabcis/recommendations/source_code_1/code_changes_1_1/version_control.yml +45 -0
- gitlabcis/recommendations/source_code_1/code_risks_1_5/dast_api_scanning.yml +50 -0
- gitlabcis/recommendations/source_code_1/code_risks_1_5/dast_web_scanning.yml +51 -0
- gitlabcis/recommendations/source_code_1/code_risks_1_5/dependency_scanning.yml +84 -0
- gitlabcis/recommendations/source_code_1/code_risks_1_5/enable_secret_detection.yml +45 -0
- gitlabcis/recommendations/source_code_1/code_risks_1_5/license_scanning.yml +47 -0
- gitlabcis/recommendations/source_code_1/code_risks_1_5/readme.md +14 -0
- gitlabcis/recommendations/source_code_1/code_risks_1_5/secure_iac_instructions.yml +81 -0
- gitlabcis/recommendations/source_code_1/code_risks_1_5/secure_pipeline_instructions.yml +62 -0
- gitlabcis/recommendations/source_code_1/code_risks_1_5/vulnerability_scanning.yml +48 -0
- gitlabcis/recommendations/source_code_1/contribution_access_1_3/domain_verification.yml +65 -0
- gitlabcis/recommendations/source_code_1/contribution_access_1_3/ensure_2_admins_per_repo.yml +56 -0
- gitlabcis/recommendations/source_code_1/contribution_access_1_3/limit_top_level_group_creation.yml +61 -0
- gitlabcis/recommendations/source_code_1/contribution_access_1_3/limit_user_registration_domain.yml +58 -0
- gitlabcis/recommendations/source_code_1/contribution_access_1_3/minimum_number_of_admins.yml +56 -0
- gitlabcis/recommendations/source_code_1/contribution_access_1_3/org_provided_ssh_certs.yml +70 -0
- gitlabcis/recommendations/source_code_1/contribution_access_1_3/readme.md +21 -0
- gitlabcis/recommendations/source_code_1/contribution_access_1_3/require_mfa_at_org_level.yml +89 -0
- gitlabcis/recommendations/source_code_1/contribution_access_1_3/require_mfa_for_contributors.yml +76 -0
- gitlabcis/recommendations/source_code_1/contribution_access_1_3/restrict_ip_addresses.yml +84 -0
- gitlabcis/recommendations/source_code_1/contribution_access_1_3/review_and_remove_inactive_users.yml +62 -0
- gitlabcis/recommendations/source_code_1/contribution_access_1_3/scm_notification_restriction.yml +46 -0
- gitlabcis/recommendations/source_code_1/contribution_access_1_3/strict_permissions_for_repo.yml +62 -0
- gitlabcis/recommendations/source_code_1/contribution_access_1_3/track_code_anomalies.yml +43 -0
- gitlabcis/recommendations/source_code_1/readme.md +13 -0
- gitlabcis/recommendations/source_code_1/repository_management_1_2/limit_issue_deletions.yml +57 -0
- gitlabcis/recommendations/source_code_1/repository_management_1_2/limit_repo_creations.yml +64 -0
- gitlabcis/recommendations/source_code_1/repository_management_1_2/limit_repo_deletions.yml +57 -0
- gitlabcis/recommendations/source_code_1/repository_management_1_2/public_repos_have_security_file.yml +59 -0
- gitlabcis/recommendations/source_code_1/repository_management_1_2/readme.md +15 -0
- gitlabcis/recommendations/source_code_1/repository_management_1_2/review_and_archive_stale_repos.yml +65 -0
- gitlabcis/recommendations/source_code_1/repository_management_1_2/track_forks.yml +74 -0
- gitlabcis/recommendations/source_code_1/repository_management_1_2/track_project_visibility_status.yml +74 -0
- gitlabcis/recommendations/source_code_1/third_party_1_4/README.md +12 -0
- gitlabcis/recommendations/source_code_1/third_party_1_4/admin_approval_for_app_installs.yml +83 -0
- gitlabcis/recommendations/source_code_1/third_party_1_4/least_privilge_app_permissions.yml +103 -0
- gitlabcis/recommendations/source_code_1/third_party_1_4/secure_webhooks.yml +73 -0
- gitlabcis/recommendations/source_code_1/third_party_1_4/stale_app_reviews.yml +66 -0
- gitlabcis/recommendations/template.yml +30 -0
- gitlabcis/tests/__init__.py +0 -0
- gitlabcis/tests/input/__init__.py +0 -0
- gitlabcis/tests/input/conftest.py +29 -0
- gitlabcis/tests/input/no_input_test.py +82 -0
- gitlabcis/tests/input/switch_test.py +19 -0
- gitlabcis/tests/input/version_test.py +7 -0
- gitlabcis/tests/unit/__init__.py +0 -0
- gitlabcis/tests/unit/benchmarks/artifacts_4/access_to_artifacts_4_2_test.py +131 -0
- gitlabcis/tests/unit/benchmarks/artifacts_4/origin_traceability_4_4_test.py +15 -0
- gitlabcis/tests/unit/benchmarks/artifacts_4/package_registries_4_3_test.py +102 -0
- gitlabcis/tests/unit/benchmarks/artifacts_4/verification_4_1_test.py +78 -0
- gitlabcis/tests/unit/benchmarks/build_pipelines_2/build_environment_2_1_test.py +239 -0
- gitlabcis/tests/unit/benchmarks/build_pipelines_2/build_worker_2_2_test.py +105 -0
- gitlabcis/tests/unit/benchmarks/build_pipelines_2/pipeline_instructions_2_3_test.py +340 -0
- gitlabcis/tests/unit/benchmarks/build_pipelines_2/pipeline_integrity_2_4_test.py +115 -0
- gitlabcis/tests/unit/benchmarks/conftest.py +47 -0
- gitlabcis/tests/unit/benchmarks/dependencies_3/third_party_packages_3_1_test.py +135 -0
- gitlabcis/tests/unit/benchmarks/dependencies_3/validate_packages_3_2_test.py +171 -0
- gitlabcis/tests/unit/benchmarks/deployment_5/deployment_configuration_5_1_test.py +140 -0
- gitlabcis/tests/unit/benchmarks/deployment_5/deployment_environment_5_2_test.py +60 -0
- gitlabcis/tests/unit/benchmarks/function_test.py +24 -0
- gitlabcis/tests/unit/benchmarks/source_code_1/code_changes_1_1_test.py +565 -0
- gitlabcis/tests/unit/benchmarks/source_code_1/code_risks_1_5_test.py +419 -0
- gitlabcis/tests/unit/benchmarks/source_code_1/contribution_access_1_3_test.py +265 -0
- gitlabcis/tests/unit/benchmarks/source_code_1/repository_management_1_2_test.py +142 -0
- gitlabcis/tests/unit/benchmarks/source_code_1/third_party_1_4_test.py +119 -0
- gitlabcis/tests/unit/conftest.py +94 -0
- gitlabcis/tests/unit/log/log_test.py +23 -0
- gitlabcis/tests/unit/utils/argfilters_test.py +9 -0
- gitlabcis/tests/unit/utils/ci_test.py +156 -0
- gitlabcis/tests/unit/utils/output_test.py +95 -0
- gitlabcis/tests/unit/utils/utils_general_test.py +149 -0
- gitlabcis/tests/unit/utils/version_test.py +11 -0
- gitlabcis/tests/unit/yaml/bad_file_test.py +15 -0
- gitlabcis/tests/unit/yaml/recommendation_test.py +123 -0
- gitlabcis/utils/__init__.py +146 -0
- gitlabcis/utils/ci.py +132 -0
- gitlabcis-1.3.2.dist-info/LICENSE +21 -0
- gitlabcis-1.3.2.dist-info/METADATA +241 -0
- gitlabcis-1.3.2.dist-info/RECORD +218 -0
- gitlabcis-1.3.2.dist-info/WHEEL +5 -0
- gitlabcis-1.3.2.dist-info/entry_points.txt +2 -0
- gitlabcis-1.3.2.dist-info/top_level.txt +1 -0
gitlabcis/cli/main.py
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from argparse import ArgumentParser, FileType
|
|
5
|
+
from collections import namedtuple
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from os import environ
|
|
9
|
+
from sys import exit
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
import gitlab
|
|
13
|
+
from tqdm import tqdm
|
|
14
|
+
|
|
15
|
+
from gitlabcis import (__version__, benchmarks, countRecommendations,
|
|
16
|
+
mapRecommendations, readRecommendations)
|
|
17
|
+
from gitlabcis.cli import log, output
|
|
18
|
+
|
|
19
|
+
# -----------------------------------------------------------------------------
|
|
20
|
+
# Load project & group benchmark functions:
|
|
21
|
+
# -----------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
# load all of the compliance check functions:
|
|
24
|
+
|
|
25
|
+
benchmarkFunctions = [
|
|
26
|
+
getattr(getattr(getattr(benchmarks, catFile), subCatFile), func)
|
|
27
|
+
for catFile in dir(benchmarks)
|
|
28
|
+
if not catFile.startswith('__')
|
|
29
|
+
for subCatFile in dir(getattr(benchmarks, catFile))
|
|
30
|
+
if not subCatFile.startswith('__')
|
|
31
|
+
for func in dir(getattr(getattr(benchmarks, catFile), subCatFile))
|
|
32
|
+
if not func.startswith('__')
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# -----------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
PROFILES = [1, 2]
|
|
38
|
+
IMPLEMENTATION_GROUPS = ['IG1', 'IG2', 'IG3']
|
|
39
|
+
OUTPUT_FORMATS = ['terminal', 'yaml', 'json', 'csv', 'xml', 'txt']
|
|
40
|
+
MAX_WORKERS = 15
|
|
41
|
+
|
|
42
|
+
# -----------------------------------------------------------------------------
|
|
43
|
+
# Main:
|
|
44
|
+
# -----------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main():
|
|
48
|
+
|
|
49
|
+
# -------------------------------------------------------------------------
|
|
50
|
+
# Obtain Input:
|
|
51
|
+
# -------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
parser = ArgumentParser(
|
|
54
|
+
description=f'GitLab CIS Benchmark Scanner Version: {__version__}\n'
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Add arguments
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
'url',
|
|
60
|
+
metavar='URL',
|
|
61
|
+
nargs='*',
|
|
62
|
+
type=str,
|
|
63
|
+
help='The URL to the project to audit'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
'-t',
|
|
68
|
+
'--token',
|
|
69
|
+
dest='token',
|
|
70
|
+
metavar='TOKEN',
|
|
71
|
+
type=str,
|
|
72
|
+
help='GitLab Personal Access Token'
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
'-ci',
|
|
77
|
+
'--cis-controls',
|
|
78
|
+
dest='cis_controls',
|
|
79
|
+
metavar='CIS_CONTROL_IDS',
|
|
80
|
+
nargs='*',
|
|
81
|
+
type=float,
|
|
82
|
+
help='The IDs of the CIS Controls to audit (e.g. 18.1)'
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
'-ids',
|
|
87
|
+
'--recommendation-ids',
|
|
88
|
+
dest='recommendation_ids',
|
|
89
|
+
metavar='RECOMMENDATION_IDS',
|
|
90
|
+
nargs='*',
|
|
91
|
+
type=str,
|
|
92
|
+
help='The IDs of the recommedation controls to audit (e.g. 1.1.1)'
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
'-s',
|
|
97
|
+
'--skip',
|
|
98
|
+
dest='skip_recommendation_ids',
|
|
99
|
+
metavar='RECOMMENDATION_IDS_TO_SKIP',
|
|
100
|
+
nargs='*',
|
|
101
|
+
type=str,
|
|
102
|
+
help='The IDs of the recommedation controls to SKIP (e.g. 1.1.1)'
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
'-p',
|
|
107
|
+
'--profile',
|
|
108
|
+
dest='profile',
|
|
109
|
+
metavar='PROFILE',
|
|
110
|
+
type=int,
|
|
111
|
+
choices=PROFILES,
|
|
112
|
+
help='Which benchmark profile to use (default: both 1 & 2)'
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
'-r',
|
|
117
|
+
'--remediations',
|
|
118
|
+
dest='remediations',
|
|
119
|
+
action='store_true',
|
|
120
|
+
help='Include remediations in the results output'
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
'-o',
|
|
125
|
+
'--output',
|
|
126
|
+
dest='output_file',
|
|
127
|
+
metavar='OUTPUT_FILE',
|
|
128
|
+
type=FileType('w', encoding='utf-8'),
|
|
129
|
+
help='The name of the file to output results to'
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
'-g',
|
|
134
|
+
'--implementation-groups',
|
|
135
|
+
dest='implementation_groups',
|
|
136
|
+
metavar='IMPLEMENTATION_GROUPS',
|
|
137
|
+
nargs='*',
|
|
138
|
+
type=str,
|
|
139
|
+
choices=IMPLEMENTATION_GROUPS,
|
|
140
|
+
help=f'Which CIS Implementation Group to use {IMPLEMENTATION_GROUPS} '
|
|
141
|
+
'(default: all)'
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
'-os',
|
|
146
|
+
'--omit-skipped',
|
|
147
|
+
dest='omit_skipped',
|
|
148
|
+
action='store_true',
|
|
149
|
+
help='Excludes SKIP results from the output'
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
parser.add_argument(
|
|
153
|
+
'-f',
|
|
154
|
+
'--format',
|
|
155
|
+
dest='output_format',
|
|
156
|
+
default='terminal',
|
|
157
|
+
type=str,
|
|
158
|
+
choices=OUTPUT_FORMATS,
|
|
159
|
+
help='Output format (default: terminal)'
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
'-mw',
|
|
164
|
+
'--max-workers',
|
|
165
|
+
dest='max_workers',
|
|
166
|
+
default=15,
|
|
167
|
+
type=int,
|
|
168
|
+
help='Maximum number of Worker threads (default: 15)'
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
parser.add_argument(
|
|
172
|
+
'-d',
|
|
173
|
+
'--debug',
|
|
174
|
+
dest='debug',
|
|
175
|
+
action='store_true',
|
|
176
|
+
help='Enable debugging mode'
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
parser.add_argument(
|
|
180
|
+
'-v',
|
|
181
|
+
'--version',
|
|
182
|
+
dest='version',
|
|
183
|
+
action='store_true',
|
|
184
|
+
help='Print the currently installed version of gitlabcis'
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Parse arguments
|
|
188
|
+
args = parser.parse_args()
|
|
189
|
+
|
|
190
|
+
if args.version:
|
|
191
|
+
print(f'GitLabCIS {__version__}')
|
|
192
|
+
exit(0)
|
|
193
|
+
|
|
194
|
+
if not args.url:
|
|
195
|
+
parser.print_usage()
|
|
196
|
+
exit(2)
|
|
197
|
+
|
|
198
|
+
# -------------------------------------------------------------------------
|
|
199
|
+
# bools to determine what entity to run checks against:
|
|
200
|
+
# -------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
isProject = False
|
|
203
|
+
isGroup = False
|
|
204
|
+
isDotCom = False
|
|
205
|
+
|
|
206
|
+
# -------------------------------------------------------------------------
|
|
207
|
+
# Token heirachy:
|
|
208
|
+
# -------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
# If a user provided a token, that should take highest priority, next
|
|
211
|
+
# is a GITLAB_TOKEN environment variable:
|
|
212
|
+
|
|
213
|
+
token = None
|
|
214
|
+
token_var = None
|
|
215
|
+
|
|
216
|
+
_availableTokens = {
|
|
217
|
+
'--token': args.token,
|
|
218
|
+
'GITLAB_TOKEN': environ.get('GITLAB_TOKEN'),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for _type, token in _availableTokens.items():
|
|
222
|
+
if token is not None:
|
|
223
|
+
token_var = _type
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
if token is None:
|
|
227
|
+
print(
|
|
228
|
+
'Error: No access token found, you must either have the '
|
|
229
|
+
'environment variable: "GITLAB_TOKEN" or provide a token via the '
|
|
230
|
+
'command line (--token).'
|
|
231
|
+
)
|
|
232
|
+
exit(1)
|
|
233
|
+
|
|
234
|
+
# -------------------------------------------------------------------------
|
|
235
|
+
# Input sanity:
|
|
236
|
+
# -------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
if args.output_format.lower() != 'terminal' and args.output_file is None:
|
|
239
|
+
print(
|
|
240
|
+
'Error: Output format provided but no output file provided'
|
|
241
|
+
)
|
|
242
|
+
exit(1)
|
|
243
|
+
|
|
244
|
+
if len(args.url) > 1:
|
|
245
|
+
print('Error: Only one URL is currently supported')
|
|
246
|
+
exit(1)
|
|
247
|
+
else:
|
|
248
|
+
args.url = args.url[0]
|
|
249
|
+
|
|
250
|
+
# -------------------------------------------------------------------------
|
|
251
|
+
# Logging:
|
|
252
|
+
# -------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
if args.debug is False:
|
|
255
|
+
logLevel = 'INFO'
|
|
256
|
+
logging.getLogger('gql.transport.requests').setLevel(logging.ERROR)
|
|
257
|
+
else:
|
|
258
|
+
logLevel = 'DEBUG'
|
|
259
|
+
|
|
260
|
+
logging.basicConfig(
|
|
261
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
262
|
+
level=getattr(logging, logLevel.upper())
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Suppress tokens in logs & max pool size:
|
|
266
|
+
logFilter = log.CustomLogFilter(token)
|
|
267
|
+
logging.getLogger('urllib3.connectionpool').addFilter(logFilter)
|
|
268
|
+
logging.getLogger().addFilter(logFilter)
|
|
269
|
+
|
|
270
|
+
logging.debug(f'args: {args}')
|
|
271
|
+
|
|
272
|
+
# -------------------------------------------------------------------------
|
|
273
|
+
# Auth to GitLab:
|
|
274
|
+
# -------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
urlParsedInput = urlparse(args.url)
|
|
277
|
+
userInputPath = urlParsedInput.path.strip('/')
|
|
278
|
+
userInputHost = f'{urlParsedInput.scheme}://{urlParsedInput.netloc}' # noqa: E231, E501
|
|
279
|
+
|
|
280
|
+
if urlParsedInput.netloc == 'gitlab.com':
|
|
281
|
+
isDotCom = True
|
|
282
|
+
|
|
283
|
+
logging.debug(f'{isDotCom=}')
|
|
284
|
+
|
|
285
|
+
# instantiate the gl obj:
|
|
286
|
+
gl = gitlab.Gitlab(
|
|
287
|
+
userInputHost,
|
|
288
|
+
private_token=token
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# attempt a dry-run auth to make sure the token works:
|
|
292
|
+
try:
|
|
293
|
+
gl.auth()
|
|
294
|
+
|
|
295
|
+
except gitlab.exceptions.GitlabAuthenticationError as e:
|
|
296
|
+
print(
|
|
297
|
+
f'Error: The token provided failed to authenticate to: {args.url}'
|
|
298
|
+
)
|
|
299
|
+
logging.debug(f'Auth Error: {e}')
|
|
300
|
+
exit(1)
|
|
301
|
+
|
|
302
|
+
except (
|
|
303
|
+
gitlab.exceptions.GitlabHttpError,
|
|
304
|
+
gitlab.exceptions.GitlabGetError
|
|
305
|
+
) as e:
|
|
306
|
+
|
|
307
|
+
logging.debug(f'Exception: {e}')
|
|
308
|
+
|
|
309
|
+
if e.response_code == 403 and e.error_message == 'insufficient_scope':
|
|
310
|
+
print(f'Error: The "{token_var}" token has an insufficient scope.')
|
|
311
|
+
exit(1)
|
|
312
|
+
|
|
313
|
+
print(
|
|
314
|
+
f'Error: The host: {userInputHost} does not appear to be a '
|
|
315
|
+
'GitLab instance. If this is erroneous, please raise a bug report.'
|
|
316
|
+
)
|
|
317
|
+
exit(1)
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
print(f'Error: Unable to connect to GitLab instance: {args.url}')
|
|
321
|
+
logging.debug(f'Connection Error: {e}')
|
|
322
|
+
exit(1)
|
|
323
|
+
|
|
324
|
+
# add a warning for gitlab.com admins:
|
|
325
|
+
try:
|
|
326
|
+
if isDotCom is True and gl.user.is_admin:
|
|
327
|
+
|
|
328
|
+
if input('\nWARNING: You are authenticated as a GitLab.com admin. '
|
|
329
|
+
'Running a "full scan" may create significant load.\n\n'
|
|
330
|
+
' Do you wish to continue? (y/n): ').lower() == 'y':
|
|
331
|
+
pass
|
|
332
|
+
else:
|
|
333
|
+
exit(0)
|
|
334
|
+
|
|
335
|
+
# if CTRL-C was pressed, exit cleanly:
|
|
336
|
+
except KeyboardInterrupt:
|
|
337
|
+
exit(0)
|
|
338
|
+
|
|
339
|
+
# if gl.user.is_admin does not return a bool:
|
|
340
|
+
except AttributeError:
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
# -------------------------------------------------------------------------
|
|
344
|
+
# Check if we are dealing with a group or a project:
|
|
345
|
+
# -------------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
entity = gl.projects.get(userInputPath)
|
|
349
|
+
isProject = True
|
|
350
|
+
|
|
351
|
+
except gitlab.exceptions.GitlabGetError as e:
|
|
352
|
+
if '404 Project Not Found' in str(e):
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
entity = gl.groups.get(userInputPath)
|
|
356
|
+
isGroup = True
|
|
357
|
+
|
|
358
|
+
except gitlab.exceptions.GitlabGetError as e:
|
|
359
|
+
if '404 Project Not Found' in str(e):
|
|
360
|
+
print(
|
|
361
|
+
'Either you do not have access to the provided URL or '
|
|
362
|
+
'the URL is invalid. '
|
|
363
|
+
'Please provide a URL using the following '
|
|
364
|
+
'format: https://gitlab.com/path-to-group-or-project '
|
|
365
|
+
'e.g. https://gitlab.com/gitlab/gitlab-com'
|
|
366
|
+
)
|
|
367
|
+
exit(1)
|
|
368
|
+
|
|
369
|
+
if isGroup is False and isProject is False:
|
|
370
|
+
print(f'Error: Unable to find group/project: "{userInputPath}"')
|
|
371
|
+
exit()
|
|
372
|
+
|
|
373
|
+
# -------------------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
# Load the filtered ones from user input:
|
|
376
|
+
filteredRecommendations = readRecommendations(args)
|
|
377
|
+
if len(filteredRecommendations) == 0:
|
|
378
|
+
print('Error: No recommendations were found.')
|
|
379
|
+
exit(1)
|
|
380
|
+
|
|
381
|
+
# -------------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
# format the plan:
|
|
384
|
+
_prof = args.profile if args.profile else ', '.join(
|
|
385
|
+
str(p) for p in PROFILES)
|
|
386
|
+
|
|
387
|
+
_ciCon = ', '.join(
|
|
388
|
+
str(ci)
|
|
389
|
+
for ci in args.cis_controls
|
|
390
|
+
if args.cis_controls) if args.cis_controls else 'All applicable'
|
|
391
|
+
|
|
392
|
+
_impl = ', '.join(
|
|
393
|
+
args.implementation_groups
|
|
394
|
+
if args.implementation_groups
|
|
395
|
+
else IMPLEMENTATION_GROUPS)
|
|
396
|
+
|
|
397
|
+
_start = datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S")
|
|
398
|
+
|
|
399
|
+
# -------------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
graphQLEndpoint = f'{userInputHost}/api/graphql'
|
|
402
|
+
graphQLHeaders = {
|
|
403
|
+
'Authorization': f'Bearer {token}',
|
|
404
|
+
'Content-Type': 'application/json'
|
|
405
|
+
}
|
|
406
|
+
workers = args.max_workers if args.max_workers else MAX_WORKERS
|
|
407
|
+
|
|
408
|
+
# -------------------------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
# determine benchmarks to exec:
|
|
411
|
+
_filteredRecs = len(filteredRecommendations)
|
|
412
|
+
if _filteredRecs == countRecommendations():
|
|
413
|
+
_recs = len(benchmarkFunctions)
|
|
414
|
+
else:
|
|
415
|
+
_recs = _filteredRecs
|
|
416
|
+
|
|
417
|
+
# Print the plan to the user:
|
|
418
|
+
print(
|
|
419
|
+
f'\nRunning CIS benchmark scanner: \n\n'
|
|
420
|
+
f' - Scan Started: {_start}\n'
|
|
421
|
+
f' - Host: {userInputHost}\n'
|
|
422
|
+
f' - {"Group" if isGroup else "Project"}: {userInputPath}\n'
|
|
423
|
+
f' - Output Format: {args.output_format}\n'
|
|
424
|
+
f' - Output File: {args.output_file.name if args.output_file else "stdout"}\n' # noqa: E501
|
|
425
|
+
f' - Profile(s) applied: {_prof}\n'
|
|
426
|
+
f' - CIS Controls: {_ciCon}\n'
|
|
427
|
+
f' - Implementation Group(s): {_impl}\n'
|
|
428
|
+
f' - Benchmarks to check: {_recs}\n\n'
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# -------------------------------------------------------------------------
|
|
432
|
+
# Map the benchmarks:
|
|
433
|
+
# -------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
logging.debug(
|
|
436
|
+
'Running CIS benchmark checks '
|
|
437
|
+
f'against {"project" if isProject else "group"}: {userInputPath}'
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
results = []
|
|
441
|
+
stats = {
|
|
442
|
+
'PASSED': 0,
|
|
443
|
+
'FAILED': 0,
|
|
444
|
+
'SKIPPED': 0,
|
|
445
|
+
'TOTAL': _recs
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
mappedFuncs = mapRecommendations(
|
|
449
|
+
benchmarkFunctions, filteredRecommendations)
|
|
450
|
+
|
|
451
|
+
# -------------------------------------------------------------------------
|
|
452
|
+
# Setup kwargs that each benchmark function can have access to:
|
|
453
|
+
# -------------------------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
kwargs = {'isDotCom': isDotCom, 'isProject': isProject,
|
|
456
|
+
'isGroup': isGroup, 'graphQLEndpoint': graphQLEndpoint,
|
|
457
|
+
'graphQLHeaders': graphQLHeaders}
|
|
458
|
+
|
|
459
|
+
# -------------------------------------------------------------------------
|
|
460
|
+
# Store benchmark results:
|
|
461
|
+
# -------------------------------------------------------------------------
|
|
462
|
+
|
|
463
|
+
Benchmark = namedtuple('Benchmark', ['projectCheck', 'result', 'func'])
|
|
464
|
+
|
|
465
|
+
def executeBenchmark(_projectFunction, _projectCheck, entity, gl,
|
|
466
|
+
**kwargs):
|
|
467
|
+
|
|
468
|
+
logging.debug(f'Executing benchmark: {_projectFunction.__name__}')
|
|
469
|
+
return Benchmark(
|
|
470
|
+
projectCheck=_projectCheck,
|
|
471
|
+
result=_projectFunction(entity, gl, **kwargs),
|
|
472
|
+
func=_projectFunction)
|
|
473
|
+
|
|
474
|
+
# -------------------------------------------------------------------------
|
|
475
|
+
# Projects:
|
|
476
|
+
# -------------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
if isGroup:
|
|
479
|
+
raise NotImplementedError('We do not support groups just yet...')
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
|
|
483
|
+
with ThreadPoolExecutor(max_workers=workers) as executor:
|
|
484
|
+
futures = [
|
|
485
|
+
executor.submit(
|
|
486
|
+
executeBenchmark, _projectFunction, _projectCheck, entity,
|
|
487
|
+
gl, **kwargs)
|
|
488
|
+
for _projectFunction, _projectCheck in mappedFuncs.items()
|
|
489
|
+
]
|
|
490
|
+
|
|
491
|
+
for future in tqdm(
|
|
492
|
+
as_completed(futures),
|
|
493
|
+
total=len(mappedFuncs),
|
|
494
|
+
colour='green',
|
|
495
|
+
desc='Scanning',
|
|
496
|
+
bar_format=(
|
|
497
|
+
'{l_bar}{bar}| {n_fmt}/{total_fmt} completed '
|
|
498
|
+
'[elapsed: {elapsed} remaining: {remaining}]')):
|
|
499
|
+
_res = future.result()
|
|
500
|
+
|
|
501
|
+
try:
|
|
502
|
+
if next(iter(_res.result)) is True:
|
|
503
|
+
_resStr = 'PASS'
|
|
504
|
+
stats['PASSED'] += 1
|
|
505
|
+
elif next(iter(_res.result)) is False:
|
|
506
|
+
_resStr = 'FAIL'
|
|
507
|
+
stats['FAILED'] += 1
|
|
508
|
+
elif next(iter(_res.result)) is None:
|
|
509
|
+
_resStr = 'SKIP'
|
|
510
|
+
stats['SKIPPED'] += 1
|
|
511
|
+
except TypeError:
|
|
512
|
+
logging.error(f'Function: {_res.func.__name__} did '
|
|
513
|
+
'not return a dict')
|
|
514
|
+
exit(1)
|
|
515
|
+
|
|
516
|
+
result = {
|
|
517
|
+
'id': _res.projectCheck['id'],
|
|
518
|
+
'title': _res.projectCheck['title'],
|
|
519
|
+
'reason': list(_res.result.values())[0],
|
|
520
|
+
'result': _resStr
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if args.remediations is True:
|
|
524
|
+
result['remediation'] = _res.projectCheck['remediation']
|
|
525
|
+
|
|
526
|
+
if args.omit_skipped is True \
|
|
527
|
+
and result.get('result') == 'SKIP':
|
|
528
|
+
continue
|
|
529
|
+
|
|
530
|
+
results.append(result)
|
|
531
|
+
|
|
532
|
+
output.output(results, stats, args.output_format, args.output_file)
|
|
533
|
+
|
|
534
|
+
except KeyboardInterrupt:
|
|
535
|
+
exit(1)
|
|
536
|
+
|
|
537
|
+
# -----------------------------------------------------------------------------
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
if __name__ == "__main__":
|
|
541
|
+
main()
|
gitlabcis/cli/output.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from xml.etree import ElementTree as ET # nosec: B405
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
from defusedxml import ElementTree as DET
|
|
10
|
+
from tabulate import tabulate
|
|
11
|
+
|
|
12
|
+
# -----------------------------------------------------------------------------
|
|
13
|
+
# Output:
|
|
14
|
+
# -----------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def output(results, stats, outputFormat='terminal', outputFile=None):
|
|
18
|
+
"""
|
|
19
|
+
Desc: Output the results
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Terminal Icons:
|
|
23
|
+
CHECK = '\u2714'
|
|
24
|
+
CROSS = '\u2718'
|
|
25
|
+
LINE = '\u0021'
|
|
26
|
+
|
|
27
|
+
# Colours:
|
|
28
|
+
GREEN = '\033[32m'
|
|
29
|
+
RED = '\033[91m'
|
|
30
|
+
BLUE = '\033[36m'
|
|
31
|
+
YELLOW = '\033[93m'
|
|
32
|
+
RESET = '\033[0m'
|
|
33
|
+
|
|
34
|
+
if results:
|
|
35
|
+
|
|
36
|
+
_sortedResults = sorted(
|
|
37
|
+
results,
|
|
38
|
+
key=lambda col: [int(_id) for _id in col['id'].split('.')])
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------
|
|
41
|
+
# Terminal output:
|
|
42
|
+
# ---------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
if outputFormat in ['terminal', 'txt']:
|
|
45
|
+
|
|
46
|
+
if _sortedResults:
|
|
47
|
+
_headers = _sortedResults[0].keys()
|
|
48
|
+
_rows = [res.values() for res in _sortedResults]
|
|
49
|
+
else:
|
|
50
|
+
_headers = []
|
|
51
|
+
_rows = []
|
|
52
|
+
|
|
53
|
+
for result in _sortedResults:
|
|
54
|
+
result['result'] = (
|
|
55
|
+
f'{GREEN}PASS {CHECK}{RESET}'
|
|
56
|
+
if result['result'] == 'PASS'
|
|
57
|
+
else f'{BLUE}SKIP {LINE}{RESET}'
|
|
58
|
+
if result['result'] == 'SKIP'
|
|
59
|
+
else f'{RED}FAIL {CROSS}{RESET}'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
table = tabulate(
|
|
63
|
+
_rows,
|
|
64
|
+
headers=_headers,
|
|
65
|
+
tablefmt='rounded_grid',
|
|
66
|
+
maxcolwidths=[None, 75, 25, None])
|
|
67
|
+
|
|
68
|
+
if outputFormat == 'terminal':
|
|
69
|
+
print(f'\nResults:\n\n{table}') # noqa: E231
|
|
70
|
+
|
|
71
|
+
elif outputFormat == 'txt':
|
|
72
|
+
outputFile.write(table)
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------
|
|
75
|
+
# JSON output:
|
|
76
|
+
# ---------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
if outputFormat == 'json':
|
|
79
|
+
|
|
80
|
+
json.dump(_sortedResults, outputFile, indent=4)
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------
|
|
83
|
+
# YAML output:
|
|
84
|
+
# ---------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
if outputFormat == 'yaml':
|
|
87
|
+
|
|
88
|
+
yaml.dump(_sortedResults, outputFile, indent=4)
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------
|
|
91
|
+
# XML output:
|
|
92
|
+
# ---------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
if outputFormat == 'xml':
|
|
95
|
+
|
|
96
|
+
root = ET.Element('root')
|
|
97
|
+
|
|
98
|
+
for item in _sortedResults:
|
|
99
|
+
|
|
100
|
+
sub_element = ET.SubElement(root, 'item')
|
|
101
|
+
|
|
102
|
+
for key, value in item.items():
|
|
103
|
+
ET.SubElement(sub_element, key).text = str(value)
|
|
104
|
+
|
|
105
|
+
outputFile.write(DET.tostring(root, encoding='utf-8').decode())
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------
|
|
108
|
+
# CSV output:
|
|
109
|
+
# ---------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
if outputFormat == 'csv':
|
|
112
|
+
|
|
113
|
+
if _sortedResults:
|
|
114
|
+
_headers = _sortedResults[0].keys()
|
|
115
|
+
else:
|
|
116
|
+
_headers = []
|
|
117
|
+
|
|
118
|
+
writer = csv.DictWriter(outputFile, fieldnames=_headers)
|
|
119
|
+
|
|
120
|
+
writer.writeheader()
|
|
121
|
+
|
|
122
|
+
for row in _sortedResults:
|
|
123
|
+
writer.writerow(row)
|
|
124
|
+
|
|
125
|
+
# -------------------------------------------------------------------------
|
|
126
|
+
# Determine the score - output the stats:
|
|
127
|
+
# -------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
score = round(
|
|
131
|
+
(stats["PASSED"] / (stats["TOTAL"] - stats["SKIPPED"])) * 100, 2)
|
|
132
|
+
except ZeroDivisionError:
|
|
133
|
+
# the total score matched the amount of skipped results
|
|
134
|
+
score = 0
|
|
135
|
+
|
|
136
|
+
if score >= 75:
|
|
137
|
+
scoreColor = GREEN
|
|
138
|
+
elif score >= 50 and score < 75:
|
|
139
|
+
scoreColor = YELLOW
|
|
140
|
+
else:
|
|
141
|
+
scoreColor = RED
|
|
142
|
+
|
|
143
|
+
print(
|
|
144
|
+
'\nScan finished: '
|
|
145
|
+
f'{datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S")}\n\n'
|
|
146
|
+
'Stats:\n\n'
|
|
147
|
+
f' - {GREEN}PASSED: {stats["PASSED"]}/{stats["TOTAL"]}{RESET}\n'
|
|
148
|
+
f' - {RED}FAILED: {stats["FAILED"]}/{stats["TOTAL"]}{RESET}\n'
|
|
149
|
+
f' - {BLUE}SKIPPED: {stats["SKIPPED"]}/{stats["TOTAL"]}{RESET}\n'
|
|
150
|
+
f' - SCORE: {scoreColor}{score}%{RESET} (excludes SKIPPED)\n'
|
|
151
|
+
)
|