From 60be335ce5b4652e2fe9046f30b210781099b6a3 Mon Sep 17 00:00:00 2001 From: Gabriel Becker Date: Thu, 28 Aug 2025 17:51:05 +0200 Subject: [PATCH 1/6] Move pygithub import right before when it is needed. We need a way to run in dry run mode without requiring the pygithub module. This way also helps the ruff upstream gating code. --- utils/ansible_playbook_to_role.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/utils/ansible_playbook_to_role.py b/utils/ansible_playbook_to_role.py index e3c4bc4ae19..fb161475624 100755 --- a/utils/ansible_playbook_to_role.py +++ b/utils/ansible_playbook_to_role.py @@ -18,13 +18,6 @@ SSG_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) PLAYBOOK_ROOT = os.path.join(SSG_ROOT, "build", "ansible") -try: - from github import Github, InputGitAuthor, UnknownObjectException -except ImportError: - print("Please install PyGithub, on Fedora it's in the python-PyGithub package.", - file=sys.stderr) - raise SystemExit(1) - try: import ssg.ansible @@ -443,6 +436,13 @@ def _get_contents(self, path_name, branch='main'): blob = self._get_blob_content(branch, path_name) if blob is None: + + try: + from github import UnknownObjectException + except ImportError: + print("Please install PyGithub, on Fedora it's in the python-PyGithub package.", + file=sys.stderr) + raise SystemExit(1) raise UnknownObjectException( 'unable to locate file: ' + path_name + ' in branch: ' + branch) return blob @@ -462,6 +462,12 @@ def _update_content_if_needed(self, filepath): remote_content, sha = self._remote_content(filepath) if self._local_content(filepath) != remote_content: + try: + from github import InputGitAuthor + except ImportError: + print("Please install PyGithub, on Fedora it's in the python-PyGithub package.", + file=sys.stderr) + raise SystemExit(1) self.remote_repo.update_file( filepath, "Updated " + filepath, @@ -582,6 +588,13 @@ def main(): username = args.token password = "" + + try: + from github import Github + except ImportError: + print("Please install PyGithub, on Fedora it's in the python-PyGithub package.", + file=sys.stderr) + raise SystemExit(1) github = Github(username, password) github_org = github.get_organization(args.organization) github_repositories = [repo.name for repo in github_org.get_repos()] From b3b358b11cc129821098e3ce395483879d639647 Mon Sep 17 00:00:00 2001 From: Gabriel Becker Date: Thu, 28 Aug 2025 20:00:37 +0200 Subject: [PATCH 2/6] Add a new SSG_ANSIBLE_ROLES_ENABLED to build ansible roles. Introduces a new macro function to build ansible roles out of the profile ansible playbooks. These files can be further turned into Ansible collections for example to support ansible content publishing. --- CMakeLists.txt | 2 ++ cmake/SSGCommon.cmake | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 774519b55e5..5d498ca869a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,6 +62,7 @@ set(SSG_UTILS_SCRIPTS "${CMAKE_SOURCE_DIR}/utils") # Content Generation Opetions option(SSG_ANSIBLE_PLAYBOOKS_ENABLED "If enabled, Ansible Playbooks for each profile will be built and installed." TRUE) option(SSG_ANSIBLE_PLAYBOOKS_PER_RULE_ENABLED "If enabled, Ansible Playbooks for each rule will be built and installed." FALSE) +option(SSG_ANSIBLE_ROLES_ENABLED "If enabled, Ansible Roles will be built. Depends on SSG_ANSIBLE_PLAYBOOKS_ENABLED." FALSE) option(SSG_BASH_SCRIPTS_ENABLED "If enabled, Bash remediation scripts for each profile will be built and installed." TRUE) option(SSG_BUILD_DISA_DELTA_FILES "If enabled, If the product has automated content from DISA for its STIG a tailoring file will be created with rules not covered by DISA's content enabled." TRUE) option(SSG_JINJA2_CACHE_ENABLED "If enabled, the jinja2 templating files will be cached into bytecode. Also see SSG_JINJA2_CACHE_DIR." TRUE) @@ -284,6 +285,7 @@ message(STATUS "Target OVAL version: ${SSG_TARGET_OVAL_VERSION}") message(STATUS "Logging: ${SSG_LOG}") message(STATUS "Ansible Playbooks: ${SSG_ANSIBLE_PLAYBOOKS_ENABLED}") message(STATUS "Ansible Playbooks Per Rule: ${SSG_ANSIBLE_PLAYBOOKS_PER_RULE_ENABLED}") +message(STATUS "Ansible Roles: ${SSG_ANSIBLE_ROLES_ENABLED}") message(STATUS "Bash scripts: ${SSG_BASH_SCRIPTS_ENABLED}") message(STATUS "Build SCE Content: ${SSG_SCE_ENABLED}") message(STATUS "Build SRG XLSX Export: ${SSG_SRG_XLSX_EXPORT}") diff --git a/cmake/SSGCommon.cmake b/cmake/SSGCommon.cmake index 0a8b98d6cb1..018df806861 100644 --- a/cmake/SSGCommon.cmake +++ b/cmake/SSGCommon.cmake @@ -257,6 +257,20 @@ macro(ssg_build_ansible_playbooks PRODUCT) endif() endmacro() +macro(ssg_build_ansible_roles) + set(ANSIBLE_ROLES_DIR "${CMAKE_BINARY_DIR}/ansible_roles") + add_custom_command( + OUTPUT "${ANSIBLE_ROLES_DIR}/ansible_roles-${PRODUCT}" + COMMAND env "PYTHONPATH=$ENV{PYTHONPATH}" "${Python_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/utils/ansible_playbook_to_role.py" --dry-run "${ANSIBLE_ROLES_DIR}" --product "${PRODUCT}" --build-playbooks-dir "${CMAKE_BINARY_DIR}/ansible" + DEPENDS generate-all-profile-playbooks-${PRODUCT} + COMMENT "[${PRODUCT}-content] Generating Ansible Roles" + ) + add_custom_target( + generate-${PRODUCT}-ansible-roles + DEPENDS "${ANSIBLE_ROLES_DIR}/ansible_roles-${PRODUCT}" + ) +endmacro() + macro(ssg_build_remediations PRODUCT) message(STATUS "Scanning for dependencies of ${PRODUCT} fixes (bash, ansible, puppet, anaconda, ignition, kubernetes and blueprint)...") @@ -772,8 +786,17 @@ macro(ssg_build_product PRODUCT) ) add_dependencies(${PRODUCT} ${PRODUCT}-profile-playbooks) add_dependencies(zipfile ${PRODUCT}-profile-playbooks) + + if(SSG_ANSIBLE_ROLES_ENABLED) + ssg_build_ansible_roles(${PRODUCT}) + add_dependencies( + ${PRODUCT}-content + generate-${PRODUCT}-ansible-roles + ) + endif() endif() + if("${PRODUCT_BASH_REMEDIATION_ENABLED}" AND SSG_BASH_SCRIPTS_ENABLED) ssg_build_profile_bash_scripts(${PRODUCT}) add_custom_target( From e0bc9aa663486c94bc0831f843a33ba397617d51 Mon Sep 17 00:00:00 2001 From: Gabriel Becker Date: Thu, 28 Aug 2025 20:04:43 +0200 Subject: [PATCH 3/6] Do not print information that pollutes the build system output. --- utils/ansible_playbook_to_role.py | 1 - 1 file changed, 1 deletion(-) diff --git a/utils/ansible_playbook_to_role.py b/utils/ansible_playbook_to_role.py index fb161475624..03e3502936d 100755 --- a/utils/ansible_playbook_to_role.py +++ b/utils/ansible_playbook_to_role.py @@ -394,7 +394,6 @@ def _generate_defaults_content(self): return ("%s%s%s" % ("\n".join(header), default_vars_local_content, "\n".join(lines))) def save_to_disk(self, directory): - print("Converting Ansible Playbook {} to Ansible Role {}".format(self._local_playbook_filename, os.path.join(directory, self.name))) for filename in self.PRODUCED_FILES: abs_path = os.path.join(directory, self.name, filename) mkdir_p(os.path.dirname(abs_path)) From ec64525fcb9789dbe7997d0dd8dce2aee2dcf70b Mon Sep 17 00:00:00 2001 From: Gabriel Becker Date: Fri, 29 Aug 2025 12:45:49 +0200 Subject: [PATCH 4/6] Switch Ansible Roles org name to CaC. --- utils/ansible_playbook_to_role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/ansible_playbook_to_role.py b/utils/ansible_playbook_to_role.py index 03e3502936d..4a852387bcd 100755 --- a/utils/ansible_playbook_to_role.py +++ b/utils/ansible_playbook_to_role.py @@ -85,7 +85,7 @@ def dict_constructor(loader, node): ]) -ORGANIZATION_NAME = "RedHatOfficial" +ORGANIZATION_NAME = "ComplianceAsCode" GIT_COMMIT_AUTHOR_NAME = "ComplianceAsCode development team" GIT_COMMIT_AUTHOR_EMAIL = "scap-security-guide@lists.fedorahosted.org" META_TEMPLATE_PATH = os.path.join( From 3f2d4b7870145bdbeaa7a44bb798d33d93a8a0f3 Mon Sep 17 00:00:00 2001 From: Gabriel Becker Date: Fri, 29 Aug 2025 12:50:29 +0200 Subject: [PATCH 5/6] Dry run mode overrides the profile and product allow list. utils/ansible_playbook_to_role.py: When running in dry mode, the script will process every available ansible playbook found in the build/ansible folder. --- utils/ansible_playbook_to_role.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/utils/ansible_playbook_to_role.py b/utils/ansible_playbook_to_role.py index 4a852387bcd..e955359fc83 100755 --- a/utils/ansible_playbook_to_role.py +++ b/utils/ansible_playbook_to_role.py @@ -515,7 +515,7 @@ def parse_args(): help="What profiles to upload, if not specified, upload all that are applicable.") parser.add_argument( "--product", "-r", default=[], action="append", - metavar="PRODUCT", choices=PRODUCT_ALLOWLIST, + metavar="PRODUCT", help="What products to upload, if not specified, upload all that are applicable.") parser.add_argument( "--tag-release", "-n", default=False, action="store_true", @@ -539,14 +539,14 @@ def locally_clone_and_init_repositories(organization, repo_list): def select_roles_to_upload(product_allowlist, profile_allowlist, - build_playbooks_dir): + build_playbooks_dir, dry_run): selected_roles = dict() for filename in sorted(os.listdir(build_playbooks_dir)): root, ext = os.path.splitext(filename) if ext == ".yml": # the format is product-playbook-profile.yml product, _, profile = root.split("-", 2) - if product in product_allowlist and profile in profile_allowlist: + if dry_run or (product in product_allowlist and profile in profile_allowlist): role_name = "ansible-role-%s-%s" % (product, profile) selected_roles[role_name] = (product, profile) return selected_roles @@ -569,7 +569,7 @@ def main(): profile_allowlist &= set(args.profile) selected_roles = select_roles_to_upload( - product_allowlist, profile_allowlist, args.build_playbooks_dir + product_allowlist, profile_allowlist, args.build_playbooks_dir, args.dry_run ) if args.dry_run: From 151adcc7bd8959bf061aadd3f95efb962ab3d3e6 Mon Sep 17 00:00:00 2001 From: Gabriel Becker Date: Tue, 2 Sep 2025 13:50:48 +0200 Subject: [PATCH 6/6] Install ansible roles if they are built. These ansible roles are installed at scap-security-guide/ansible/roles installation folder. --- CMakeLists.txt | 1 + cmake/SSGCommon.cmake | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d498ca869a..4e32d036187 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ set(SSG_VENDOR "ssgproject" CACHE STRING "Specify the XCCDF 1.2 vendor string.") # Define Install Directories, where built content will be stored. set(SSG_ANSIBLE_ROLE_INSTALL_DIR "${CMAKE_INSTALL_DATADIR}/scap-security-guide/ansible") set(SSG_ANSIBLE_PER_RULE_PLAYBOOKS_INSTALL_DIR "${SSG_ANSIBLE_ROLE_INSTALL_DIR}/rule_playbooks") +set(SSG_ANSIBLE_ROLES_INSTALL_DIR "${SSG_ANSIBLE_ROLE_INSTALL_DIR}/roles") set(SSG_BASH_ROLE_INSTALL_DIR "${CMAKE_INSTALL_DATADIR}/scap-security-guide/bash") set(SSG_CONTENT_INSTALL_DIR "${CMAKE_INSTALL_DATADIR}/xml/scap/ssg/content") set(SSG_GUIDE_INSTALL_DIR "${CMAKE_INSTALL_DOCDIR}/guides") diff --git a/cmake/SSGCommon.cmake b/cmake/SSGCommon.cmake index 018df806861..31e8f119e4f 100644 --- a/cmake/SSGCommon.cmake +++ b/cmake/SSGCommon.cmake @@ -915,6 +915,18 @@ macro(ssg_build_product PRODUCT) " ) endif() + if(SSG_ANSIBLE_PLAYBOOKS_ENABLED AND SSG_ANSIBLE_ROLES_ENABLED) + if(NOT IS_ABSOLUTE "${SSG_ANSIBLE_ROLES_INSTALL_DIR}") + set(DESTINATION_DIR "${CMAKE_INSTALL_PREFIX}/${SSG_ANSIBLE_ROLES_INSTALL_DIR}") + else() + set(DESTINATION_DIR "${SSG_ANSIBLE_ROLES_INSTALL_DIR}") + endif() + + install( + DIRECTORY "${CMAKE_BINARY_DIR}/ansible_roles/" + DESTINATION "${DESTINATION_DIR}" + ) + endif() if(ENABLE_SCAPVAL13) add_test(