Skip to content

Fix non-skippable ansible-lint certification findings in generated Ansible content - #15159

Closed
ggbecker wants to merge 18 commits into
ComplianceAsCode:masterfrom
ggbecker:fix-ansible-lint-certification
Closed

ggbecker wants to merge 18 commits into
ComplianceAsCode:masterfrom
ggbecker:fix-ansible-lint-certification

Conversation

@ggbecker

@ggbecker ggbecker commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Description:

  • This series drives the non-skippable ansible-lint production-profile rules to zero findings in the generated hardening roles / Ansible collection, using genuine remediation fixes instead of inline # noqa skips.
  • The rules addressed are command-instead-of-module, risky-shell-pipe, no-changed-when, and risky-file-permissions, plus supporting lint cleanups in the shared Ansible macros and a tightened CI lint gate so regressions are caught at build time.
  • Fixes are made at the source of generation — shared Jinja macros (shared/macros/10-ansible.jinja), the relevant shared/templates/*/ansible.template templates (e.g. file_owner, file_groupowner, systemd_dropin_configuration), and individual rule ansible/shared.yml files — so every profile's generated role benefits, not just one output.
  • Where a raw command/shell task genuinely cannot be expressed as a module, it is either converted to the ssg_hardening module or explicitly marked and the rule skipped with a documented justification.
  • The final commit additionally fixes a runtime ordering bug found while testing the generated roles: AnsibleSnippetsProcessor emitted the single "Gather the service facts" task after the special_service_block tasks that consume ansible_facts.services, so service rules such as debug-shell, autofs and kdump failed at apply time with object of type 'dict' has no attribute 'services'. The gather is moved ahead of those service tasks (still after package installs). A unit test locks in the ordering.
  • A follow-up commit re-gathers the service facts again after the service blocks. A special_service_block may change a service's state (e.g. start firewalld), so a single early snapshot left later assertions — such as the ccn_advanced firewalld "Informative Message Based on Service State" rule — reading stale facts and failing. Service facts are now gathered both before the blocks (for their when conditions) and after them (so later tasks see the changed state), mirroring the existing package_facts handling.
  • To stop future regressions, the RHEL Ansible-role CI gate now assembles the collection the same way downstream does and lints it with ansible-lint's command-instead-of-module rule (the Automation Hub certification rule that the ssg_hardening rewrite exists to satisfy). A new upstream remediation whose command/shell has a module equivalent but is not mapped to ssg_hardening fails on the pull request that introduces it, rather than only at publishing time. Commands without a module equivalent (find, grep, custom shell) are not flagged, so they do not cause false failures.

Rationale:

  • Ansible partner certification requires zero findings for the non-skippable lint rules in the generated collection. Inline noqa directives cause certification to fail, so each violation has to be resolved with a real fix in the content that generates the tasks.
  • Setting explicit file modes, replacing command tasks with native modules, adding changed_when, and removing risky shell pipes also improves the correctness and idempotency of the generated remediations for all consumers, independent of certification.
  • The service-facts ordering fix is a straightforward correctness bug: the affected service rules could never run successfully in the generated playbooks/roles because the facts they gate on were gathered too late.

Review Hints:

  • The commits are intentionally scoped one lint rule (or concern) per commit and are easiest to review in sequence:
    1. Mark unavoidable command-instead-of-module tasks and skip the rule
    2. Tighten the Ansible role lint gate for certification rules
    3. Fix lint violations in shared Ansible remediation macros
    4. Fix risky-shell-pipe in Ansible remediations
    5. Replace command tasks with Ansible modules
    6. Add changed_when to Ansible command/shell remediations
    7. Replace raw command remediations with the ssg_hardening module in the collection
    8. Set file modes in Ansible remediations to satisfy risky-file-permissions
    9. Gather service facts before special_service_block tasks (runtime ordering fix, covered by a new unit test in tests/unit/ssg-module/test_ansible.py)
    10. Gate the assembled collection on command-instead-of-module in the RHEL Ansible-role CI workflow so unmapped command/shell remediations fail the pull request
    11. Re-gather service facts after special_service_block tasks so post-change state assertions (e.g. firewalld) are correct (covered by a new unit test in tests/unit/ssg-module/test_ansible.py)
  • The authoritative lint check is a single combined ansible-lint invocation over all generated roles (running multiple ansible-lint processes concurrently can crash silently and report false zeros). To reproduce:
    ADDITIONAL_CMAKE_OPTIONS="-DSSG_ANSIBLE_ROLES_ENABLED=TRUE" ./build_product rhel9 --datastream
    ninja -C build generate-rhel9-ansible-roles
    ansible-lint build/ansible_roles/rhel9_*/tasks/main.yml
    
  • For risky-file-permissions, the file modes were taken from the repository's own file_permissions / directory_permissions rules for the same paths (authoritative source), not invented.
  • Note: ansible-lint's risky-file-permissions intentionally exempts ownership-only recursive file tasks (owner/group/setype with no content change), so those correctly remain without a mode.

@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Used by openshift-ci bot. label Oct 2, 2026
@openshift-ci

openshift-ci Bot commented Oct 2, 2026

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

This datastream diff is auto generated by the check Compare DS/Generate Diff.
Due to the excessive size of the diff, it has been trimmed to fit the 65535-character limit.

Click here to see the trimmed diff
ansible remediation for rule 'xccdf_org.ssgproject.content_rule_rpm_verify_hashes' differs.
--- xccdf_org.ssgproject.content_rule_rpm_verify_hashes
+++ xccdf_org.ssgproject.content_rule_rpm_verify_hashes
@@ -136,6 +136,7 @@
 
 - name: Reinstall packages of files with incorrect hash
   ansible.builtin.command: '{{ package_manager_reinstall_cmd }} ''{{ item }}'''
+  changed_when: true
   with_items: '{{ list_of_packages.results | map(attribute=''stdout_lines'') | list
     | unique }}'
   when:

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_rpm_verify_ownership' differs.
--- xccdf_org.ssgproject.content_rule_rpm_verify_ownership
+++ xccdf_org.ssgproject.content_rule_rpm_verify_ownership
@@ -80,6 +80,7 @@
 
 - name: Correct file ownership with RPM
   ansible.builtin.command: rpm --restore '{{ item }}'
+  changed_when: true
   with_items: '{{ list_of_packages.results | map(attribute=''stdout_lines'') | list
     | unique }}'
   when:

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_rpm_verify_permissions' differs.
--- xccdf_org.ssgproject.content_rule_rpm_verify_permissions
+++ xccdf_org.ssgproject.content_rule_rpm_verify_permissions
@@ -83,6 +83,7 @@
 
 - name: Correct file permissions with RPM
   ansible.builtin.command: rpm --restore '{{ item }}'
+  changed_when: true
   with_items: '{{ list_of_packages.results | map(attribute=''stdout_lines'') | list
     | unique }}'
   when:

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_fips_custom_stig_sub_policy' differs.
--- xccdf_org.ssgproject.content_rule_fips_custom_stig_sub_policy
+++ xccdf_org.ssgproject.content_rule_fips_custom_stig_sub_policy
@@ -86,6 +86,7 @@
 
 - name: Implement STIG Sub Crypto Policy - Update crypto-policies
   ansible.builtin.command: update-crypto-policies --set FIPS:STIG
+  changed_when: true
   when:
   - '"kernel-core" in ansible_facts.packages'
   - current_crypto_policy.stdout.strip() != "FIPS:STIG"

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_configure_crypto_policy' differs.
--- xccdf_org.ssgproject.content_rule_configure_crypto_policy
+++ xccdf_org.ssgproject.content_rule_configure_crypto_policy
@@ -130,6 +130,7 @@
 - name: Configure System Cryptography Policy - Verify that Crypto Policy is Set (runtime)
   ansible.builtin.command: /usr/bin/update-crypto-policies --set {{ var_system_crypto_policy
     }}
+  changed_when: true
   when: (current_crypto_policy.stdout.strip() != var_system_crypto_policy) or (config_file_stat.stat.exists
     and current_file_stat.stat.exists and config_file_stat.stat.mtime > current_file_stat.stat.mtime)
     or (not nss_config_stat.stat.exists)

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_configure_custom_crypto_policy_cis' differs.
--- xccdf_org.ssgproject.content_rule_configure_custom_crypto_policy_cis
+++ xccdf_org.ssgproject.content_rule_configure_custom_crypto_policy_cis
@@ -188,6 +188,7 @@
 - name: Implement Custom Crypto Policy Modules for CIS Benchmark - Update crypto-policies
   ansible.builtin.command: update-crypto-policies --set {{ expected_crypto_policy
     }}
+  changed_when: true
   when: current_crypto_policy.stdout.strip() != expected_crypto_policy
   tags:
   - CCE-86707-7

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_openssl_use_strong_entropy' differs.
--- xccdf_org.ssgproject.content_rule_openssl_use_strong_entropy
+++ xccdf_org.ssgproject.content_rule_openssl_use_strong_entropy
@@ -1,6 +1,7 @@
 - name: Put a file with shell wrapper to configure OpenSSL to always use strong entropy
   copy:
     dest: /etc/profile.d/openssl-rand.sh
+    mode: '0644'
     content: |
       # provide a default -rand /dev/random option to openssl commands that
       # support it

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_db_up_to_date' differs.
--- xccdf_org.ssgproject.content_rule_dconf_db_up_to_date
+++ xccdf_org.ssgproject.content_rule_dconf_db_up_to_date
@@ -60,6 +60,7 @@
     keyfiles - Run dconf update for gdm
   ansible.builtin.command:
     cmd: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - '"kernel-core" in ansible_facts.packages'
@@ -124,6 +125,7 @@
     keyfiles - Run dconf update for local
   ansible.builtin.command:
     cmd: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - '"kernel-core" in ansible_facts.packages'

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_disable_restart_shutdown' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_disable_restart_shutdown
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_disable_restart_shutdown
@@ -57,6 +57,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_disable_user_list' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_disable_user_list
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_disable_user_list
@@ -62,6 +62,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_enable_smartcard_auth' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_enable_smartcard_auth
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_enable_smartcard_auth
@@ -65,6 +65,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_lock_screen_on_smartcard_removal' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_lock_screen_on_smartcard_removal
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_lock_screen_on_smartcard_removal
@@ -136,6 +136,7 @@
 
 - name: Dconf Update - removal-action
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - default_file is changed or existing_files is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_login_retries' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_login_retries
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_login_retries
@@ -50,6 +50,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_disable_automount' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_disable_automount
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_disable_automount
@@ -68,6 +68,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_disable_automount_open' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_disable_automount_open
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_disable_automount_open
@@ -68,6 +68,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_disable_autorun' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_disable_autorun
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_disable_autorun
@@ -62,6 +62,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_disable_thumbnailers' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_disable_thumbnailers
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_disable_thumbnailers
@@ -56,6 +56,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_disable_wifi_create' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_disable_wifi_create
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_disable_wifi_create
@@ -47,6 +47,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_disable_wifi_notification' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_disable_wifi_notification
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_disable_wifi_notification
@@ -47,6 +47,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_remote_access_credential_prompt' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_remote_access_credential_prompt
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_remote_access_credential_prompt
@@ -50,6 +50,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_remote_access_encryption' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_remote_access_encryption
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_remote_access_encryption
@@ -59,6 +59,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_idle_activation_enabled' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_idle_activation_enabled
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_idle_activation_enabled
@@ -68,6 +68,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_idle_activation_locked' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_idle_activation_locked
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_idle_activation_locked
@@ -36,6 +36,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_idle_delay' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_idle_delay
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_idle_delay
@@ -53,6 +53,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_delay' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_delay
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_delay
@@ -51,6 +51,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_enabled' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_enabled
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_enabled
@@ -155,6 +155,7 @@
     disable-lock-screen false
   ansible.builtin.command: gsettings set org.gnome.desktop.lockdown disable-lock-screen
     false
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - ansible_distribution == 'SLES'
@@ -177,6 +178,7 @@
 - name: Enable GNOME3 Screensaver Lock After Idle Period - Update dconf database for
     non-SLES systems
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - ansible_distribution != 'SLES'
@@ -199,6 +201,7 @@
 - name: Enable GNOME3 Screensaver Lock After Idle Period - Update dconf database for
     SLES systems
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - ansible_distribution == 'SLES'

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_locked' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_locked
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_lock_locked
@@ -38,6 +38,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_mode_blank' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_mode_blank
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_mode_blank
@@ -71,6 +71,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_user_info' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_user_info
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_user_info
@@ -50,6 +50,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_user_locks' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_user_locks
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_screensaver_user_locks
@@ -36,6 +36,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_session_idle_user_locks' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_session_idle_user_locks
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_session_idle_user_locks
@@ -42,6 +42,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_disable_ctrlaltdel_reboot' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_disable_ctrlaltdel_reboot
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_disable_ctrlaltdel_reboot
@@ -62,6 +62,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_disable_geolocation' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_disable_geolocation
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_disable_geolocation
@@ -82,6 +82,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini1 is changed or result_ini2 is changed or result_lineinfile1 is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_disable_user_admin' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_disable_user_admin
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_disable_user_admin
@@ -128,6 +128,7 @@
 
 - name: Dconf Update - user-administration-disabled
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - default_file is changed or existing_files is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_directory_groupowner_etc_sudoersd' differs.
--- xccdf_org.ssgproject.content_rule_directory_groupowner_etc_sudoersd
+++ xccdf_org.ssgproject.content_rule_directory_groupowner_etc_sudoersd
@@ -42,12 +42,10 @@
   - medium_severity
   - no_reboot_needed
 
-- name: Ensure group owner on /etc/sudoers.d/
-  ansible.builtin.file:
+- name: Test for existence /etc/sudoers.d/
+  ansible.builtin.stat:
     path: /etc/sudoers.d/
-    follow: false
-    state: directory
-    group: '{{ directory_groupowner_etc_sudoersd_newgroup }}'
+  register: file_exists
   when: '"kernel-core" in ansible_facts.packages'
   tags:
   - CCE-86340-7
@@ -57,3 +55,20 @@
   - low_disruption
   - medium_severity
   - no_reboot_needed
+
+- name: Ensure group owner on /etc/sudoers.d/
+  ansible.builtin.file:
+    path: /etc/sudoers.d/
+    follow: false
+    state: directory
+    group: '{{ directory_groupowner_etc_sudoersd_newgroup }}'
+    mode: '{{ file_exists.stat.mode if file_exists.stat.exists else ''0750'' }}'
+  when: '"kernel-core" in ansible_facts.packages'
+  tags:
+  - CCE-86340-7
+  - configure_strategy
+  - directory_groupowner_etc_sudoersd
+  - low_complexity
+  - low_disruption
+  - medium_severity
+  - no_reboot_needed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_directory_owner_etc_sudoersd' differs.
--- xccdf_org.ssgproject.content_rule_directory_owner_etc_sudoersd
+++ xccdf_org.ssgproject.content_rule_directory_owner_etc_sudoersd
@@ -23,12 +23,10 @@
   - medium_severity
   - no_reboot_needed
 
-- name: Ensure owner on directory /etc/sudoers.d/
-  ansible.builtin.file:
+- name: Test for existence /etc/sudoers.d/
+  ansible.builtin.stat:
     path: /etc/sudoers.d/
-    follow: false
-    state: directory
-    owner: '{{ directory_owner_etc_sudoersd_newown }}'
+  register: file_exists
   when: '"kernel-core" in ansible_facts.packages'
   tags:
   - CCE-86355-5
@@ -38,3 +36,20 @@
   - low_disruption
   - medium_severity
   - no_reboot_needed
+
+- name: Ensure owner on directory /etc/sudoers.d/
+  ansible.builtin.file:
+    path: /etc/sudoers.d/
+    follow: false
+    state: directory
+    owner: '{{ directory_owner_etc_sudoersd_newown }}'
+    mode: '{{ file_exists.stat.mode if file_exists.stat.exists else ''0750'' }}'
+  when: '"kernel-core" in ansible_facts.packages'
+  tags:
+  - CCE-86355-5
+  - configure_strategy
+  - directory_owner_etc_sudoersd
+  - low_complexity
+  - low_disruption
+  - medium_severity
+  - no_reboot_needed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_sudoers_validate_passwd' differs.
--- xccdf_org.ssgproject.content_rule_sudoers_validate_passwd
+++ xccdf_org.ssgproject.content_rule_sudoers_validate_passwd
@@ -210,6 +210,7 @@
     path: /etc/sudoers
     create: false
     regexp: ^Defaults !targetpw$
+    mode: '0440'
     state: absent
   check_mode: true
   changed_when: false
@@ -234,6 +235,7 @@
     path: /etc/sudoers
     create: false
     regexp: ^Defaults !targetpw$
+    mode: '0440'
     state: absent
   when:
   - '"kernel-core" in ansible_facts.packages'
@@ -256,6 +258,7 @@
     path: /etc/sudoers
     create: false
     regexp: ^Defaults !targetpw$
+    mode: '0440'
     line: Defaults !targetpw
     state: present
   when:
@@ -278,6 +281,7 @@
     path: /etc/sudoers
     create: false
     regexp: ^Defaults !rootpw$
+    mode: '0440'
     state: absent
   check_mode: true
   changed_when: false
@@ -302,6 +306,7 @@
     path: /etc/sudoers
     create: false
     regexp: ^Defaults !rootpw$
+    mode: '0440'
     state: absent
   when:
   - '"kernel-core" in ansible_facts.packages'
@@ -324,6 +329,7 @@
     path: /etc/sudoers
     create: false
     regexp: ^Defaults !rootpw$
+    mode: '0440'
     line: Defaults !rootpw
     state: present
   when:
@@ -346,6 +352,7 @@
     path: /etc/sudoers
     create: false
     regexp: ^Defaults !runaspw$
+    mode: '0440'
     state: absent
   check_mode: true
   changed_when: false
@@ -370,6 +377,7 @@
     path: /etc/sudoers
     create: false
     regexp: ^Defaults !runaspw$
+    mode: '0440'
     state: absent
   when:
   - '"kernel-core" in ansible_facts.packages'
@@ -392,6 +400,7 @@
     path: /etc/sudoers
     create: false
     regexp: ^Defaults !runaspw$
+    mode: '0440'
     line: Defaults !runaspw
     state: present
   when:

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_ensure_gpgcheck_never_disabled' differs.
--- xccdf_org.ssgproject.content_rule_ensure_gpgcheck_never_disabled
+++ xccdf_org.ssgproject.content_rule_ensure_gpgcheck_never_disabled
@@ -35,6 +35,7 @@
     option: gpgcheck
     value: '1'
     no_extra_spaces: true
+    mode: '0644'
   loop: '{{ repo_grep_results.stdout |regex_findall( ''(.+\.repo):\[(.+)\]\n?'' )
     if repo_grep_results is not skipped else [] }}'
   when: repo_grep_results is not skipped

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_ensure_redhat_gpgkey_installed' differs.
--- xccdf_org.ssgproject.content_rule_ensure_redhat_gpgkey_installed
+++ xccdf_org.ssgproject.content_rule_ensure_redhat_gpgkey_installed
@@ -100,6 +100,7 @@
 
 - name: 'Ensure Red Hat GPG Key Installed: Import RedHat GPG key'
   ansible.builtin.command: rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
+  changed_when: true
   when:
   - gpg_key_directory_permission.stat.mode <= '0755'
   - (gpg_installed_fingerprints | difference(gpg_valid_fingerprints)) | length ==

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_enable_authselect' differs.
--- xccdf_org.ssgproject.content_rule_enable_authselect
+++ xccdf_org.ssgproject.content_rule_enable_authselect
@@ -114,6 +114,7 @@
 - name: Enable authselect - Force authselect Profile Selection
   ansible.builtin.command:
     cmd: authselect select --force "{{ var_authselect_profile }}"
+  changed_when: true
   when:
   - '"kernel-core" in ansible_facts.packages'
   - result_authselect_current.rc != 0

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_banner_enabled' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_banner_enabled
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_banner_enabled
@@ -65,6 +65,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_dconf_gnome_login_banner_text' differs.
--- xccdf_org.ssgproject.content_rule_dconf_gnome_login_banner_text
+++ xccdf_org.ssgproject.content_rule_dconf_gnome_login_banner_text
@@ -116,6 +116,7 @@
 
 - name: Dconf Update
   ansible.builtin.command: dconf update
+  changed_when: true
   when:
   - '"gdm" in ansible_facts.packages'
   - result_ini is changed or result_lineinfile is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_accounts_password_pam_modules_in_authselect_profile' differs.
--- xccdf_org.ssgproject.content_rule_accounts_password_pam_modules_in_authselect_profile
+++ xccdf_org.ssgproject.content_rule_accounts_password_pam_modules_in_authselect_profile
@@ -59,7 +59,10 @@
 - name: Ensure Active Authselect Profile Includes PAM Modules - Get authselect current
     profile
   ansible.builtin.shell:
-    cmd: authselect current -r | awk '{ print $1 }'
+    cmd: |-
+      set -o pipefail
+      authselect current -r | awk '{ print $1 }'
+    executable: /bin/bash
   register: result_authselect_profile
   changed_when: false
   when:
@@ -116,7 +119,10 @@
 - name: Ensure Active Authselect Profile Includes PAM Modules - Get authselect current
     features to also enable them in the custom profile
   ansible.builtin.shell:
-    cmd: authselect current | tail -n+3 | awk '{ print $2 }'
+    cmd: |-
+      set -o pipefail
+      authselect current | tail -n+3 | awk '{ print $2 }'
+    executable: /bin/bash
   register: result_authselect_features
   changed_when: false
   check_mode: false
@@ -158,6 +164,7 @@
     custom profile based on the current profile
   ansible.builtin.command:
     cmd: authselect create-profile hardening -b {{ authselect_current_profile }}
+  changed_when: true
   when:
   - '"pam" in ansible_facts.packages'
   - result_authselect_profile is not skipped
@@ -178,6 +185,7 @@
     custom profile based on sssd profile
   ansible.builtin.command:
     cmd: authselect create-profile hardening -b sssd
+  changed_when: true
   when:
   - '"pam" in ansible_facts.packages'
   - result_authselect_profile is not skipped
@@ -198,6 +206,7 @@
     changes are applied
   ansible.builtin.command:
     cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
+  changed_when: true
   when:
   - '"pam" in ansible_facts.packages'
   - result_authselect_check_cmd is success
@@ -220,6 +229,7 @@
     cmd: authselect select {{ authselect_custom_profile }} {{ result_authselect_features.stdout_lines
       | join(' ') }}
   register: result_pam_authselect_select_profile
+  changed_when: true
   when:
   - '"pam" in ansible_facts.packages'
   - result_authselect_check_cmd is success
@@ -240,6 +250,7 @@
     changes are applied
   ansible.builtin.command:
     cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
+  changed_when: true
   when:
   - '"pam" in ansible_facts.packages'
   - result_authselect_check_cmd is success
@@ -535,6 +546,7 @@
     changes are applied
   ansible.builtin.command:
     cmd: authselect apply-changes -b
+  changed_when: true
   when:
   - '"pam" in ansible_facts.packages'
   - result_authselect_check_cmd is success

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_display_login_attempts' differs.
--- xccdf_org.ssgproject.content_rule_display_login_attempts
+++ xccdf_org.ssgproject.content_rule_display_login_attempts
@@ -99,7 +99,10 @@
   - name: Ensure PAM Displays Last Logon/Access Notification - Get authselect Features
       Currently Enabled
     ansible.builtin.shell:
-      cmd: authselect current | tail -n+3 | awk '{ print $2 }'
+      cmd: |-
+        set -o pipefail
+        authselect current | tail -n+3 | awk '{ print $2 }'
+      executable: /bin/bash
     register: result_authselect_features
     changed_when: false
     check_mode: false
@@ -111,6 +114,7 @@
     ansible.builtin.command:
       cmd: authselect disable-feature with-silent-lastlog
     register: result_authselect_disable_feature_cmd
+    changed_when: true
     when:
     - result_authselect_check_cmd is success
     - result_authselect_features.stdout is search("with-silent-lastlog")
@@ -119,6 +123,7 @@
       are applied
     ansible.builtin.command:
       cmd: authselect apply-changes -b
+    changed_when: true
     when:
     - result_authselect_disable_feature_cmd is not skipped
     - result_authselect_disable_feature_cmd is success
@@ -190,7 +195,10 @@
     - name: Ensure PAM Displays Last Logon/Access Notification - Get authselect current
         profile
       ansible.builtin.shell:
-        cmd: authselect current -r | awk '{ print $1 }'
+        cmd: |-
+          set -o pipefail
+          authselect current -r | awk '{ print $1 }'
+        executable: /bin/bash
       register: result_authselect_profile
       changed_when: false
       when:
@@ -217,7 +225,10 @@
     - name: Ensure PAM Displays Last Logon/Access Notification - Get authselect current
         features to also enable them in the custom profile
       ansible.builtin.shell:
-        cmd: authselect current | tail -n+3 | awk '{ print $2 }'
+        cmd: |-
+          set -o pipefail
+          authselect current | tail -n+3 | awk '{ print $2 }'
+        executable: /bin/bash
       register: result_authselect_features
       changed_when: false
       check_mode: false
@@ -240,6 +251,7 @@
       ansible.builtin.command:
         cmd: authselect create-profile hardening -b {{ authselect_current_profile
           }}
+      changed_when: true
       when:
       - result_authselect_profile is not skipped
       - result_authselect_check_cmd is success
@@ -250,6 +262,7 @@
         custom profile based on sssd profile
       ansible.builtin.command:
         cmd: authselect create-profile hardening -b sssd
+      changed_when: true
       when:
       - result_authselect_profile is not skipped
       - result_authselect_check_cmd is success
@@ -260,6 +273,7 @@
         changes are applied
       ansible.builtin.command:
         cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
+      changed_when: true
       when:
       - result_authselect_check_cmd is success
       - result_authselect_profile is not skipped
@@ -272,6 +286,7 @@
         cmd: authselect select {{ authselect_custom_profile }} {{ result_authselect_features.stdout_lines
           | join(' ') }}
       register: result_pam_authselect_select_profile
+      changed_when: true
       when:
       - result_authselect_check_cmd is success
       - result_authselect_profile is not skipped
@@ -282,6 +297,7 @@
         changes are applied
       ansible.builtin.command:
         cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
+      changed_when: true
       when:
       - result_authselect_check_cmd is success
       - result_authselect_profile is not skipped
@@ -350,6 +366,7 @@
         changes are applied
       ansible.builtin.command:
         cmd: authselect apply-changes -b
+      changed_when: true
       when:
       - result_authselect_present is defined
       - result_authselect_present.stat.exists

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_account_password_pam_faillock_password_auth' differs.
--- xccdf_org.ssgproject.content_rule_account_password_pam_faillock_password_auth
+++ xccdf_org.ssgproject.content_rule_account_password_pam_faillock_password_auth
@@ -38,6 +38,7 @@
     File. - Force reselect authselect profile
   ansible.builtin.command:
     cmd: authselect select {{ authselect_current_profile.stdout }} --force
+  changed_when: true
   when: '"kernel-core" in ansible_facts.packages'
   tags:
   - CCE-86931-3
@@ -103,7 +104,10 @@
   - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/password-auth
       File. - Get authselect current features
     ansible.builtin.shell:
-      cmd: authselect current | tail -n+3 | awk '{ print $2 }'
+      cmd: |-
+        set -o pipefail
+        authselect current | tail -n+3 | awk '{ print $2 }'
+      executable: /bin/bash
     register: result_authselect_features
     changed_when: false
     check_mode: false
@@ -115,6 +119,7 @@
     ansible.builtin.command:
       cmd: authselect enable-feature with-faillock
     register: result_authselect_enable_feature_cmd
+    changed_when: true
     when:
     - result_authselect_check_cmd is success
     - result_authselect_features.stdout is not search("with-faillock")
@@ -123,6 +128,7 @@
       File. - Ensure authselect changes are applied
     ansible.builtin.command:
       cmd: authselect apply-changes -b
+    changed_when: true
     when:
     - result_authselect_enable_feature_cmd is not skipped
     - result_authselect_enable_feature_cmd is success

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_account_password_pam_faillock_system_auth' differs.
--- xccdf_org.ssgproject.content_rule_account_password_pam_faillock_system_auth
+++ xccdf_org.ssgproject.content_rule_account_password_pam_faillock_system_auth
@@ -65,7 +65,10 @@
   - name: Configure the Use of the pam_faillock.so Module in the /etc/pam.d/system-auth
       File. - Get authselect current features
     ansible.builtin.shell:
-      cmd: authselect current | tail -n+3 | awk '{ print $2 }'
+      cmd: |-
+        set -o pipefail
+        authselect current | tail -n+3 | awk '{ print $2 }'
+      executable: /bin/bash
     register: result_authselect_features
     changed_when: false
     check_mode: false
@@ -77,6 +80,7 @@
     ansible.builtin.command:
       cmd: authselect enable-feature with-faillock
     register: result_authselect_enable_feature_cmd
+    changed_when: true
     when:
     - result_authselect_check_cmd is success
     - result_authselect_features.stdout is not search("with-faillock")
@@ -85,6 +89,7 @@
       File. - Ensure authselect changes are applied
     ansible.builtin.command:
       cmd: authselect apply-changes -b
+    changed_when: true
     when:
     - result_authselect_enable_feature_cmd is not skipped
     - result_authselect_enable_feature_cmd is success

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_account_password_selinux_faillock_dir' differs.
--- xccdf_org.ssgproject.content_rule_account_password_selinux_faillock_dir
+++ xccdf_org.ssgproject.content_rule_account_password_selinux_faillock_dir
@@ -15,11 +15,15 @@
 
 - name: An SELinux Context must be configured for the pam_faillock.so records directory
     - Get directories from faillock
-  ansible.builtin.shell: grep -oP '^\s*(?:auth.*pam_faillock.so.*)?dir\s*=\s*(\S+)'
-    "{{ item }}" | sed -r 's/.*=\s*(\S+)/\1/'
+  ansible.builtin.shell:
+    cmd: |-
+      set -o pipefail
+      grep -oP '^\s*(?:auth.*pam_faillock.so.*)?dir\s*=\s*(\S+)' "{{ item }}" | sed -r 's/.*=\s*(\S+)/\1/'
+    executable: /bin/bash
   register: faillock_output
   changed_when: false
   check_mode: false
+  failed_when: false
   with_items:
   - /etc/security/faillock.conf
   - /etc/pam.d/system-auth
@@ -105,6 +109,7 @@
     if ! semanage fcontext -a -t faillog_t "{{ item.item }}(/.*)?"; then
       semanage fcontext -m -t faillog_t "{{ item.item }}(/.*)?"
     fi
+  changed_when: true
   with_items: '{{ faillock_selinux_context.results }}'
   when:
   - '"kernel-core" in ansible_facts.packages'
@@ -126,6 +131,7 @@
 - name: An SELinux Context must be configured for the pam_faillock.so records directory
     - Restore SELinux context
   ansible.builtin.command: restorecon -R -v "{{ item.item }}"
+  changed_when: true
   with_items: '{{ faillock_selinux_context.results }}'
   when:
   - '"kernel-core" in ansible_facts.packages'

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_account_passwords_pam_faillock_audit' differs.
--- xccdf_org.ssgproject.content_rule_account_passwords_pam_faillock_audit
+++ xccdf_org.ssgproject.content_rule_account_passwords_pam_faillock_audit
@@ -56,7 +56,10 @@
 
   - name: Account Lockouts Must Be Logged - Get authselect current features
     ansible.builtin.shell:
-      cmd: authselect current | tail -n+3 | awk '{ print $2 }'
+      cmd: |-
+        set -o pipefail
+        authselect current | tail -n+3 | awk '{ print $2 }'
+      executable: /bin/bash
     register: result_authselect_features
     changed_when: false
     check_mode: false
@@ -68,6 +71,7 @@
     ansible.builtin.command:
       cmd: authselect enable-feature with-faillock
     register: result_authselect_enable_feature_cmd
+    changed_when: true
     when:
     - result_authselect_check_cmd is success
     - result_authselect_features.stdout is not search("with-faillock")
@@ -75,6 +79,7 @@
   - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
     ansible.builtin.command:
       cmd: authselect apply-changes -b
+    changed_when: true
     when:
     - result_authselect_enable_feature_cmd is not skipped
     - result_authselect_enable_feature_cmd is success
@@ -246,7 +251,10 @@
 
       - name: Account Lockouts Must Be Logged - Get authselect current profile
         ansible.builtin.shell:
-          cmd: authselect current -r | awk '{ print $1 }'
+          cmd: |-
+            set -o pipefail
+            authselect current -r | awk '{ print $1 }'
+          executable: /bin/bash
         register: result_authselect_profile
         changed_when: false
         when:
@@ -273,7 +281,10 @@
       - name: Account Lockouts Must Be Logged - Get authselect current features to
           also enable them in the custom profile
         ansible.builtin.shell:
-          cmd: authselect current | tail -n+3 | awk '{ print $2 }'
+          cmd: |-
+            set -o pipefail
+            authselect current | tail -n+3 | awk '{ print $2 }'
+          executable: /bin/bash
         register: result_authselect_features
         changed_when: false
         check_mode: false
@@ -296,6 +307,7 @@
         ansible.builtin.command:
           cmd: authselect create-profile hardening -b {{ authselect_current_profile
             }}
+        changed_when: true
         when:
         - result_authselect_profile is not skipped
         - result_authselect_check_cmd is success
@@ -306,6 +318,7 @@
           based on sssd profile
         ansible.builtin.command:
           cmd: authselect create-profile hardening -b sssd
+        changed_when: true
         when:
         - result_authselect_profile is not skipped
         - result_authselect_check_cmd is success
@@ -315,6 +328,7 @@
       - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
         ansible.builtin.command:
           cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
+        changed_when: true
         when:
         - result_authselect_check_cmd is success
         - result_authselect_profile is not skipped
@@ -327,6 +341,7 @@
           cmd: authselect select {{ authselect_custom_profile }} {{ result_authselect_features.stdout_lines
             | join(' ') }}
         register: result_pam_authselect_select_profile
+        changed_when: true
         when:
         - result_authselect_check_cmd is success
         - result_authselect_profile is not skipped
@@ -336,6 +351,7 @@
       - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
         ansible.builtin.command:
           cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
+        changed_when: true
         when:
         - result_authselect_check_cmd is success
         - result_authselect_profile is not skipped
@@ -374,6 +390,7 @@
     - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
       ansible.builtin.command:
         cmd: authselect apply-changes -b
+      changed_when: true
       when:
       - result_authselect_present.stat.exists
       - result_pam_option_removal is changed
@@ -431,7 +448,10 @@
 
       - name: Account Lockouts Must Be Logged - Get authselect current profile
         ansible.builtin.shell:
-          cmd: authselect current -r | awk '{ print $1 }'
+          cmd: |-
+            set -o pipefail
+            authselect current -r | awk '{ print $1 }'
+          executable: /bin/bash
         register: result_authselect_profile
         changed_when: false
         when:
@@ -458,7 +478,10 @@
       - name: Account Lockouts Must Be Logged - Get authselect current features to
           also enable them in the custom profile
         ansible.builtin.shell:
-          cmd: authselect current | tail -n+3 | awk '{ print $2 }'
+          cmd: |-
+            set -o pipefail
+            authselect current | tail -n+3 | awk '{ print $2 }'
+          executable: /bin/bash
         register: result_authselect_features
         changed_when: false
         check_mode: false
@@ -481,6 +504,7 @@
         ansible.builtin.command:
           cmd: authselect create-profile hardening -b {{ authselect_current_profile
             }}
+        changed_when: true
         when:
         - result_authselect_profile is not skipped
         - result_authselect_check_cmd is success
@@ -491,6 +515,7 @@
           based on sssd profile
         ansible.builtin.command:
           cmd: authselect create-profile hardening -b sssd
+        changed_when: true
         when:
         - result_authselect_profile is not skipped
         - result_authselect_check_cmd is success
@@ -500,6 +525,7 @@
       - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
         ansible.builtin.command:
           cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
+        changed_when: true
         when:
         - result_authselect_check_cmd is success
         - result_authselect_profile is not skipped
@@ -512,6 +538,7 @@
           cmd: authselect select {{ authselect_custom_profile }} {{ result_authselect_features.stdout_lines
             | join(' ') }}
         register: result_pam_authselect_select_profile
+        changed_when: true
         when:
         - result_authselect_check_cmd is success
         - result_authselect_profile is not skipped
@@ -521,6 +548,7 @@
       - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
         ansible.builtin.command:
           cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
+        changed_when: true
         when:
         - result_authselect_check_cmd is success
         - result_authselect_profile is not skipped
@@ -559,6 +587,7 @@
     - name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
       ansible.builtin.command:
         cmd: authselect apply-changes -b
+      changed_when: true
       when:
       - result_authselect_present.stat.exists
       - result_pam_option_removal is changed

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_enforce_for_root' differs.
--- xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_enforce_for_root
+++ xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_enforce_for_root
@@ -1,6 +1,24 @@
 - name: Gather the package facts
   package_facts:
     manager: auto
+  tags:
+  - CCE-86734-1
+  - NIST-800-171-03.05.12
+  - accounts_password_pam_pwhistory_enforce_for_root
+  - low_complexity
+  - low_disruption
+  - medium_severity
+  - no_reboot_needed
+  - restrict_strategy
+
+- name: Check the current permissions of /etc/security/pwhistory.conf to preserve
+    them
+  ansible.builtin.stat:
+    path: /etc/security/pwhistory.conf
+  register: lineinfile_mode_stat
+  when:
+  - '"kernel-core" in ansible_facts.packages'
+  - '"pam" in ansible_facts.packages'
   tags:
   - CCE-86734-1
   - NIST-800-171-03.05.12
@@ -16,6 +34,8 @@
     path: /etc/security/pwhistory.conf
     create: true
     regexp: ''
+    mode: '{{ lineinfile_mode_stat.stat.mode if lineinfile_mode_stat.stat.exists else
+      ''0644'' }}'
     line: enforce_for_root
     state: present
   when:

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_password_auth' differs.
--- xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_password_auth
+++ xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_password_auth
@@ -111,7 +111,10 @@
 
   - name: 'Limit Password Reuse: password-auth - Get authselect current features'
     ansible.builtin.shell:
-      cmd: authselect current | tail -n+3 | awk '{ print $2 }'
+      cmd: |-
+        set -o pipefail
+        authselect current | tail -n+3 | awk '{ print $2 }'
+      executable: /bin/bash
     register: result_authselect_features
     changed_when: false
     check_mode: false
@@ -123,6 +126,7 @@
     ansible.builtin.command:
       cmd: authselect enable-feature with-pwhistory
     register: result_authselect_enable_feature_cmd
+    changed_when: true
     when:
     - result_authselect_check_cmd is success
     - result_authselect_features.stdout is not search("with-pwhistory")
@@ -130,6 +134,7 @@
   - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
     ansible.builtin.command:
       cmd: authselect apply-changes -b
+    changed_when: true
     when:
     - result_authselect_enable_feature_cmd is not skipped
     - result_authselect_enable_feature_cmd is success
@@ -201,7 +206,10 @@
 
     - name: 'Limit Password Reuse: password-auth - Get authselect current profile'
       ansible.builtin.shell:
-        cmd: authselect current -r | awk '{ print $1 }'
+        cmd: |-
+          set -o pipefail
+          authselect current -r | awk '{ print $1 }'
+        executable: /bin/bash
       register: result_authselect_profile
       changed_when: false
       when:
@@ -228,7 +236,10 @@
     - name: 'Limit Password Reuse: password-auth - Get authselect current features
         to also enable them in the custom profile'
       ansible.builtin.shell:
-        cmd: authselect current | tail -n+3 | awk '{ print $2 }'
+        cmd: |-
+          set -o pipefail
+          authselect current | tail -n+3 | awk '{ print $2 }'
+        executable: /bin/bash
       register: result_authselect_features
       changed_when: false
       check_mode: false
@@ -251,6 +262,7 @@
       ansible.builtin.command:
         cmd: authselect create-profile hardening -b {{ authselect_current_profile
           }}
+      changed_when: true
       when:
       - result_authselect_profile is not skipped
       - result_authselect_check_cmd is success
@@ -261,6 +273,7 @@
         based on sssd profile'
       ansible.builtin.command:
         cmd: authselect create-profile hardening -b sssd
+      changed_when: true
       when:
       - result_authselect_profile is not skipped
       - result_authselect_check_cmd is success
@@ -270,6 +283,7 @@
     - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
       ansible.builtin.command:
         cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
+      changed_when: true
       when:
       - result_authselect_check_cmd is success
       - result_authselect_profile is not skipped
@@ -282,6 +296,7 @@
         cmd: authselect select {{ authselect_custom_profile }} {{ result_authselect_features.stdout_lines
           | join(' ') }}
       register: result_pam_authselect_select_profile
+      changed_when: true
       when:
       - result_authselect_check_cmd is success
       - result_authselect_profile is not skipped
@@ -291,6 +306,7 @@
     - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
       ansible.builtin.command:
         cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
+      changed_when: true
       when:
       - result_authselect_check_cmd is success
       - result_authselect_profile is not skipped
@@ -359,6 +375,7 @@
     - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
       ansible.builtin.command:
         cmd: authselect apply-changes -b
+      changed_when: true
       when:
       - result_authselect_present is defined
       - result_authselect_present.stat.exists
@@ -483,7 +500,10 @@
 
         - name: 'Limit Password Reuse: password-auth - Get authselect current profile'
           ansible.builtin.shell:
-            cmd: authselect current -r | awk '{ print $1 }'
+            cmd: |-
+              set -o pipefail
+              authselect current -r | awk '{ print $1 }'
+            executable: /bin/bash
           register: result_authselect_profile
           changed_when: false
           when:
@@ -510,7 +530,10 @@
         - name: 'Limit Password Reuse: password-auth - Get authselect current features
             to also enable them in the custom profile'
           ansible.builtin.shell:
-            cmd: authselect current | tail -n+3 | awk '{ print $2 }'
+            cmd: |-
+              set -o pipefail
+              authselect current | tail -n+3 | awk '{ print $2 }'
+            executable: /bin/bash
           register: result_authselect_features
           changed_when: false
           check_mode: false
@@ -533,6 +556,7 @@
           ansible.builtin.command:
             cmd: authselect create-profile hardening -b {{ authselect_current_profile
               }}
+          changed_when: true
           when:
           - result_authselect_profile is not skipped
           - result_authselect_check_cmd is success
@@ -543,6 +567,7 @@
             profile based on sssd profile'
           ansible.builtin.command:
             cmd: authselect create-profile hardening -b sssd
+          changed_when: true
           when:
           - result_authselect_profile is not skipped
           - result_authselect_check_cmd is success
@@ -553,6 +578,7 @@
             applied'
           ansible.builtin.command:
             cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
+          changed_when: true
           when:
           - result_authselect_check_cmd is success
           - result_authselect_profile is not skipped
@@ -565,6 +591,7 @@
             cmd: authselect select {{ authselect_custom_profile }} {{ result_authselect_features.stdout_lines
               | join(' ') }}
           register: result_pam_authselect_select_profile
+          changed_when: true
           when:
           - result_authselect_check_cmd is success
           - result_authselect_profile is not skipped
@@ -575,6 +602,7 @@
             applied'
           ansible.builtin.command:
             cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
+          changed_when: true
           when:
           - result_authselect_check_cmd is success
           - result_authselect_profile is not skipped
@@ -614,6 +642,7 @@
           applied'
         ansible.builtin.command:
           cmd: authselect apply-changes -b
+        changed_when: true
         when:
         - result_authselect_present.stat.exists
         - result_pam_option_removal is changed
@@ -686,7 +715,10 @@
 
     - name: 'Limit Password Reuse: password-auth - Get authselect current profile'
       ansible.builtin.shell:
-        cmd: authselect current -r | awk '{ print $1 }'
+        cmd: |-
+          set -o pipefail
+          authselect current -r | awk '{ print $1 }'
+        executable: /bin/bash
       register: result_authselect_profile
       changed_when: false
       when:
@@ -713,7 +745,10 @@
     - name: 'Limit Password Reuse: password-auth - Get authselect current features
         to also enable them in the custom profile'
       ansible.builtin.shell:
-        cmd: authselect current | tail -n+3 | awk '{ print $2 }'
+        cmd: |-
+          set -o pipefail
+          authselect current | tail -n+3 | awk '{ print $2 }'
+        executable: /bin/bash
       register: result_authselect_features
       changed_when: false
       check_mode: false
@@ -736,6 +771,7 @@
       ansible.builtin.command:
         cmd: authselect create-profile hardening -b {{ authselect_current_profile
           }}
+      changed_when: true
       when:
       - result_authselect_profile is not skipped
       - result_authselect_check_cmd is success
@@ -746,6 +782,7 @@
         based on sssd profile'
       ansible.builtin.command:
         cmd: authselect create-profile hardening -b sssd
+      changed_when: true
       when:
       - result_authselect_profile is not skipped
       - result_authselect_check_cmd is success
@@ -755,6 +792,7 @@
     - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
       ansible.builtin.command:
         cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
+      changed_when: true
       when:
       - result_authselect_check_cmd is success
       - result_authselect_profile is not skipped
@@ -767,6 +805,7 @@
         cmd: authselect select {{ authselect_custom_profile }} {{ result_authselect_features.stdout_lines
           | join(' ') }}
       register: result_pam_authselect_select_profile
+      changed_when: true
       when:
       - result_authselect_check_cmd is success
       - result_authselect_profile is not skipped
@@ -776,6 +815,7 @@
     - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
       ansible.builtin.command:
         cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
+      changed_when: true
       when:
       - result_authselect_check_cmd is success
       - result_authselect_profile is not skipped
@@ -842,6 +882,7 @@
     - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
       ansible.builtin.command:
         cmd: authselect apply-changes -b
+      changed_when: true
       when:
       - result_authselect_present is defined
       - result_authselect_present.stat.exists
@@ -897,6 +938,7 @@
   - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
     ansible.builtin.command:
       cmd: authselect apply-changes -b
+    changed_when: true
     when:
     - result_authselect_present.stat.exists
     - (result_pam_remember_add is defined and result_pam_remember_add.changed) or

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_system_auth' differs.
--- xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_system_auth
+++ xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_system_auth
@@ -111,7 +111,10 @@
 
   - name: 'Limit Password Reuse: system-auth - Get authselect current features'
     ansible.builtin.shell:
-      cmd: authselect current | tail -n+3 | awk '{ print $2 }'
+      cmd: |-
+        set -o pipefail
+        authselect current | tail -n+3 | awk '{ print $2 }'
+      executable: /bin/bash
     register: result_authselect_features
     changed_when: false
     check_mode: false
@@ -123,6 +126,7 @@
     ansible.builtin.command:
       cmd: authselect enable-feature with-pwhistory
     register: result_authselect_enable_feature_cmd
+    changed_when: true
     when:
     - result_authselect_check_cmd is success
     - result_authselect_features.stdout is not search("with-pwhistory")
@@ -130,6 +134,7 @@
   - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
     ansible.builtin.command:
       cmd: authselect apply-changes -b
+    changed_when: true
     when:
     - result_authselect_enable_feature_cmd is not skipped
     - result_authselect_enable_feature_cmd is success
@@ -201,7 +206,10 @@
 
     - name: 'Limit Password Reuse: system-auth - Get authselect current profile'
       ansible.builtin.shell:
-        cmd: authselect current -r | awk '{ print $1 }'
+        cmd: |-
+          set -o pipefail
+          authselect current -r | awk '{ print $1 }'
+        executable: /bin/bash
       register: result_authselect_profile
       changed_when: false
       when:
@@ -228,7 +236,10 @@
     - name: 'Limit Password Reuse: system-auth - Get authselect current features to
         also enable them in the custom profile'
       ansible.builtin.shell:
-        cmd: authselect current | tail -n+3 | awk '{ print $2 }'
+        cmd: |-
+          set -o pipefail
+          authselect current | tail -n+3 | awk '{ print $2 }'
+        executable: /bin/bash
       register: result_authselect_features
       changed_when: false
       check_mode: false
@@ -251,6 +262,7 @@
       ansible.builtin.command:
         cmd: authselect create-profile hardening -b {{ authselect_current_profile
           }}
+      changed_when: true
       when:
       - result_authselect_profile is not skipped
       - result_authselect_check_cmd is success
@@ -261,6 +273,7 @@
         based on sssd profile'
       ansible.builtin.command:
         cmd: authselect create-profile hardening -b sssd
+      changed_when: true
       when:
       - result_authselect_profile is not skipped
       - result_authselect_check_cmd is success
@@ -270,6 +283,7 @@
     - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
       ansible.builtin.command:
         cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
+      changed_when: true
       when:
       - result_authselect_check_cmd is success
       - result_authselect_profile is not skipped
@@ -282,6 +296,7 @@
         cmd: authselect select {{ authselect_custom_profile }} {{ result_authselect_features.stdout_lines
           | join(' ') }}
       register: result_pam_authselect_select_profile
+      changed_when: true
       when:
       - result_authselect_check_cmd is success
       - result_authselect_profile is not skipped
@@ -291,6 +306,7 @@
     - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
       ansible.builtin.command:
         cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
+      changed_when: true
       when:
       - result_authselect_check_cmd is success
       - result_authselect_profile is not skipped
@@ -359,6 +375,7 @@
     - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
       ansible.builtin.command:
         cmd: authselect apply-changes -b
+      changed_when: true
       when:
       - result_authselect_present is defined
       - result_authselect_present.stat.exists
@@ -483,7 +500,10 @@
 
         - name: 'Limit Password Reuse: system-auth - Get authselect current profile'
           ansible.builtin.shell:
-            cmd: authselect current -r | awk '{ print $1 }'
+            cmd: |-
+              set -o pipefail
+              authselect current -r | awk '{ print $1 }'
+            executable: /bin/bash
           register: result_authselect_profile
           changed_when: false
           when:
@@ -510,7 +530,10 @@
         - name: 'Limit Password Reuse: system-auth - Get authselect current features
             to also enable them in the custom profile'
           ansible.builtin.shell:
-            cmd: authselect current | tail -n+3 | awk '{ print $2 }'
+            cmd: |-
+              set -o pipefail
+              authselect current | tail -n+3 | awk '{ print $2 }'
+            executable: /bin/bash
           register: result_authselect_features
           changed_when: false
           check_mode: false
@@ -533,6 +556,7 @@
           ansible.builtin.command:
             cmd: authselect create-profile hardening -b {{ authselect_current_profile
               }}
+          changed_when: true
           when:
           - result_authselect_profile is not skipped
           - result_authselect_check_cmd is success
@@ -543,6 +567,7 @@
             based on sssd profile'
           ansible.builtin.command:
             cmd: authselect create-profile hardening -b sssd
+          changed_when: true
           when:
           - result_authselect_profile is not skipped
           - result_authselect_check_cmd is success
@@ -553,6 +578,7 @@
             applied'
           ansible.builtin.command:
             cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
+          changed_when: true
           when:
           - result_authselect_check_cmd is success
           - result_authselect_profile is not skipped
@@ -565,6 +591,7 @@
             cmd: authselect select {{ authselect_custom_profile }} {{ result_authselect_features.stdout_lines
               | join(' ') }}
           register: result_pam_authselect_select_profile
+          changed_when: true
           when:
           - result_authselect_check_cmd is success
           - result_authselect_profile is not skipped
@@ -575,6 +602,7 @@
             applied'
           ansible.builtin.command:
             cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
+          changed_when: true
           when:
           - result_authselect_check_cmd is success
           - result_authselect_profile is not skipped
@@ -613,6 +641,7 @@
       - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
         ansible.builtin.command:
           cmd: authselect apply-changes -b
+        changed_when: true
         when:
         - result_authselect_present.stat.exists
         - result_pam_option_removal is changed
@@ -685,7 +714,10 @@
 
     - name: 'Limit Password Reuse: system-auth - Get authselect current profile'
       ansible.builtin.shell:
-        cmd: authselect current -r | awk '{ print $1 }'
+        cmd: |-
+          set -o pipefail
+          authselect current -r | awk '{ print $1 }'
+        executable: /bin/bash
       register: result_authselect_profile
       changed_when: false
       when:
@@ -712,7 +744,10 @@
     - name: 'Limit Password Reuse: system-auth - Get authselect current features to
         also enable them in the custom profile'
       ansible.builtin.shell:
-        cmd: authselect current | tail -n+3 | awk '{ print $2 }'
+        cmd: |-
+          set -o pipefail
+          authselect current | tail -n+3 | awk '{ print $2 }'
+        executable: /bin/bash
       register: result_authselect_features
       changed_when: false
       check_mode: false
@@ -735,6 +770,7 @@
       ansible.builtin.command:
         cmd: authselect create-profile hardening -b {{ authselect_current_profile
           }}
+      changed_when: true
       when:
       - result_authselect_profile is not skipped
       - result_authselect_check_cmd is success
@@ -745,6 +781,7 @@
         based on sssd profile'
       ansible.builtin.command:
         cmd: authselect create-profile hardening -b sssd
+      changed_when: true
       when:
       - result_authselect_profile is not skipped
       - result_authselect_check_cmd is success
@@ -754,6 +791,7 @@
     - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
       ansible.builtin.command:
         cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
+      changed_when: true
       when:
       - result_authselect_check_cmd is success
       - result_authselect_profile is not skipped
@@ -766,6 +804,7 @@
         cmd: authselect select {{ authselect_custom_profile }} {{ result_authselect_features.stdout_lines
           | join(' ') }}
       register: result_pam_authselect_select_profile
+      changed_when: true
       when:
       - result_authselect_check_cmd is success
       - result_authselect_profile is not skipped
@@ -775,6 +814,7 @@
     - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
       ansible.builtin.command:
         cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
+      changed_when: true
       when:
       - result_authselect_check_cmd is success
       - result_authselect_profile is not skipped
@@ -841,6 +881,7 @@
     - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
       ansible.builtin.command:
         cmd: authselect apply-changes -b
+      changed_when: true
       when:
       - result_authselect_present is defined
       - result_authselect_present.stat.exists
@@ -896,6 +937,7 @@
   - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
     ansible.builtin.command:
       cmd: authselect apply-changes -b
+    changed_when: true
     when:
     - result_authselect_present.stat.exists
     - (result_pam_remember_add is defined and result_pam_remember_add.changed) or

ansible remediation for rule 'xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_use_authtok' differs.
--- xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_use_authtok
+++ xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_use_authtok
@@ -60,7 +60,10 @@
 
   - name: Enforce Password History with use_authtok - Get authselect current profile
     ansible.builtin.shell:
-      cmd: authselect current -r | awk '{ print $1 }'
+      cmd: |-
+        set -o pipefail
+        authselect current -r | awk '{ print $1 }'
+      executable: /bin/bash
     register: result_authselect_profile
     changed_when: false
     when:
@@ -87,7 +90,10 @@
   - name: Enforce Password History with use_authtok - Get authselect current features
       to also enable them in the 

... The diff is trimmed here ...

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Change in Ansible shell module found.

Please consider using more suitable Ansible module than shell if possible.

@ggbecker
ggbecker force-pushed the fix-ansible-lint-certification branch from 6d3b454 to f19aa57 Compare October 2, 2026 22:54
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Change in Ansible shell module found.

Please consider using more suitable Ansible module than shell if possible.

@ggbecker ggbecker added this to the 0.1.83 milestone Oct 5, 2026
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Change in Ansible shell module found.

Please consider using more suitable Ansible module than shell if possible.

2 similar comments
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Change in Ansible shell module found.

Please consider using more suitable Ansible module than shell if possible.

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Change in Ansible shell module found.

Please consider using more suitable Ansible module than shell if possible.

@ggbecker
ggbecker force-pushed the fix-ansible-lint-certification branch from 5588b4f to 92a264e Compare October 5, 2026 13:23
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Change in Ansible shell module found.

Please consider using more suitable Ansible module than shell if possible.

A handful of remediations must call a binary directly because no Ansible
module provides the behavior:

* `service auditd restart` - auditd sets RefuseManualStop=yes, so the
  service/systemd modules cannot restart it.
* `rpm -Va`/`rpm -qf`/`rpm -qV`/`rpm --restore`/`rpm --setperms`/
  `rpm --setugids` - the package module has no verify/query-owner/restore
  semantics.
* the rsyslog RainerScript block/range sed removals - neither replace nor
  lineinfile can delete a multilined block conditionally on its content or a
  line range without a fragile DOTALL/lookahead regex, and the companion
  /etc/rsyslog.d/*.conf tasks must iterate a file glob those modules cannot
  target.

These tasks carry an inline `# noqa command-instead-of-module` with the
rationale in the rule and macro sources, but the build re-serializes YAML and
strips comments before the collection is assembled, so the rule is also added
to the global ansible-lint skip list.
Remove command-instead-of-module, risky-file-permissions and the legacy
301/303/403/503 ids from the content CI ansible-lint skip list, and update
the role lint gate workflow so the non-skippable certification rules are
enforced on generated roles.
Add `changed_when: true` to the authselect, grub bootloader and dconf
command tasks (no-changed-when), add `set -o pipefail` to the authselect
feature-detection shell pipes (risky-shell-pipe), and set an explicit file
mode on the sshd/selinux config lineinfile helpers (risky-file-permissions).
Add `set -o pipefail` to shell tasks that pipe through grep/awk/xargs, and
replace the grep alternation and sed address delimiter that relied on the
`|` character (chrony, rsyslog) so the pipeline failure modes are honest.
Convert remediations that shelled out to a binary over to the equivalent
Ansible module: `firewall-cmd` to ansible.posix.firewalld, `rpm --import`
to ansible.builtin.rpm_key, and the systemctl list-unit-files probes to
ansible.builtin.service_facts / ansible.builtin.find.
Set an explicit `changed_when` on command and shell tasks that previously
reported a change on every run (no-changed-when): dconf updates, crypto and
FIPS policy application, grub/zipl boot entry updates, chage/passwd account
changes, smartcard driver config, SELinux context restore, wireless
deactivation, postfix newaliases, usbguard policy generation and the
rpm verify/reinstall helpers.
A handful of hardening remediations must call a binary directly because no
stock Ansible module provides the behavior (auditd restart, rpm verify/
restore/setperms/setugids, multiline RainerScript block/range removal from
rsyslog config). These produced command-instead-of-module ansible-lint
violations in the generated collection roles.

Add a repository-local module utils/ansible_modules/ssg_hardening.py that
encapsulates those operations as discrete named operations, and teach the
collection builder (utils/ansible_roles_to_collection.py) to vendor it into
plugins/modules/ and rewrite the matching command/shell tasks to call it.

The rewrite happens only at collection-build time, so the datastream and
standalone playbooks keep their original command tasks unchanged.
Ansible partner certification's risky-file-permissions rule flags any
file-creating/modifying task (lineinfile with create, ini_file, file with
state) that does not specify an explicit mode. Inline noqa is not accepted
by certification, so every occurrence is fixed with the file's real
expected mode instead.

Templates (lineinfile, key_value_pair_in_file, file_owner, file_groupowner)
gain an optional filemode/FILEMODE var threaded into the generated tasks,
emitted only when set so existing behavior is unchanged. systemd_dropin
and the raw ini_file/lineinfile remediations get modes matching the
packaged defaults (0644 for config files, 0440 sudoers, 0600 usbguard).
Owner/group rules for cron dirs and sshd_config.d get 0700 and /var/log
gets 0755, matching the corresponding file_permissions rules.
AnsibleSnippetsProcessor.get_ansible_tasks() emitted the single
"Gather the service facts" task after the special_service_block tasks
it collected. Those blocks (for example the debug-shell, autofs and
kdump service rules) gate their actions on ansible_facts.services, so
running them before the service_facts task made the generated playbook
fail with:

    object of type 'dict' has no attribute 'services'

Move the service_facts task ahead of the service tasks (still after the
package installs, so services added by a package are seen) so the
special_service_block tasks can rely on ansible_facts.services being
populated. Add a unit test covering the ordering.
The previous commit moved the single "Gather the service facts" task
ahead of the special_service_block tasks so their "when" conditions on
ansible_facts.services could be evaluated. That fixed the masking rules
(debug-shell, autofs, kdump) but regressed rules that assert on a
service's running state afterwards: a special_service_block may change
that state (e.g. start firewalld), and a single early snapshot leaves the
later assertion reading stale facts. This surfaced as the ccn_advanced
"Configure Firewalld to Restrict Loopback Traffic - Informative Message
Based on Service State" task failing with "firewalld service is not
active" even though a preceding block had just started it.

Mirror the package_facts handling: when service blocks are present,
gather the service facts both before and after them, so the blocks see
populated facts and subsequent tasks see the state the blocks changed.
When there are no service blocks, keep the single gather before the
remaining tasks. Covered by a new unit test.
The downstream redhat.rhel_hardening_roles collection enforces
ansible-lint's command-instead-of-module rule for Automation Hub
certification. Command/shell remediations that have a module equivalent
are rewritten to the vendored ssg_hardening module when the collection is
assembled; a new upstream remediation whose command is not mapped would
stay raw and regress certification, which is only discovered at publishing
time today.

Extend the RHEL Ansible-role gate to assemble the collection the same way
downstream does and lint it with just the command-instead-of-module rule
(with ANSIBLE_COLLECTIONS_PATH set so the vendored modules resolve). This
fails the pull request that introduces an unmapped command instead of the
publishing pipeline. Commands without a module equivalent (find, grep,
custom shell, ...) are not flagged by this rule, so they do not cause
false failures.
@ggbecker
ggbecker force-pushed the fix-ansible-lint-certification branch from 92a264e to abaff6f Compare October 6, 2026 12:18
@ggbecker

ggbecker commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

/packit test -i contest-all --env TESTS=/hardening/ansible,/hardening/host-os/ansible --env PLAN=/plans/weekly

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Change in Ansible shell module found.

Please consider using more suitable Ansible module than shell if possible.

@jan-cerny jan-cerny self-assigned this Oct 6, 2026
- name: {{{ rule_title }}} - Find all files that contain {{{ SYSCTLVAR }}}
ansible.builtin.shell:
cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep -HP '^\s*{{{ SYSCTLVAR }}}\s*=\s*.*$'
cmd: |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should |- be used instead? That would be consistent with other occurences of the ansible.builtin.shell module.


- name: "{{{ rule_title }}}: Import RedHat GPG key"
ansible.builtin.command: rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
ansible.builtin.rpm_key:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This year we intentionally replaced the rpm_key module usage by the command module because of the post quantum cryptography, see #14517

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, looks like we will have to bundle this command in the ssg_hardening newly created ansible module as well

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although, it seems to be fixed across 2.16, 2.18, 2.20 and beyond as per: ansible/ansible#86237 (comment)

So, maybe we can reevaluate if we could revert the command replacement. Not for this task, but as a follow up.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created #15175 to track this

playbooks keep the original ``command`` tasks.
"""

from __future__ import absolute_import, division, print_function

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The print_function shouldn't be imported from future. That import would be for Python 2 compatibility but we don't care about Python 2 anymore.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't remove it because of the ansible 2.16, here is the explanation (AI assisted):

The vendored ssg_hardening module must keep from __future__ import absolute_import, division, print_function and __metaclass__ = type because ansible-test's future-import-boilerplate and metaclass-boilerplate sanity tests are still enforced on the ansible-core 2.16 certification floor (they are only dropped in 2.18+), so removing them fails the 2.16 sanity gate even though the code itself is Python 3-only.

@comps

comps commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

Okay, the /packit test errored out on used up disk space. I guess I never tested it on with-gui tests before, sorry.

@comps

comps commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

/packit test -i contest-all --env TESTS=/hardening/ansible --env PLAN=/plans/weekly

@packit-as-a-service

Copy link
Copy Markdown

Account comps has no write access nor is author of PR!

@ggbecker

ggbecker commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

/packit test -i contest-all --env TESTS=/hardening/ansible --env PLAN=/plans/weekly

Comment thread .config/ansible-lint.yml
Comment on lines +44 to +46
# in the rule source playbooks, but the build re-serializes YAML and strips
# comments before the collection is assembled, so a global skip is required.
- command-instead-of-module

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand it well that it means that the gating CI job can't verify whether our code meets the certification requirements because of this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread utils/ansible_modules/ssg_hardening.py Outdated
type="str",
required=True,
choices=[
"service_restart",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the operations and their parameters be defined at a single place, ie. by having a constant dict at the beginning of the file, instead of having the definitions spread over the place?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

ansible.builtin.shell:
cmd: firewall-cmd --get-active-zones | grep -v "^ " | cut -d " " -f 1
cmd: |-
set -o pipefail

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allegedly (AI says) that we need to add executable: /bin/bash so that we can use set -o pipefail in the command

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the bash part as well

name: key_value_pair_in_file
vars:
path: '/etc/usbguard/usbguard-daemon.conf'
filemode: '0644'

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

~ 0 $ ls -la /etc/usbguard/usbguard-daemon.conf
-rw-------. 1 root root 5366 Apr 15 02:00 /etc/usbguard/usbguard-daemon.conf

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0600 instead

The risky-file-permissions certification rule requires every
file-creating/modifying task to declare a mode, but pinning a literal
mode rewrites the permissions of files that already exist. That loosens
modes that were deliberately hardened (e.g. /etc/sudoers.d 0700 -> 0750)
and breaks distro defaults (e.g. Ubuntu /var/log root:syslog group
write).

Instead of pinning a literal mode, stat the target first and emit a
templated mode that keeps the current permissions when the path exists,
falling back to the packaged default only on creation. A templated mode
still satisfies risky-file-permissions, so certification passes without
loosening anything.

- file_owner / file_groupowner: directory and single-file branches stat
  the path and preserve the existing mode; FILEMODE becomes a
  creation-only fallback.
- key_value_pair_in_file / lineinfile: stat the config file once and
  preserve its mode, defaulting to the configured filemode or 0644 only
  when the file is created. This also closes the gap where template
  rules without an explicit filemode emitted create: true with no mode.
- systemd_dropin_configuration: preserve the mode of existing drop-in
  files and quote the 0644 creation default (unquoted octal tripped
  yaml[octal-values]).
- ansible_lineinfile macro: double-quote the emitted mode so templated
  expressions are valid YAML.
- configure_usbguard_auditbackend: correct the daemon.conf mode to 0600.
The `set -o pipefail` lines added to honor risky-shell-pipe only work in
bash. Ansible's shell/command module runs under /bin/sh, which is dash on
Debian/Ubuntu (these remediations are multi_platform_all) and does not
support `pipefail`, so the task errors out there. It worked on RHEL only
because /bin/sh is bash.

Add `executable: /bin/bash` to every shell task we gave `set -o pipefail`
in the certification work, converting the free-form `shell:` scalars to the
expanded `cmd:`/`executable:` form where needed.
…_hardening

The certification work replaced `rpm --import` with the ansible.builtin.rpm_key
module for ensure_redhat_gpgkey_installed, undoing a deliberate upstream change
(see ComplianceAsCode#14517): rpm_key cannot import post-quantum (PQC) keys, so the Red Hat key
format requires `rpm --import`.

Restore the `rpm --import` command task (the datastream and standalone
playbooks keep it, PQC-correct) and add an `rpm_import` operation to the
vendored ssg_hardening module plus the matching rewrite rule, so the generated
collection calls the module instead of a raw command and stays free of
command-instead-of-module violations.

The other GPG-key rules (almalinux, amazon, oracle, suse) already used rpm_key
before the certification work and are out of scope; they are not affected by
the RHEL PQC key format.
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Change in Ansible shell module found.

Please consider using more suitable Ansible module than shell if possible.

Switch the shell `cmd:` blocks introduced for the pipefail remediations from
`|` to `|-` so they match the chomping indicator used by the other
ansible.builtin.shell occurrences in the content. Addresses review feedback.
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Change in Ansible shell module found.

Please consider using more suitable Ansible module than shell if possible.

Define every operation once in a single OPERATIONS table (required params,
read-only flag, argv builder) and derive the argument spec choices, the
required_if rules, the read-only set and the command dispatch from it,
instead of repeating the operation list in four separate places.

No behavior change: the derived choices, required_if, read-only set and argv
for every operation match the previous hardcoded definitions.

The Python 2 compatibility boilerplate (`from __future__ import ...` and
`__metaclass__ = type`) is intentionally kept: ansible-test's
future-import-boilerplate and metaclass-boilerplate sanity tests are enforced
on the ansible-core 2.16 certification floor, and the collection declares
requires_ansible ">=2.9", so both lines are mandatory even though the code
itself is Python 3 only.
@ggbecker
ggbecker force-pushed the fix-ansible-lint-certification branch from 86a8cf1 to 2ef8d00 Compare October 8, 2026 21:36
@ggbecker

ggbecker commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

/packit test -i contest-all --env TESTS=/hardening/ansible --env PLAN=/plans/weekly

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Change in Ansible shell module found.

Please consider using more suitable Ansible module than shell if possible.

The collection builder now writes a .ansible-lint into the collection root so it
travels inside the built tarball. ansible-lint auto-discovers it in the
collection directory, so the production-profile waivers apply not only to the
GitHub partner-certification-checker (which lints the source repo) but also to
galaxy-importer when the tarball is uploaded directly to Red Hat Automation Hub.

Previously the config existed only in the downstream publishing repo and was
absent from the tarball, so a direct Automation Hub upload would have linted the
collection with ansible-lint defaults (no profile, no waivers, no exclude_paths),
re-triggering the generated-content findings and the RHEL 10 schema[meta] issue.
Raise min_ansible_version from 2.9 to 2.16 so the generated collection's
meta/runtime.yml, the standalone roles' meta/main.yml and the version-assert
pre_task in generated playbooks all require a currently-supported ansible-core.
ansible-core 2.9 through 2.15 are end-of-life, and 2.16 is the floor exercised by
the collection's Automation Hub certification matrix.
@ggbecker
ggbecker force-pushed the fix-ansible-lint-certification branch from 2ef8d00 to 591e422 Compare October 12, 2026 11:22
@github-actions

Copy link
Copy Markdown
Contributor

Change in Ansible shell module found.

Please consider using more suitable Ansible module than shell if possible.

@ggbecker

Copy link
Copy Markdown
Member Author

Superseded. To make review easier, the changes in this PR have been split into four focused, independently reviewable pull requests (they touch disjoint files and can be merged in any order):

Together these four cover all the changes from this PR. Closing in favor of them.

@ggbecker ggbecker closed this Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progress Used by openshift-ci bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants