Guide to the Secure Configuration of Red Hat Enterprise Linux 9 | OpenSCAP Security Guide

## Guide to the Secure Configuration of Red Hat Enterprise Linux 9

> with profile Centro Criptológico Nacional (CCN) - STIC for Red Hat Enterprise Linux 9 - Basic
> This profile defines a baseline that aligns with the "Basic" configuration of the CCN-STIC-610A22 Guide issued by the National Cryptological Center of Spain in 2022-10. The CCN-STIC-610A22 guide includes hardening settings for Red Hat Enterprise Linux 9 at basic, intermediate, and advanced levels.

The SCAP Security Guide Project
https://www.open-scap.org/security-policies/scap-security-guide
This guide presents a catalog of security-relevant configuration settings for Red Hat Enterprise Linux 9. It is a rendering of content structured in the eXtensible Configuration Checklist Description Format (XCCDF) in order to support security automation. The SCAP content is is available in the `scap-security-guide` package which is developed at https://www.open-scap.org/security-policies/scap-security-guide.
Providing system administrators with such guidance informs them how to securely configure systems under their control in a variety of network roles. Policy makers and baseline creators can use this catalog of settings, with its associated references to higher-level security control catalogs, in order to assist them in security baseline creation. This guide is a catalog, not a checklist, and satisfaction of every item is not likely to be possible or sensible in many operational scenarios. However, the XCCDF format enables granular selection and adjustment of settings, and their association with OVAL and OCIL content provides an automated checking capability. Transformations of this document, and its associated automated checking content, are capable of providing baselines that meet a diverse set of policy objectives. Some example XCCDF Profiles, which are selections of items that form checklists and can be used as baselines, are available with this guide. They can be processed, in an automated fashion, with tools that support the Security Content Automation Protocol (SCAP). The DISA STIG, which provides required settings for US Department of Defense systems, is one example of a baseline created from this guidance.

This benchmark is a direct port of a SCAP Security Guide benchmark developed for Red Hat Enterprise Linux. It has been modified through an automated process to remove specific dependencies on Red Hat Enterprise Linux and to function with CentOS. The result is a generally useful SCAP Security Guide benchmark with the following caveats:

- CentOS is not an exact copy of Red Hat Enterprise Linux. There may be configuration differences that produce false positives and/or false negatives. If this occurs please file a bug report.
- CentOS has its own build system, compiler options, patchsets, and is a community supported, non-commercial operating system. CentOS does not inherit certifications or evaluations from Red Hat Enterprise Linux. As such, some configuration rules (such as those requiring FIPS 140-2 encryption) will continue to fail on CentOS.

Members of the CentOS community are invited to participate in OpenSCAP and SCAP Security Guide development. Bug reports and patches can be sent to GitHub: https://github.com/ComplianceAsCode/content. The mailing list is at https://fedorahosted.org/mailman/listinfo/scap-security-guide.

Do not attempt to implement any of the settings in this guide without first testing them in a non-operational environment. The creators of this guidance assume no responsibility whatsoever for its use by other parties, and makes no guarantees, expressed or implied, about its quality, reliability, or any other characteristic.

## Profile Information

Profile Title | Centro Criptológico Nacional (CCN) - STIC for Red Hat Enterprise Linux 9 - Basic
Profile ID | xccdf_org.ssgproject.content_profile_ccn_basic

#### CPE Platforms

- cpe:/o:redhat:enterprise_linux:9
- cpe:/o:centos:centos:9

## Revision History

Current version: 0.1.80

- draft (as of 2026-03-06)

## Table of Contents

1. System Settings
   1. Installing and Maintaining Software
   2. Account and Access Control
   3. GRUB2 bootloader configuration
   4. Network Configuration and Firewalls
   5. File Permissions and Masks
2. Services
   1. SSH Server
   2. USBGuard daemon
3. System Accounting with auditd
   1. Configure auditd Rules for Comprehensive Auditing
   2. Configure auditd Data Retention

## Checklist

Group Guide to the Secure Configuration of Red Hat Enterprise Linux 9 Group contains 41 groups and 101 rules
Group System Settings Group contains 31 groups and 68 rules
[ref] Contains rules that check correct system settings.
Group Installing and Maintaining Software Group contains 4 groups and 4 rules
[ref] The following sections contain information on security-relevant choices during the initial operating system installation process and the setup of software updates.
Group System and Software Integrity Group contains 1 group and 1 rule
[ref] System and software integrity can be gained by installing antivirus, increasing system encryption strength with FIPS, verifying installed software, enabling SELinux, installing an Intrusion Prevention System, etc. However, installing or enabling integrity checking tools cannot prevent intrusions, but they can detect that an intrusion may have occurred. Requirements for integrity checking may be highly dependent on the environment in which the system will be used. Snapshot-based approaches such as AIDE may induce considerable overhead in the presence of frequent software updates.
Group System Cryptographic Policies Group contains 1 rule
[ref] Linux has the capability to centrally configure cryptographic polices. The command `update-crypto-policies` is used to set the policy applicable for the various cryptographic back-ends, such as SSL/TLS libraries. The configured cryptographic policies will be the default policy used by these backends unless the application user configures them otherwise. When the system has been configured to use the centralized cryptographic policies, the administrator is assured that any application that utilizes the supported backends will follow a policy that adheres to the configured profile. Currently the supported backends are: GnuTLS library OpenSSL library NSS library OpenJDK Libkrb5 BIND OpenSSH Applications and languages which rely on any of these backends will follow the system policies as well. Examples are apache httpd, nginx, php, and others.
#### Rule Configure System Cryptography Policy [ref]
To configure the system cryptography policy to use ciphers only from the `DEFAULT` policy, run the following command:
    $ sudo update-crypto-policies --set DEFAULT
The rule checks if settings for selected crypto policy are configured as expected. Configuration files in the `/etc/crypto-policies/back-ends` are either symlinks to correct files provided by Crypto-policies package or they are regular files in case crypto policy customizations are applied. Crypto policies may be customized by crypto policy modules, in which case it is delimited from the base policy using a colon. Warning: The system needs to be rebooted for these changes to take effect. Warning: System Crypto Modules must be provided by a vendor that undergoes FIPS-140 certifications. FIPS-140 is applicable to all Federal agencies that use cryptographic-based security systems to protect sensitive information in computer and telecommunication systems (including voice systems) as defined in Section 5131 of the Information Technology Management Reform Act of 1996, Public Law 104-106. This standard shall be used in designing and implementing cryptographic modules that Federal departments and agencies operate or are operated for them under contract. See https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.140-2.pdf To meet this, the system has to have cryptographic software provided by a vendor that has undergone this certification. This means providing documentation, test results, design information, and independent third party review by an accredited lab. While open source software is capable of meeting this, it does not meet FIPS-140 unless the vendor submits to this process.
Rationale: | Centralized cryptographic policies simplify applying secure ciphers across an operating system and the applications that run on that operating system. Use of weak or untested encryption algorithms undermines the purposes of utilizing encryption to protect data.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_configure_crypto_policy
References: |
hipaa | 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.312(e)(1), 164.312(e)(2)(ii)
nerc-cip | CIP-003-8 R4.2, CIP-007-3 R5.1, CIP-007-3 R7.1
nist | AC-17(a), AC-17(2), CM-6(a), MA-4(6), SC-13, SC-12(2), SC-12(3)
ospp | FCS_COP.1(1), FCS_COP.1(2), FCS_COP.1(3), FCS_COP.1(4), FCS_CKM.1, FCS_CKM.2, FCS_TLSC_EXT.1
os-srg | SRG-OS-000396-GPOS-00176, SRG-OS-000393-GPOS-00173, SRG-OS-000394-GPOS-00174
ism | 1446
pcidss4 | 2.2.7, 2.2
stigref | SV-258241r1106302_rule
Remediation Kubernetes snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        systemd:
          units:
            - name: configure-crypto-policy.service
              enabled: true
              contents: |
                [Unit]
                Before=kubelet.service
                [Service]
                Type=oneshot
                ExecStart=update-crypto-policies --set {{.var_system_crypto_policy}}
                RemainAfterExit=yes
                [Install]
                WantedBy=multi-user.target
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    var_system_crypto_policy='DEFAULT'

    stderr_of_call=$(update-crypto-policies --set ${var_system_crypto_policy} 2>&1 > /dev/null)
    rc=$?

    if test "$rc" = 127; then
    	echo "$stderr_of_call" >&2
    	echo "Make sure that the script is installed on the remediated system." >&2
    	echo "See output of the 'dnf provides update-crypto-policies' command" >&2
    	echo "to see what package to (re)install" >&2

    	false  # end with an error code
    elif test "$rc" != 0; then
    	echo "Error invoking the update-crypto-policies script: $stderr_of_call" >&2
    	false  # end with an error code
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: XCCDF Value var_system_crypto_policy # promote to variable
      set_fact:
        var_system_crypto_policy: !!str DEFAULT
      tags:
        - always

    - name: Configure System Cryptography Policy - Check current crypto policy (runtime)
      ansible.builtin.command: /usr/bin/update-crypto-policies --show
      register: current_crypto_policy
      changed_when: false
      failed_when: false
      check_mode: false
      tags:
      - DISA-STIG-RHEL-09-215105
      - DISA-STIG-RHEL-09-672030
      - NIST-800-53-AC-17(2)
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-MA-4(6)
      - NIST-800-53-SC-12(2)
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SC-13
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.7
      - configure_crypto_policy
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - restrict_strategy

    - name: Configure System Cryptography Policy - Get mtime of /etc/crypto-policies/config
      ansible.builtin.stat:
        path: /etc/crypto-policies/config
      register: config_file_stat
      changed_when: false
      failed_when: false
      check_mode: false
      tags:
      - DISA-STIG-RHEL-09-215105
      - DISA-STIG-RHEL-09-672030
      - NIST-800-53-AC-17(2)
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-MA-4(6)
      - NIST-800-53-SC-12(2)
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SC-13
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.7
      - configure_crypto_policy
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - restrict_strategy

    - name: Configure System Cryptography Policy - Get mtime of /etc/crypto-policies/state/current
      ansible.builtin.stat:
        path: /etc/crypto-policies/state/current
      register: current_file_stat
      changed_when: false
      failed_when: false
      check_mode: false
      tags:
      - DISA-STIG-RHEL-09-215105
      - DISA-STIG-RHEL-09-672030
      - NIST-800-53-AC-17(2)
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-MA-4(6)
      - NIST-800-53-SC-12(2)
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SC-13
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.7
      - configure_crypto_policy
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - restrict_strategy

    - name: Configure System Cryptography Policy - Check existence of /etc/crypto-policies/back-ends/nss.config
      ansible.builtin.stat:
        path: /etc/crypto-policies/back-ends/nss.config
      register: nss_config_stat
      changed_when: false
      failed_when: false
      check_mode: false
      tags:
      - DISA-STIG-RHEL-09-215105
      - DISA-STIG-RHEL-09-672030
      - NIST-800-53-AC-17(2)
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-MA-4(6)
      - NIST-800-53-SC-12(2)
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SC-13
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.7
      - configure_crypto_policy
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - restrict_strategy

    - 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
        }}
      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)
      tags:
      - DISA-STIG-RHEL-09-215105
      - DISA-STIG-RHEL-09-672030
      - NIST-800-53-AC-17(2)
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-MA-4(6)
      - NIST-800-53-SC-12(2)
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SC-13
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.7
      - configure_crypto_policy
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - restrict_strategy
Group GNOME Desktop Environment Group contains 1 rule
[ref] GNOME is a graphical desktop environment bundled with many Linux distributions that allow users to easily interact with the operating system graphically rather than textually. The GNOME Graphical Display Manager (GDM) provides login, logout, and user switching contexts as well as display server management. GNOME is developed by the GNOME Project and is considered the default Red Hat Graphical environment. For more information on GNOME and the GNOME Project, see https://www.gnome.org.
#### Rule Make sure that the dconf databases are up-to-date with regards to respective keyfiles [ref]
By default, DConf uses a binary database as a data backend. The system-level database is compiled from keyfiles in the /etc/dconf/db/ directory by the
    dconf update
command. More specifically, content present in the following directories:
    /etc/dconf/db/distro.d
    /etc/dconf/db/local.d
Rationale: | Unlike text-based keyfiles, the binary database is impossible to check by OVAL. Therefore, in order to evaluate dconf configuration, both have to be true at the same time - configuration files have to be compliant, and the database needs to be more recent than those keyfiles, which gives confidence that it reflects them.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_dconf_db_up_to_date
References: |
hipaa | 164.308(a)(1)(ii)(B), 164.308(a)(5)(ii)(A)
pcidss | Req-6.2
os-srg | SRG-OS-000480-GPOS-00227
pcidss4 | 8.2.8, 8.2
ccn | reload_dconf_db
cis | reload_dconf_db
stigref | SV-258028r991589_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q gdm && { rpm --quiet -q kernel-core; }; then

    dconf update

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | unknown
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-271090
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - dconf_db_up_to_date
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed
      - unknown_strategy

    - name: Make sure that the dconf databases are up-to-date with regards to respective
        keyfiles - Get database modification time for distro
      ansible.builtin.stat:
        path: /etc/dconf/db/distro
      register: distro_db
      when:
      - '"gdm" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-271090
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - dconf_db_up_to_date
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed
      - unknown_strategy

    - name: Make sure that the dconf databases are up-to-date with regards to respective
        keyfiles - Get keyfiles for distro
      ansible.builtin.find:
        paths: /etc/dconf/db/distro.d/
      register: distro_keyfiles
      when:
      - '"gdm" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-271090
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - dconf_db_up_to_date
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed
      - unknown_strategy

    - name: Make sure that the dconf databases are up-to-date with regards to respective
        keyfiles - Run dconf update for distro
      ansible.builtin.command:
        cmd: dconf update
      when:
      - '"gdm" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - not distro_db.stat.exists or distro_keyfiles.files | length > 0 and distro_keyfiles.files
        | map(attribute='mtime') | max > distro_db.stat.mtime
      tags:
      - DISA-STIG-RHEL-09-271090
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - dconf_db_up_to_date
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed
      - unknown_strategy

    - name: Make sure that the dconf databases are up-to-date with regards to respective
        keyfiles - Get database modification time for local
      ansible.builtin.stat:
        path: /etc/dconf/db/local
      register: local_db
      when:
      - '"gdm" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-271090
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - dconf_db_up_to_date
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed
      - unknown_strategy

    - name: Make sure that the dconf databases are up-to-date with regards to respective
        keyfiles - Get keyfiles for local
      ansible.builtin.find:
        paths: /etc/dconf/db/local.d/
      register: local_keyfiles
      when:
      - '"gdm" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-271090
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - dconf_db_up_to_date
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed
      - unknown_strategy

    - name: Make sure that the dconf databases are up-to-date with regards to respective
        keyfiles - Run dconf update for local
      ansible.builtin.command:
        cmd: dconf update
      when:
      - '"gdm" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - not local_db.stat.exists or local_keyfiles.files | length > 0 and local_keyfiles.files
        | map(attribute='mtime') | max > local_db.stat.mtime
      tags:
      - DISA-STIG-RHEL-09-271090
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - dconf_db_up_to_date
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed
      - unknown_strategy
Group Sudo Group contains 2 rules
[ref] `Sudo`, which stands for "su 'do'", provides the ability to delegate authority to certain users, groups of users, or system administrators. When configured for system users and/or groups, `Sudo` can allow a user or group to execute privileged commands that normally only `root` is allowed to execute. For more information on `Sudo` and addition `Sudo` configuration options, see https://www.sudo.ws.
#### Rule Ensure Users Re-Authenticate for Privilege Escalation - sudo [ref]
The sudo `NOPASSWD` and `!authenticate` option, when specified, allows a user to execute commands using sudo without having to authenticate. This should be disabled by making sure that `NOPASSWD` and/or `!authenticate` do not exist in `/etc/sudoers` configuration file or any sudo configuration snippets in `/etc/sudoers.d/`."
Rationale: | Without re-authentication, users may access resources or perform tasks for which they do not have authorization. When operating systems provide the capability to escalate a functional capability, it is critical that the user re-authenticate.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sudo_require_authentication
References: |
cis-csc | 1, 12, 15, 16, 5
cobit5 | DSS05.04, DSS05.10, DSS06.03, DSS06.10
isa-62443-2009 | 4.3.3.5.1, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9
iso27001-2013 | A.18.1.4, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3
nist | IA-11, CM-6(a)
nist-csf | PR.AC-1, PR.AC-7
os-srg | SRG-OS-000373-GPOS-00156
ism | 1546
pcidss4 | 2.2.6, 2.2
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    for f in /etc/sudoers /etc/sudoers.d/* ; do
      if [ ! -e "$f" ] ; then
        continue
      fi
      matching_list=$(grep -P '^(?!#).*[\s]+NOPASSWD[\s]*\:.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          # comment out "NOPASSWD" matches to preserve user data
          sed -i "s|^${entry}$|# &|g" $f
        done <<< "$matching_list"

        /usr/sbin/visudo -cf $f &> /dev/null || echo "Fail to validate $f with visudo"
      fi
    done

    for f in /etc/sudoers /etc/sudoers.d/* ; do
      if [ ! -e "$f" ] ; then
        continue
      fi
      matching_list=$(grep -P '^(?!#).*[\s]+\!authenticate.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          # comment out "!authenticate" matches to preserve user data
          sed -i "s|^${entry}$|# &|g" $f
        done <<< "$matching_list"

        /usr/sbin/visudo -cf $f &> /dev/null || echo "Fail to validate $f with visudo"
      fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-11
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_require_authentication

    - name: Find /etc/sudoers.d/ files
      ansible.builtin.find:
        paths:
        - /etc/sudoers.d/
      register: sudoers
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-11
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_require_authentication

    - name: Remove lines containing NOPASSWD from sudoers files
      ansible.builtin.replace:
        regexp: (^(?!#).*[\s]+NOPASSWD[\s]*\:.*$)
        replace: '# \g<1>'
        path: '{{ item.path }}'
        validate: /usr/sbin/visudo -cf %s
      with_items:
      - path: /etc/sudoers
      - '{{ sudoers.files }}'
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-11
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_require_authentication

    - name: Find /etc/sudoers.d/ files
      ansible.builtin.find:
        paths:
        - /etc/sudoers.d/
      register: sudoers
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-11
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_require_authentication

    - name: Remove lines containing !authenticate from sudoers files
      ansible.builtin.replace:
        regexp: (^(?!#).*[\s]+\!authenticate.*$)
        replace: '# \g<1>'
        path: '{{ item.path }}'
        validate: /usr/sbin/visudo -cf %s
      with_items:
      - path: /etc/sudoers
      - '{{ sudoers.files }}'
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-11
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_require_authentication
#### Rule Require Re-Authentication When Using the sudo Command [ref]
The sudo `timestamp_timeout` tag sets the amount of time sudo password prompt waits. The default `timestamp_timeout` value is 5 minutes. The timestamp_timeout should be configured by making sure that the `timestamp_timeout` tag exists in `/etc/sudoers` configuration file or any sudo configuration snippets in `/etc/sudoers.d/`. If the value is set to an integer less than 0, the user's time stamp will not expire and the user will not have to re-authenticate for privileged actions until the user's session is terminated.
Rationale: | Without re-authentication, users may access resources or perform tasks for which they do not have authorization. When operating systems provide the capability to escalate a functional capability, it is critical that the user re-authenticate.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sudo_require_reauthentication
References: |
nist | IA-11
os-srg | SRG-OS-000373-GPOS-00156, SRG-OS-000373-GPOS-00157, SRG-OS-000373-GPOS-00158
pcidss4 | 2.2.6, 2.2
cis | 5.2.6
stigref | SV-258084r1050789_rule
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q sudo; }; then

    var_sudo_timestamp_timeout='5'

    if grep -Px '^[\s]*Defaults.*timestamp_timeout[\s]*=.*' /etc/sudoers.d/*; then
        find /etc/sudoers.d/ -type f -exec sed -Ei "/^[[:blank:]]*Defaults.*timestamp_timeout[[:blank:]]*=.*/d" {} \;
    fi

    if /usr/sbin/visudo -qcf /etc/sudoers; then
        cp /etc/sudoers /etc/sudoers.bak
        if ! grep -P '^[\s]*Defaults.*timestamp_timeout[\s]*=[\s]*[-]?\w+.*$' /etc/sudoers; then
            # sudoers file doesn't define Option timestamp_timeout
            echo "Defaults timestamp_timeout=${var_sudo_timestamp_timeout}" >> /etc/sudoers
        else
            # sudoers file defines Option timestamp_timeout, remediate wrong values if present
            if grep -qP "^[\s]*Defaults\s.*\btimestamp_timeout[\s]*=[\s]*(?!${var_sudo_timestamp_timeout}\b)[-]?\w+\b.*$" /etc/sudoers; then
                sed -Ei "s/(^[[:blank:]]*Defaults.*timestamp_timeout[[:blank:]]*=)[[:blank:]]*[-]?\w+(.*$)/\1${var_sudo_timestamp_timeout}\2/" /etc/sudoers
            fi
        fi

        # Check validity of sudoers and cleanup bak
        if /usr/sbin/visudo -qcf /etc/sudoers; then
            rm -f /etc/sudoers.bak
        else
            echo "Fail to validate remediated /etc/sudoers, reverting to original file."
            mv /etc/sudoers.bak /etc/sudoers
            false
        fi
    else
        echo "Skipping remediation, /etc/sudoers failed to validate"
        false
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-432015
      - NIST-800-53-IA-11
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_require_reauthentication
    - name: XCCDF Value var_sudo_timestamp_timeout # promote to variable
      set_fact:
        var_sudo_timestamp_timeout: !!str 5
      tags:
        - always

    - name: Require Re-Authentication When Using the sudo Command - Find /etc/sudoers.d/*
        files containing 'Defaults timestamp_timeout'
      ansible.builtin.find:
        path: /etc/sudoers.d
        patterns: '*'
        contains: ^[\s]*Defaults\s.*\btimestamp_timeout[\s]*=.*
      register: sudoers_d_defaults_timestamp_timeout
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"sudo" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-432015
      - NIST-800-53-IA-11
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_require_reauthentication

    - name: Require Re-Authentication When Using the sudo Command - Remove 'Defaults timestamp_timeout'
        from /etc/sudoers.d/* files
      ansible.builtin.lineinfile:
        path: '{{ item.path }}'
        regexp: ^[\s]*Defaults\s.*\btimestamp_timeout[\s]*=.*
        state: absent
      with_items: '{{ sudoers_d_defaults_timestamp_timeout.files }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"sudo" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-432015
      - NIST-800-53-IA-11
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_require_reauthentication

    - name: Require Re-Authentication When Using the sudo Command - Ensure timestamp_timeout
        has the appropriate value in /etc/sudoers
      ansible.builtin.lineinfile:
        path: /etc/sudoers
        regexp: ^[\s]*Defaults\s(.*)\btimestamp_timeout[\s]*=[\s]*[-]?\w+\b(.*)$
        line: Defaults \1timestamp_timeout={{ var_sudo_timestamp_timeout }}\2
        validate: /usr/sbin/visudo -cf %s
        backrefs: true
      register: edit_sudoers_timestamp_timeout_option
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"sudo" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-432015
      - NIST-800-53-IA-11
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_require_reauthentication

    - name: Require Re-Authentication When Using the sudo Command - Enable timestamp_timeout
        option with correct value in /etc/sudoers
      ansible.builtin.lineinfile:
        path: /etc/sudoers
        line: Defaults timestamp_timeout={{ var_sudo_timestamp_timeout }}
        validate: /usr/sbin/visudo -cf %s
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"sudo" in ansible_facts.packages'
      - |
        edit_sudoers_timestamp_timeout_option is defined and not edit_sudoers_timestamp_timeout_option.changed
      tags:
      - DISA-STIG-RHEL-09-432015
      - NIST-800-53-IA-11
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_require_reauthentication

    - name: Require Re-Authentication When Using the sudo Command - Remove timestamp_timeout
        wrong values in /etc/sudoers
      ansible.builtin.lineinfile:
        path: /etc/sudoers
        regexp: ^[\s]*Defaults\s.*\btimestamp_timeout[\s]*=[\s]*(?!{{ var_sudo_timestamp_timeout
          }}\b)[-]?\w+\b.*$
        state: absent
        validate: /usr/sbin/visudo -cf %s
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"sudo" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-432015
      - NIST-800-53-IA-11
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_require_reauthentication
Group Account and Access Control Group contains 9 groups and 24 rules
[ref] In traditional Unix security, if an attacker gains shell access to a certain login account, they can perform any action or access any file to which that account has access. Therefore, making it more difficult for unauthorized people to gain shell access to accounts, particularly to privileged accounts, is a necessary part of securing a system. This section introduces mechanisms for restricting access to accounts under Red Hat Enterprise Linux 9.
Group Warning Banners for System Accesses Group contains 1 group and 5 rules
[ref] Each system should expose as little information about itself as possible. System banners, which are typically displayed just before a login prompt, give out information about the service or the host's operating system. This might include the distribution name and the system kernel version, and the particular version of a network service. This information can assist intruders in gaining access to the system as it can reveal whether the system is running vulnerable software. Most network services can be configured to limit what information is displayed. Many organizations implement security policies that require a system banner provide notice of the system's ownership, provide warning to unauthorized users, and remind authorized users of their consent to monitoring.
Group Implement a GUI Warning Banner Group contains 2 rules
[ref] In the default graphical environment, users logging directly into the system are greeted with a login screen provided by the GNOME Display Manager (GDM). The warning banner should be displayed in this graphical environment for these users. The following sections describe how to configure the GDM login banner.
#### Rule Enable GNOME3 Login Warning Banner [ref]
In the default graphical environment, displaying a login warning banner in the GNOME Display Manager's login screen can be enabled on the login screen by setting `banner-message-enable` to `true`. To enable, add or edit `banner-message-enable` to `/etc/dconf/db/distro.d/00-security-settings`. For example:
    [org/gnome/login-screen]
    banner-message-enable=true
Once the setting has been added, add a lock to `/etc/dconf/db/distro.d/locks/00-security-settings-lock` to prevent user modification. For example:
    /org/gnome/login-screen/banner-message-enable
After the settings have been set, run `dconf update`. The banner text must also be set.
Rationale: | Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. For U.S. Government systems, system use notifications are required only for access via login interfaces with human users and are not required when such human interfaces do not exist.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_dconf_gnome_banner_enabled
References: |
cis-csc | 1, 12, 15, 16
cobit5 | DSS05.04, DSS05.10, DSS06.10
cui | 3.1.9
isa-62443-2009 | 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9
iso27001-2013 | A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3
nist | AC-8(a), AC-8(b), AC-8(c)
nist-csf | PR.AC-7
os-srg | SRG-OS-000023-GPOS-00006, SRG-OS-000228-GPOS-00088
cis | 1.8.2
stigref | SV-258012r1014855_rule, SV-258013r1045082_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q gdm; then

    # Check for setting in any of the DConf db directories
    # If files contain ibus or distro, ignore them.
    # The assignment assumes that individual filenames don't contain :
    readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/login-screen\\]" "/etc/dconf/db/" \
                                    | grep -v 'distro\|ibus\|distro.d' | cut -d":" -f1)
    DCONFFILE="/etc/dconf/db/distro.d/00-security-settings"
    DBDIR="/etc/dconf/db/distro.d"

    mkdir -p "${DBDIR}"

    # Comment out the configurations in databases different from the target one
    if [ "${#SETTINGSFILES[@]}" -ne 0 ]
    then
        if grep -q "^\\s*banner-message-enable\\s*=" "${SETTINGSFILES[@]}"
        then

            sed -Ei "s/(^\s*)banner-message-enable(\s*=)/#\1banner-message-enable\2/g" "${SETTINGSFILES[@]}"
        fi
    fi

    [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}"
    if ! grep -q "\\[org/gnome/login-screen\\]" "${DCONFFILE}"
    then
        printf '%s\n' "[org/gnome/login-screen]" >> ${DCONFFILE}
    fi

    escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")"
    if grep -q "^\\s*banner-message-enable\\s*=" "${DCONFFILE}"
    then
            sed -i "s/\\s*banner-message-enable\\s*=\\s*.*/banner-message-enable=${escaped_value}/g" "${DCONFFILE}"
        else
            sed -i "\\|\\[org/gnome/login-screen\\]|a\\banner-message-enable=${escaped_value}" "${DCONFFILE}"
    fi
    dconf update
    # Check for setting in any of the DConf db directories
    LOCKFILES=$(grep -r "^/org/gnome/login-screen/banner-message-enable$" "/etc/dconf/db/" \
                | grep -v 'distro\|ibus\|distro.d' | grep ":" | cut -d":" -f1)
    LOCKSFOLDER="/etc/dconf/db/distro.d/locks"

    mkdir -p "${LOCKSFOLDER}"

    # Comment out the configurations in databases different from the target one
    if [[ ! -z "${LOCKFILES}" ]]
    then
        sed -i -E "s|^/org/gnome/login-screen/banner-message-enable$|#&|" "${LOCKFILES[@]}"
    fi

    if ! grep -qr "^/org/gnome/login-screen/banner-message-enable$" /etc/dconf/db/distro.d/
    then
        echo "/org/gnome/login-screen/banner-message-enable" >> "/etc/dconf/db/distro.d/locks/00-security-settings-lock"
    fi
    dconf update

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | unknown
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-271010
      - DISA-STIG-RHEL-09-271015
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(b)
      - NIST-800-53-AC-8(c)
      - dconf_gnome_banner_enabled
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy

    - name: Enable GNOME3 Login Warning Banner
      community.general.ini_file:
        dest: /etc/dconf/db/distro.d/00-security-settings
        section: org/gnome/login-screen
        option: banner-message-enable
        value: 'true'
        create: true
        no_extra_spaces: true
      register: result_ini
      when: '"gdm" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-271010
      - DISA-STIG-RHEL-09-271015
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(b)
      - NIST-800-53-AC-8(c)
      - dconf_gnome_banner_enabled
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy

    - name: Prevent user modification of GNOME banner-message-enabled
      ansible.builtin.lineinfile:
        path: /etc/dconf/db/distro.d/locks/00-security-settings-lock
        regexp: ^/org/gnome/login-screen/banner-message-enable$
        line: /org/gnome/login-screen/banner-message-enable
        create: true
      register: result_lineinfile
      when: '"gdm" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-271010
      - DISA-STIG-RHEL-09-271015
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(b)
      - NIST-800-53-AC-8(c)
      - dconf_gnome_banner_enabled
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy

    - name: Dconf Update
      ansible.builtin.command: dconf update
      when:
      - '"gdm" in ansible_facts.packages'
      - result_ini is changed or result_lineinfile is changed
      tags:
      - DISA-STIG-RHEL-09-271010
      - DISA-STIG-RHEL-09-271015
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(b)
      - NIST-800-53-AC-8(c)
      - dconf_gnome_banner_enabled
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy
#### Rule Set the GNOME3 Login Warning Banner Text [ref]
In the default graphical environment, configuring the login warning banner text in the GNOME Display Manager's login screen can be configured on the login screen by setting `banner-message-text` to `'APPROVED_BANNER'` where APPROVED_BANNER is the approved banner for your environment. To enable, add or edit `banner-message-text` to `/etc/dconf/db/distro.d/00-security-settings`. For example:
    [org/gnome/login-screen]
    banner-message-text='APPROVED_BANNER'
Once the setting has been added, add a lock to `/etc/dconf/db/distro.d/locks/00-security-settings-lock` to prevent user modification. For example:
    /org/gnome/login-screen/banner-message-text
After the settings have been set, run `dconf update`. When entering a warning banner that spans several lines, remember to begin and end the string with `'` and use `\n` for new lines.
Rationale: | An appropriate warning message reinforces policy awareness during the logon process and facilitates possible legal action against attackers.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_dconf_gnome_login_banner_text
References: |
cis-csc | 1, 12, 15, 16
cobit5 | DSS05.04, DSS05.10, DSS06.10
cui | 3.1.9
isa-62443-2009 | 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9
iso27001-2013 | A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3
nist | AC-8(a), AC-8(c)
nist-csf | PR.AC-7
os-srg | SRG-OS-000023-GPOS-00006, SRG-OS-000228-GPOS-00088
cis | 1.8.2
stigref | SV-270174r1044831_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q gdm; then

    login_banner_text='^(Authorized[\s\n]+users[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.|^(?!.*(\\|fedora|rhel|sle|ubuntu)).*)$'

    # Multiple regexes transform the banner regex into a usable banner
    # 0 - Remove anchors around the banner text
    login_banner_text=$(echo "$login_banner_text" | sed 's/^\^\(.*\)\$$/\1/g')
    # 1 - Keep only the first banners if there are multiple
    #    (dod_banners contains the long and short banner)
    login_banner_text=$(echo "$login_banner_text" | sed 's/^(\(.*\.\)|.*)$/\1/g')
    # 2 - Add spaces ' '. (Transforms regex for "space or newline" into a " ")
    login_banner_text=$(echo "$login_banner_text" | sed 's/\[\\s\\n\]+/ /g')
    # 3 - Adds newline "tokens". (Transforms "(?:\[\\n\]+|(?:\\n)+)" into "(n)*")
    login_banner_text=$(echo "$login_banner_text" | sed 's/(?:\[\\n\]+|(?:\\\\n)+)/(n)*/g')
    # 4 - Remove any leftover backslash. (From any parenthesis in the banner, for example).
    login_banner_text=$(echo "$login_banner_text" | sed 's/\\//g')
    # 5 - Removes the newline "token." (Transforms them into newline escape sequences "\n").
    #    ( Needs to be done after 4, otherwise the escapce sequence will become just "n".
    login_banner_text=$(echo "$login_banner_text" | sed 's/(n)\*/\\n/g')

    # Check for setting in any of the DConf db directories
    # If files contain ibus or distro, ignore them.
    # The assignment assumes that individual filenames don't contain :
    readarray -t SETTINGSFILES < <(grep -r "\\[org/gnome/login-screen\\]" "/etc/dconf/db/" \
                                    | grep -v 'distro\|ibus\|distro.d' | cut -d":" -f1)
    DCONFFILE="/etc/dconf/db/distro.d/00-security-settings"
    DBDIR="/etc/dconf/db/distro.d"

    mkdir -p "${DBDIR}"

    # Comment out the configurations in databases different from the target one
    if [ "${#SETTINGSFILES[@]}" -ne 0 ]
    then
        if grep -q "^\\s*banner-message-text\\s*=" "${SETTINGSFILES[@]}"
        then

            sed -Ei "s/(^\s*)banner-message-text(\s*=)/#\1banner-message-text\2/g" "${SETTINGSFILES[@]}"
        fi
    fi

    [ ! -z "${DCONFFILE}" ] && echo "" >> "${DCONFFILE}"
    if ! grep -q "\\[org/gnome/login-screen\\]" "${DCONFFILE}"
    then
        printf '%s\n' "[org/gnome/login-screen]" >> ${DCONFFILE}
    fi

    escaped_value="$(sed -e 's/\\/\\\\/g' <<< "'${login_banner_text}'")"
    if grep -q "^\\s*banner-message-text\\s*=" "${DCONFFILE}"
    then
            sed -i "s/\\s*banner-message-text\\s*=\\s*.*/banner-message-text=${escaped_value}/g" "${DCONFFILE}"
        else
            sed -i "\\|\\[org/gnome/login-screen\\]|a\\banner-message-text=${escaped_value}" "${DCONFFILE}"
    fi
    dconf update
    # Check for setting in any of the DConf db directories
    LOCKFILES=$(grep -r "^/org/gnome/login-screen/banner-message-text$" "/etc/dconf/db/" \
                | grep -v 'distro\|ibus\|distro.d' | grep ":" | cut -d":" -f1)
    LOCKSFOLDER="/etc/dconf/db/distro.d/locks"

    mkdir -p "${LOCKSFOLDER}"

    # Comment out the configurations in databases different from the target one
    if [[ ! -z "${LOCKFILES}" ]]
    then
        sed -i -E "s|^/org/gnome/login-screen/banner-message-text$|#&|" "${LOCKFILES[@]}"
    fi

    if ! grep -qr "^/org/gnome/login-screen/banner-message-text$" /etc/dconf/db/distro.d/
    then
        echo "/org/gnome/login-screen/banner-message-text" >> "/etc/dconf/db/distro.d/locks/00-security-settings-lock"
    fi
    dconf update

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | unknown
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-171011
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(c)
      - dconf_gnome_login_banner_text
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy
    - name: XCCDF Value login_banner_text # promote to variable
      set_fact:
        login_banner_text: !!str ^(Authorized[\s\n]+users[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.|^(?!.*(\\|fedora|rhel|sle|ubuntu)).*)$
      tags:
        - always

    - name: Set the GNOME3 Login Warning Banner Text
      ansible.builtin.file:
        path: /etc/dconf/db/{{ item }}
        owner: root
        group: root
        mode: 493
        state: directory
      with_items:
      - distro.d
      - distro.d/locks
      when: '"gdm" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-171011
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(c)
      - dconf_gnome_login_banner_text
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy

    - name: Set the GNOME3 Login Warning Banner Text
      ansible.builtin.file:
        path: /etc/dconf/db/distro.d/{{ item }}
        owner: root
        group: root
        mode: 420
        state: touch
      with_items:
      - 00-security-settings
      - locks/00-security-settings-lock
      when: '"gdm" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-171011
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(c)
      - dconf_gnome_login_banner_text
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy

    - name: Set the GNOME3 Login Warning Banner Text
      community.general.ini_file:
        dest: /etc/dconf/db/distro.d/00-security-settings
        section: org/gnome/login-screen
        option: banner-message-text
        value: '''{{ login_banner_text | regex_replace("^\^(.*)\$$", "\1") | regex_replace("^\((.*\.)\|.*\)$",
          "\1") | regex_replace("\[\\s\\n\]\+"," ") | regex_replace("\(\?:\[\\n\]\+\|\(\?:\\\\n\)\+\)",
          "(n)*") | regex_replace("\\", "") | regex_replace("\(n\)\*", "\\n") }}'''
        create: true
        no_extra_spaces: true
      register: result_ini
      when: '"gdm" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-171011
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(c)
      - dconf_gnome_login_banner_text
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy

    - name: Prevent user modification of the GNOME3 Login Warning Banner Text
      ansible.builtin.lineinfile:
        path: /etc/dconf/db/distro.d/locks/00-security-settings-lock
        regexp: ^/org/gnome/login-screen/banner-message-text$
        line: /org/gnome/login-screen/banner-message-text
        create: true
        state: present
      register: result_lineinfile
      when: '"gdm" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-171011
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(c)
      - dconf_gnome_login_banner_text
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy

    - name: Dconf Update
      ansible.builtin.command: dconf update
      when:
      - '"gdm" in ansible_facts.packages'
      - result_ini is changed or result_lineinfile is changed
      tags:
      - DISA-STIG-RHEL-09-171011
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(c)
      - dconf_gnome_login_banner_text
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy
#### Rule Modify the System Login Banner [ref]
To configure the system login banner edit `/etc/issue`. Replace the default text with a message compliant with the local site policy or a legal disclaimer. The DoD required text is either: `You are accessing a U.S. Government (USG) Information System (IS) that is provided for USG-authorized use only. By using this IS (which includes any device attached to this IS), you consent to the following conditions: -The USG routinely intercepts and monitors communications on this IS for purposes including, but not limited to, penetration testing, COMSEC monitoring, network operations and defense, personnel misconduct (PM), law enforcement (LE), and counterintelligence (CI) investigations. -At any time, the USG may inspect and seize data stored on this IS. -Communications using, or data stored on, this IS are not private, are subject to routine monitoring, interception, and search, and may be disclosed or used for any USG-authorized purpose. -This IS includes security measures (e.g., authentication and access controls) to protect USG interests -- not for your personal benefit or privacy. -Notwithstanding the above, using this IS does not constitute consent to PM, LE or CI investigative searching or monitoring of the content of privileged communications, or work product, related to personal representation or services by attorneys, psychotherapists, or clergy, and their assistants. Such communications and work product are private and confidential. See User Agreement for details.` OR: `I've read & consent to terms in IS user agreem't.`
Rationale: | Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. System use notifications are required only for access via login interfaces with human users and are not required when such human interfaces do not exist.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_banner_etc_issue
References: |
cis-csc | 1, 12, 15, 16
cobit5 | DSS05.04, DSS05.10, DSS06.10
cui | 3.1.9
isa-62443-2009 | 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9
iso27001-2013 | A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3
nist | AC-8(a), AC-8(c)
nist-csf | PR.AC-7
os-srg | SRG-OS-000023-GPOS-00006, SRG-OS-000228-GPOS-00088
stigref | SV-257779r958390_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    metadata:
      labels:
        machineconfiguration.openshift.io/role: master
        machineconfiguration.openshift.io/role: worker
      name: 75-banner-etc-issue
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,You%20are%20accessing%20a%20U.S.%20Government%20%28USG%29%20Information%20System%20%28IS%29%20that%20is%20%0Aprovided%20for%20USG-authorized%20use%20only.%20By%20using%20this%20IS%20%28which%20includes%20any%20%0Adevice%20attached%20to%20this%20IS%29%2C%20you%20consent%20to%20the%20following%20conditions%3A%0A%0A-The%20USG%20routinely%20intercepts%20and%20monitors%20communications%20on%20this%20IS%20for%20%0Apurposes%20including%2C%20but%20not%20limited%20to%2C%20penetration%20testing%2C%20COMSEC%20monitoring%2C%20%0Anetwork%20operations%20and%20defense%2C%20personnel%20misconduct%20%28PM%29%2C%20law%20enforcement%20%0A%28LE%29%2C%20and%20counterintelligence%20%28CI%29%20investigations.%0A%0A-At%20any%20time%2C%20the%20USG%20may%20inspect%20and%20seize%20data%20stored%20on%20this%20IS.%0A%0A-Communications%20using%2C%20or%20data%20stored%20on%2C%20this%20IS%20are%20not%20private%2C%20are%20subject%20%0Ato%20routine%20monitoring%2C%20interception%2C%20and%20search%2C%20and%20may%20be%20disclosed%20or%20used%20%0Afor%20any%20USG-authorized%20purpose.%0A%0A-This%20IS%20includes%20security%20measures%20%28e.g.%2C%20authentication%20and%20access%20controls%29%20%0Ato%20protect%20USG%20interests--not%20for%20your%20personal%20benefit%20or%20privacy.%0A%0A-Notwithstanding%20the%20above%2C%20using%20this%20IS%20does%20not%20constitute%20consent%20to%20PM%2C%20LE%20%0Aor%20CI%20investigative%20searching%20or%20monitoring%20of%20the%20content%20of%20privileged%20%0Acommunications%2C%20or%20work%20product%2C%20related%20to%20personal%20representation%20or%20services%20%0Aby%20attorneys%2C%20psychotherapists%2C%20or%20clergy%2C%20and%20their%20assistants.%20Such%20%0Acommunications%20and%20work%20product%20are%20private%20and%20confidential.%20See%20User%20%0AAgreement%20for%20details.
            mode: 0644
            path: /etc/issue.d/legal-notice
            overwrite: true
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    read -r -d '' login_banner_text <<'EOF' || true
    ^(Authorized[\s\n]+users[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.|^(?!.*(\\|fedora|rhel|sle|ubuntu)).*)$
    EOF

    # Multiple regexes transform the banner regex into a usable banner
    # 0 - Remove anchors around the banner text
    login_banner_text=$(echo "$login_banner_text" | sed 's/^\^\(.*\)\$$/\1/g')
    # 1 - Keep only the first banners if there are multiple
    #    (dod_banners contains the long and short banner)
    login_banner_text=$(echo "$login_banner_text" | sed 's/^(\(.*\.\)|.*)$/\1/g')
    # 2 - Add spaces ' '. (Transforms regex for "space or newline" into a " ")
    login_banner_text=$(echo "$login_banner_text" | sed 's/\[\\s\\n\]+/ /g')
    # 3 - Adds newlines. (Transforms "(?:\[\\n\]+|(?:\\n)+)" into "\n")
    login_banner_text=$(echo "$login_banner_text" | sed 's/(?:\[\\n\]+|(?:\\\\n)+)/\n/g')
    # 4 - Remove any leftover backslash. (From any parenthesis in the banner, for example).
    login_banner_text=$(echo "$login_banner_text" | sed 's/\\//g')
    formatted=$(echo "$login_banner_text" | fold -sw 80)
    cat <<EOF >/etc/issue
    $formatted
    EOF

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | unknown
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-211020
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(c)
      - banner_etc_issue
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy
    - name: XCCDF Value login_banner_text # promote to variable
      set_fact:
        login_banner_text: !!str ^(Authorized[\s\n]+users[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.|^(?!.*(\\|fedora|rhel|sle|ubuntu)).*)$
      tags:
        - always

    - name: Modify the System Login Banner - Ensure Correct Banner
      ansible.builtin.copy:
        dest: /etc/issue
        content: '{{ login_banner_text | regex_replace("^\^(.*)\$$", "\1") | regex_replace("^\((.*\.)\|.*\)$",
          "\1") | regex_replace("\[\\s\\n\]\+"," ") | regex_replace("\(\?:\[\\n\]\+\|\(\?:\\\\n\)\+\)",
          "\n") | regex_replace("\\", "") | wordwrap() }}'
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-211020
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(c)
      - banner_etc_issue
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy
#### Rule Modify the System Login Banner for Remote Connections [ref]
To configure the system login banner edit `/etc/issue.net`. Replace the default text with a message compliant with the local site policy or a legal disclaimer. The DoD required text is either: `You are accessing a U.S. Government (USG) Information System (IS) that is provided for USG-authorized use only. By using this IS (which includes any device attached to this IS), you consent to the following conditions: -The USG routinely intercepts and monitors communications on this IS for purposes including, but not limited to, penetration testing, COMSEC monitoring, network operations and defense, personnel misconduct (PM), law enforcement (LE), and counterintelligence (CI) investigations. -At any time, the USG may inspect and seize data stored on this IS. -Communications using, or data stored on, this IS are not private, are subject to routine monitoring, interception, and search, and may be disclosed or used for any USG-authorized purpose. -This IS includes security measures (e.g., authentication and access controls) to protect USG interests -- not for your personal benefit or privacy. -Notwithstanding the above, using this IS does not constitute consent to PM, LE or CI investigative searching or monitoring of the content of privileged communications, or work product, related to personal representation or services by attorneys, psychotherapists, or clergy, and their assistants. Such communications and work product are private and confidential. See User Agreement for details.` OR: `I've read & consent to terms in IS user agreem't.`
Rationale: | Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. System use notifications are required only for access via login interfaces with human users and are not required when such human interfaces do not exist.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_banner_etc_issue_net
References: |
os-srg | SRG-OS-000023-GPOS-00006, SRG-OS-000228-GPOS-00088
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    remote_login_banner_text='^(Authorized[\s\n]+uses[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.|^(?!.*(\\|fedora|rhel|sle|ubuntu)).*)$'

    # Multiple regexes transform the banner regex into a usable banner
    # 0 - Remove anchors around the banner text
    remote_login_banner_text=$(echo "$remote_login_banner_text" | sed 's/^\^\(.*\)\$$/\1/g')
    # 1 - Keep only the first banners if there are multiple
    #    (dod_banners contains the long and short banner)
    remote_login_banner_text=$(echo "$remote_login_banner_text" | sed 's/^(\(.*\.\)|.*)$/\1/g')
    # 2 - Add spaces ' '. (Transforms regex for "space or newline" into a " ")
    remote_login_banner_text=$(echo "$remote_login_banner_text" | sed 's/\[\\s\\n\]+/ /g')
    # 3 - Adds newlines. (Transforms "(?:\[\\n\]+|(?:\\n)+)" into "\n")
    remote_login_banner_text=$(echo "$remote_login_banner_text" | sed 's/(?:\[\\n\]+|(?:\\\\n)+)/\n/g')
    # 4 - Remove any leftover backslash. (From any parenthesis in the banner, for example).
    remote_login_banner_text=$(echo "$remote_login_banner_text" | sed 's/\\//g')
    formatted=$(echo "$remote_login_banner_text" | fold -sw 80)

    cat <<EOF >/etc/issue.net
    $formatted
    EOF

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | unknown
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - banner_etc_issue_net
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy
    - name: XCCDF Value remote_login_banner_text # promote to variable
      set_fact:
        remote_login_banner_text: !!str ^(Authorized[\s\n]+uses[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.|^(?!.*(\\|fedora|rhel|sle|ubuntu)).*)$
      tags:
        - always

    - name: Modify the System Login Banner for Remote Connections - ensure correct banner
      ansible.builtin.copy:
        dest: /etc/issue.net
        content: '{{ remote_login_banner_text | regex_replace("^\^(.*)\$$", "\1") | regex_replace("^\((.*\.)\|.*\)$",
          "\1") | regex_replace("\[\\s\\n\]\+"," ") | regex_replace("\(\?:\[\\n\]\+\|\(\?:\\\\n\)\+\)",
          "\n") | regex_replace("\\", "") | wordwrap() }}'
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - banner_etc_issue_net
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy
#### Rule Modify the System Message of the Day Banner [ref]
To configure the system message banner edit `/etc/motd`. Replace the default text with a message compliant with the local site policy or a legal disclaimer. The DoD required text is either: `You are accessing a U.S. Government (USG) Information System (IS) that is provided for USG-authorized use only. By using this IS (which includes any device attached to this IS), you consent to the following conditions: -The USG routinely intercepts and monitors communications on this IS for purposes including, but not limited to, penetration testing, COMSEC monitoring, network operations and defense, personnel misconduct (PM), law enforcement (LE), and counterintelligence (CI) investigations. -At any time, the USG may inspect and seize data stored on this IS. -Communications using, or data stored on, this IS are not private, are subject to routine monitoring, interception, and search, and may be disclosed or used for any USG-authorized purpose. -This IS includes security measures (e.g., authentication and access controls) to protect USG interests -- not for your personal benefit or privacy. -Notwithstanding the above, using this IS does not constitute consent to PM, LE or CI investigative searching or monitoring of the content of privileged communications, or work product, related to personal representation or services by attorneys, psychotherapists, or clergy, and their assistants. Such communications and work product are private and confidential. See User Agreement for details.` OR: `I've read & consent to terms in IS user agreem't.`
Rationale: | Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. System use notifications are required only for access via login interfaces with human users and are not required when such human interfaces do not exist.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_banner_etc_motd
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    motd_banner_text='^Authorized[\s\n]+uses[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.$'

    # Multiple regexes transform the banner regex into a usable banner
    # 0 - Remove anchors around the banner text
    motd_banner_text=$(echo "$motd_banner_text" | sed 's/^\^\(.*\)\$$/\1/g')
    # 1 - Keep only the first banners if there are multiple
    #    (dod_banners contains the long and short banner)
    motd_banner_text=$(echo "$motd_banner_text" | sed 's/^(\(.*\.\)|.*)$/\1/g')
    # 2 - Add spaces ' '. (Transforms regex for "space or newline" into a " ")
    motd_banner_text=$(echo "$motd_banner_text" | sed 's/\[\\s\\n\]+/ /g')
    # 3 - Adds newlines. (Transforms "(?:\[\\n\]+|(?:\\n)+)" into "\n")
    motd_banner_text=$(echo "$motd_banner_text" | sed 's/(?:\[\\n\]+|(?:\\\\n)+)/\n/g')
    # 4 - Remove any leftover backslash. (From any parenthesis in the banner, for example).
    motd_banner_text=$(echo "$motd_banner_text" | sed 's/\\//g')
    formatted=$(echo "$motd_banner_text" | fold -sw 80)

    cat <<EOF >/etc/motd
    $formatted
    EOF

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | unknown
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - banner_etc_motd
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy
    - name: XCCDF Value motd_banner_text # promote to variable
      set_fact:
        motd_banner_text: !!str ^Authorized[\s\n]+uses[\s\n]+only\.[\s\n]+All[\s\n]+activity[\s\n]+may[\s\n]+be[\s\n]+monitored[\s\n]+and[\s\n]+reported\.$
      tags:
        - always

    - name: Modify the System Message of the Day Banner - ensure correct banner
      ansible.builtin.copy:
        dest: /etc/motd
        content: '{{ motd_banner_text | regex_replace("^\^(.*)\$$", "\1") | regex_replace("^\((.*\.)\|.*\)$",
          "\1") | regex_replace("\[\\s\\n\]\+"," ") | regex_replace("\(\?:\[\\n\]\+\|\(\?:\\\\n\)\+\)",
          "\n") | regex_replace("\\", "") | wordwrap() }}'
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - banner_etc_motd
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy
Group Protect Accounts by Configuring PAM Group contains 4 groups and 11 rules
[ref] PAM, or Pluggable Authentication Modules, is a system which implements modular authentication for Linux programs. PAM provides a flexible and configurable architecture for authentication, and it should be configured to minimize exposure to unnecessary risk. This section contains guidance on how to accomplish that. PAM is implemented as a set of shared objects which are loaded and invoked whenever an application wishes to authenticate a user. Typically, the application must be running as root in order to take advantage of PAM, because PAM's modules often need to be able to access sensitive stores of account information, such as /etc/shadow. Traditional privileged network listeners (e.g. sshd) or SUID programs (e.g. sudo) already meet this requirement. An SUID root application, userhelper, is provided so that programs which are not SUID or privileged themselves can still take advantage of PAM. PAM looks in the directory `/etc/pam.d` for application-specific configuration information. For instance, if the program login attempts to authenticate a user, then PAM's libraries follow the instructions in the file `/etc/pam.d/login` to determine what actions should be taken. One very important file in `/etc/pam.d` is `/etc/pam.d/system-auth`. This file, which is included by many other PAM configuration files, defines 'default' system authentication measures. Modifying this file is a good way to make far-reaching authentication changes, for instance when implementing a centralized authentication service. Warning: Be careful when making changes to PAM's configuration files. The syntax for these files is complex, and modifications can have unexpected consequences. The default configurations shipped with applications should be sufficient for most users. Warning: Running `authconfig` or `system-config-authentication` will re-write the PAM configuration files, destroying any manually made changes and replacing them with a series of system defaults. One reference to the configuration file syntax can be found at https://fossies.org/linux/Linux-PAM-docs/doc/sag/Linux-PAM_SAG.pdf.
Group Set Lockouts for Failed Password Attempts Group contains 2 rules
[ref] The `pam_faillock` PAM module provides the capability to lock out user accounts after a number of failed login attempts. Its documentation is available in `/usr/share/doc/pam-VERSION/txts/README.pam_faillock`. Warning: Locking out user accounts presents the risk of a denial-of-service attack. The lockout policy must weigh whether the risk of such a denial-of-service attack outweighs the benefits of thwarting password guessing attacks.
#### Rule Limit Password Reuse: password-auth [ref]
Do not allow users to reuse recent passwords. This can be accomplished by using the `remember` option for the `pam_pwhistory` PAM module. On systems with newer versions of `authselect`, the `pam_pwhistory` PAM module can be enabled via authselect feature:
    authselect enable-feature with-pwhistory
Otherwise, it should be enabled using an authselect custom profile. Newer systems also have the `/etc/security/pwhistory.conf` file for setting `pam_pwhistory` module options. This file should be used whenever available. Otherwise, the `pam_pwhistory` module options can be set in PAM files. The value for `remember` option must be equal or greater than 20 Warning: If the system relies on `authselect` tool to manage PAM settings, the remediation will also use `authselect` tool. However, if any manual modification was made in PAM files, the `authselect` integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. Warning: Newer versions of `authselect` contain an authselect feature to easily and properly enable `pam_pwhistory.so` module. If this feature is not yet available in your system, an authselect custom profile must be used to avoid integrity issues in PAM files. If a custom profile was created and used in the system before this authselect feature was available, the new feature can't be used with this custom profile and the remediation will fail. In this case, the custom profile should be recreated or manually updated.
Rationale: | Preventing reuse of previous passwords helps ensure that a compromised password is not reused by a user.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_password_auth
References: |
cis-csc | 1, 12, 15, 16, 5
cjis | 5.6.2.1.1
cobit5 | DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
cui | 3.5.8
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1
iso27001-2013 | A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3
nist | IA-5(f), IA-5(1)(e)
nist-csf | PR.AC-1, PR.AC-6, PR.AC-7
pcidss | Req-8.2.5
os-srg | SRG-OS-000077-GPOS-00045
pcidss4 | 8.3.7, 8.3
cis | 5.3.3.3.1
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q pam; }; then

    var_password_pam_remember='20'
    var_password_pam_remember_control_flag='requisite'

    var_password_pam_remember_control_flag="$(echo $var_password_pam_remember_control_flag | cut -d \, -f 1)"

    if [ -f /usr/bin/authselect ]; then
        if authselect list-features sssd | grep -q with-pwhistory; then
            if ! authselect check; then
            echo "
            authselect integrity check failed. Remediation aborted!
            This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
            It is not recommended to manually edit the PAM files when authselect tool is available.
            In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
            exit 1
            fi
            authselect enable-feature with-pwhistory

            authselect apply-changes -b
        else

            if ! authselect check; then
            echo "
            authselect integrity check failed. Remediation aborted!
            This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
            It is not recommended to manually edit the PAM files when authselect tool is available.
            In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
            exit 1
            fi

            CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
            # If not already in use, a custom profile is created preserving the enabled features.
            if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                # The "local" profile does not contain essential security features required by multiple Benchmarks.
                # If currently used, it is replaced by "sssd", which is the best option in this case.
                if [[ $CURRENT_PROFILE == local ]]; then
                    CURRENT_PROFILE="sssd"
                fi
                authselect create-profile hardening -b $CURRENT_PROFILE
                CURRENT_PROFILE="custom/hardening"

                authselect apply-changes -b --backup=before-hardening-custom-profile
                authselect select $CURRENT_PROFILE
                for feature in $ENABLED_FEATURES; do
                    authselect enable-feature $feature;
                done

                authselect apply-changes -b --backup=after-hardening-custom-profile
            fi
            PAM_FILE_NAME=$(basename "/etc/pam.d/password-auth")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b

            if ! grep -qP "^\s*password\s+\$var_password_pam_remember_control_flag\s+pam_pwhistory.so\s*.*" "$PAM_FILE_PATH"; then
                # Line matching group + control + module was not found. Check group + module.
                if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then
                    # The control is updated only if one single line matches.
                    sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1$var_password_pam_remember_control_flag \2/" "$PAM_FILE_PATH"
                else
                    LAST_MATCH_LINE=$(grep -nP "^password.*requisite.*pam_pwquality\.so" "$PAM_FILE_PATH" | tail -n 1 | cut -d: -f 1)
                    if [ ! -z $LAST_MATCH_LINE ]; then
                        sed -i --follow-symlinks $LAST_MATCH_LINE" a password     $var_password_pam_remember_control_flag    pam_pwhistory.so" "$PAM_FILE_PATH"
                    else
                        echo "password    $var_password_pam_remember_control_flag    pam_pwhistory.so" >> "$PAM_FILE_PATH"
                    fi
                fi
            fi
        fi
    else

        if ! grep -qP "^\s*password\s+\$var_password_pam_remember_control_flag\s+pam_pwhistory.so\s*.*" "/etc/pam.d/password-auth"; then
            # Line matching group + control + module was not found. Check group + module.
            if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "/etc/pam.d/password-auth")" -eq 1 ]; then
                # The control is updated only if one single line matches.
                sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1$var_password_pam_remember_control_flag \2/" "/etc/pam.d/password-auth"
            else
                LAST_MATCH_LINE=$(grep -nP "^password.*requisite.*pam_pwquality\.so" "/etc/pam.d/password-auth" | tail -n 1 | cut -d: -f 1)
                if [ ! -z $LAST_MATCH_LINE ]; then
                    sed -i --follow-symlinks $LAST_MATCH_LINE" a password     $var_password_pam_remember_control_flag    pam_pwhistory.so" "/etc/pam.d/password-auth"
                else
                    echo "password    $var_password_pam_remember_control_flag    pam_pwhistory.so" >> "/etc/pam.d/password-auth"
                fi
            fi
        fi

    fi

    PWHISTORY_CONF="/etc/security/pwhistory.conf"
    if [ -f $PWHISTORY_CONF ]; then
        regex="^\s*remember\s*="
        line="remember = $var_password_pam_remember"
        if ! grep -q $regex $PWHISTORY_CONF; then
            echo $line >> $PWHISTORY_CONF
        else
            sed -i --follow-symlinks 's|^\s*\(remember\s*=\s*\)\(\S\+\)|\1'"$var_password_pam_remember"'|g' $PWHISTORY_CONF
        fi
        if [ -e "/etc/pam.d/password-auth" ] ; then
            PAM_FILE_PATH="/etc/pam.d/password-auth"
            if [ -f /usr/bin/authselect ]; then

                if ! authselect check; then
                echo "
                authselect integrity check failed. Remediation aborted!
                This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
                It is not recommended to manually edit the PAM files when authselect tool is available.
                In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
                exit 1
                fi

                CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
                # If not already in use, a custom profile is created preserving the enabled features.
                if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                    ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                    # The "local" profile does not contain essential security features required by multiple Benchmarks.
                    # If currently used, it is replaced by "sssd", which is the best option in this case.
                    if [[ $CURRENT_PROFILE == local ]]; then
                        CURRENT_PROFILE="sssd"
                    fi
                    authselect create-profile hardening -b $CURRENT_PROFILE
                    CURRENT_PROFILE="custom/hardening"

                    authselect apply-changes -b --backup=before-hardening-custom-profile
                    authselect select $CURRENT_PROFILE
                    for feature in $ENABLED_FEATURES; do
                        authselect enable-feature $feature;
                    done

                    authselect apply-changes -b --backup=after-hardening-custom-profile
                fi
                PAM_FILE_NAME=$(basename "/etc/pam.d/password-auth")
                PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

                authselect apply-changes -b
            fi

        if grep -qP "^\s*password\s.*\bpam_pwhistory.so\s.*\bremember\b" "$PAM_FILE_PATH"; then
            sed -i -E --follow-symlinks "s/(.*password.*pam_pwhistory.so.*)\bremember\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH"
        fi
            if [ -f /usr/bin/authselect ]; then

                authselect apply-changes -b
            fi
        else
            echo "/etc/pam.d/password-auth was not found" >&2
        fi
    else
        PAM_FILE_PATH="/etc/pam.d/password-auth"
        if [ -f /usr/bin/authselect ]; then

            if ! authselect check; then
            echo "
            authselect integrity check failed. Remediation aborted!
            This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
            It is not recommended to manually edit the PAM files when authselect tool is available.
            In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
            exit 1
            fi

            CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
            # If not already in use, a custom profile is created preserving the enabled features.
            if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                # The "local" profile does not contain essential security features required by multiple Benchmarks.
                # If currently used, it is replaced by "sssd", which is the best option in this case.
                if [[ $CURRENT_PROFILE == local ]]; then
                    CURRENT_PROFILE="sssd"
                fi
                authselect create-profile hardening -b $CURRENT_PROFILE
                CURRENT_PROFILE="custom/hardening"

                authselect apply-changes -b --backup=before-hardening-custom-profile
                authselect select $CURRENT_PROFILE
                for feature in $ENABLED_FEATURES; do
                    authselect enable-feature $feature;
                done

                authselect apply-changes -b --backup=after-hardening-custom-profile
            fi
            PAM_FILE_NAME=$(basename "/etc/pam.d/password-auth")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b
        fi

        if ! grep -qP "^\s*password\s+requisite\s+pam_pwhistory.so\s*.*" "$PAM_FILE_PATH"; then
            # Line matching group + control + module was not found. Check group + module.
            if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then
                # The control is updated only if one single line matches.
                sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1requisite \2/" "$PAM_FILE_PATH"
            else
                echo "password    requisite    pam_pwhistory.so" >> "$PAM_FILE_PATH"
            fi
        fi
        # Check the option
        if ! grep -qP "^\s*password\s+requisite\s+pam_pwhistory.so\s*.*\sremember\b" "$PAM_FILE_PATH"; then
            sed -i -E --follow-symlinks "/\s*password\s+requisite\s+pam_pwhistory.so.*/ s/$/ remember=$var_password_pam_remember/" "$PAM_FILE_PATH"
        else
            sed -i -E --follow-symlinks "s/(\s*password\s+requisite\s+pam_pwhistory.so\s+.*)(remember=)[[:alnum:]]*\s*(.*)/\1\2$var_password_pam_remember \3/" "$PAM_FILE_PATH"
        fi
        if [ -f /usr/bin/authselect ]; then

            authselect apply-changes -b
        fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_pwhistory_remember_password_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
    - name: XCCDF Value var_password_pam_remember # promote to variable
      set_fact:
        var_password_pam_remember: !!str 20
      tags:
        - always
    - name: XCCDF Value var_password_pam_remember_control_flag # promote to variable
      set_fact:
        var_password_pam_remember_control_flag: !!str requisite
      tags:
        - always

    - name: 'Limit Password Reuse: password-auth - Check if system relies on authselect
        tool'
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_pwhistory_remember_password_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: 'Limit Password Reuse: password-auth - Collect the available authselect features'
      ansible.builtin.command:
        cmd: authselect list-features sssd
      register: result_authselect_available_features
      changed_when: false
      check_mode: false
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      - result_authselect_present.stat.exists
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_pwhistory_remember_password_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: 'Limit Password Reuse: password-auth - Enable pam_pwhistory.so using authselect
        feature'
      block:

      - name: 'Limit Password Reuse: password-auth - Check integrity of authselect current
          profile'
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        check_mode: false
        failed_when: false

      - name: 'Limit Password Reuse: password-auth - Informative message based on the
          authselect integrity check result'
        ansible.builtin.assert:
          that:
          - ansible_check_mode or result_authselect_check_cmd.rc == 0
          fail_msg:
          - authselect integrity check failed. Remediation aborted!
          - This remediation could not be applied because an authselect profile was not
            selected or the selected profile is not intact.
          - It is not recommended to manually edit the PAM files when authselect tool
            is available.
          - In cases where the default authselect profile does not cover a specific demand,
            a custom authselect profile is recommended.
          success_msg:
          - authselect integrity check passed

      - name: 'Limit Password Reuse: password-auth - Get authselect current features'
        ansible.builtin.shell:
          cmd: authselect current | tail -n+3 | awk '{ print $2 }'
        register: result_authselect_features
        changed_when: false
        check_mode: false
        when:
        - result_authselect_check_cmd is success

      - name: 'Limit Password Reuse: password-auth - Ensure "with-pwhistory" feature is
          enabled using authselect tool'
        ansible.builtin.command:
          cmd: authselect enable-feature with-pwhistory
        register: result_authselect_enable_feature_cmd
        when:
        - result_authselect_check_cmd is success
        - result_authselect_features.stdout is not search("with-pwhistory")

      - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_enable_feature_cmd is not skipped
        - result_authselect_enable_feature_cmd is success
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      - result_authselect_present.stat.exists
      - result_authselect_available_features.stdout is search("with-pwhistory")
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_pwhistory_remember_password_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: 'Limit Password Reuse: password-auth - Enable pam_pwhistory.so in appropriate
        PAM files'
      block:

      - name: 'Limit Password Reuse: password-auth - Define the PAM file to be edited
          as a local fact'
        ansible.builtin.set_fact:
          pam_file_path: /etc/pam.d/password-auth

      - name: 'Limit Password Reuse: password-auth - Check if system relies on authselect
          tool'
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

      - name: 'Limit Password Reuse: password-auth - Ensure authselect custom profile
          is used if authselect is present'
        block:

        - name: 'Limit Password Reuse: password-auth - Check integrity of authselect current
            profile'
          ansible.builtin.command:
            cmd: authselect check
          register: result_authselect_check_cmd
          changed_when: false
          check_mode: false
          failed_when: false

        - name: 'Limit Password Reuse: password-auth - Informative message based on the
            authselect integrity check result'
          ansible.builtin.assert:
            that:
            - ansible_check_mode or result_authselect_check_cmd.rc == 0
            fail_msg:
            - authselect integrity check failed. Remediation aborted!
            - This remediation could not be applied because an authselect profile was
              not selected or the selected profile is not intact.
            - It is not recommended to manually edit the PAM files when authselect tool
              is available.
            - In cases where the default authselect profile does not cover a specific
              demand, a custom authselect profile is recommended.
            success_msg:
            - authselect integrity check passed

        - name: 'Limit Password Reuse: password-auth - Get authselect current profile'
          ansible.builtin.shell:
            cmd: authselect current -r | awk '{ print $1 }'
          register: result_authselect_profile
          changed_when: false
          when:
          - result_authselect_check_cmd is success

        - name: 'Limit Password Reuse: password-auth - Define the current authselect profile
            as a local fact'
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is match("custom/")

        - name: 'Limit Password Reuse: password-auth - Define the new authselect custom
            profile as a local fact'
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: custom/hardening
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is not match("custom/")

        - 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 }'
          register: result_authselect_features
          changed_when: false
          check_mode: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: 'Limit Password Reuse: password-auth - Check if any custom profile with
            the same name was already created'
          ansible.builtin.stat:
            path: /etc/authselect/{{ authselect_custom_profile }}
          register: result_authselect_custom_profile_present
          changed_when: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: 'Limit Password Reuse: password-auth - Create an authselect custom profile
            based on the current profile'
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b {{ authselect_current_profile
              }}
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is not match("^(custom/|local)")
          - not result_authselect_custom_profile_present.stat.exists

        - name: 'Limit Password Reuse: password-auth - Create an authselect custom profile
            based on sssd profile'
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b sssd
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is match("local")
          - not result_authselect_custom_profile_present.stat.exists

        - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: 'Limit Password Reuse: password-auth - Ensure the authselect custom profile
            is selected'
          ansible.builtin.command:
            cmd: authselect select {{ authselect_custom_profile }}
          register: result_pam_authselect_select_profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: 'Limit Password Reuse: password-auth - Restore the authselect features
            in the custom profile'
          ansible.builtin.command:
            cmd: authselect enable-feature {{ item }}
          loop: '{{ result_authselect_features.stdout_lines }}'
          register: result_pam_authselect_restore_features
          when:
          - result_authselect_profile is not skipped
          - result_authselect_features is not skipped
          - result_pam_authselect_select_profile is not skipped

        - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - result_pam_authselect_restore_features is not skipped

        - name: 'Limit Password Reuse: password-auth - Change the PAM file to be edited
            according to the custom authselect profile'
          ansible.builtin.set_fact:
            pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
              | basename }}
          when:
          - authselect_custom_profile is defined
        when:
        - result_authselect_present.stat.exists

      - name: 'Limit Password Reuse: password-auth - Define a fact for control already
          filtered in case filters are used'
        ansible.builtin.set_fact:
          pam_module_control: '{{ var_password_pam_remember_control_flag.split(",")[0]
            }}'

      - name: 'Limit Password Reuse: password-auth - Check if expected PAM module line
          is present in {{ pam_file_path }}'
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s*.*
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_line_present

      - name: 'Limit Password Reuse: password-auth - Include or update the PAM module
          line in {{ pam_file_path }}'
        block:

        - name: 'Limit Password Reuse: password-auth - Check if required PAM module line
            is present in {{ pam_file_path }} with different control'
          ansible.builtin.lineinfile:
            path: '{{ pam_file_path }}'
            regexp: ^\s*password\s+.*\s+pam_pwhistory.so\s*
            state: absent
          check_mode: true
          changed_when: false
          register: result_pam_line_other_control_present

        - name: 'Limit Password Reuse: password-auth - Ensure the correct control for
            the required PAM module line in {{ pam_file_path }}'
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: ^(\s*password\s+).*(\bpam_pwhistory.so.*)
            replace: \1{{ pam_module_control }} \2
          register: result_pam_module_edit
          when:
          - result_pam_line_other_control_present.found == 1

        - name: 'Limit Password Reuse: password-auth - Ensure the required PAM module
            line is included in {{ pam_file_path }}'
          ansible.builtin.lineinfile:
            dest: '{{ pam_file_path }}'
            insertafter: ^password.*requisite.*pam_pwquality\.so
            line: password    {{ pam_module_control }}    pam_pwhistory.so
          register: result_pam_module_add
          when:
          - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found
            > 1

        - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
          ansible.builtin.command:
            cmd: authselect apply-changes -b
          when:
          - result_authselect_present is defined
          - result_authselect_present.stat.exists
          - |-
            (result_pam_module_add is defined and result_pam_module_add.changed)
             or (result_pam_module_edit is defined and result_pam_module_edit.changed)
        when:
        - result_pam_line_present.found is defined
        - result_pam_line_present.found == 0
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      - |
        (result_authselect_available_features.stdout is defined and result_authselect_available_features.stdout is not search("with-pwhistory")) or result_authselect_available_features is not defined
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_pwhistory_remember_password_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: 'Limit Password Reuse: password-auth - Check the presence of /etc/security/pwhistory.conf
        file'
      ansible.builtin.stat:
        path: /etc/security/pwhistory.conf
      register: result_pwhistory_conf_check
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_pwhistory_remember_password_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: 'Limit Password Reuse: password-auth - pam_pwhistory.so parameters are configured
        in /etc/security/pwhistory.conf file'
      block:

      - name: 'Limit Password Reuse: password-auth - Ensure the pam_pwhistory.so remember
          parameter in /etc/security/pwhistory.conf'
        ansible.builtin.lineinfile:
          path: /etc/security/pwhistory.conf
          regexp: ^\s*remember\s*=
          line: remember = {{ var_password_pam_remember }}
          state: present

      - name: 'Limit Password Reuse: password-auth - Ensure the pam_pwhistory.so remember
          parameter is removed from PAM files'
        block:

        - name: 'Limit Password Reuse: password-auth - Check if /etc/pam.d/password-auth
            file is present'
          ansible.builtin.stat:
            path: /etc/pam.d/password-auth
          register: result_pam_password_auth_file_present

        - name: 'Limit Password Reuse: password-auth - Check the proper remediation for
            the system'
          block:

          - name: 'Limit Password Reuse: password-auth - Define the PAM file to be edited
              as a local fact'
            ansible.builtin.set_fact:
              pam_file_path: /etc/pam.d/password-auth

          - name: 'Limit Password Reuse: password-auth - Check if system relies on authselect
              tool'
            ansible.builtin.stat:
              path: /usr/bin/authselect
            register: result_authselect_present

          - name: 'Limit Password Reuse: password-auth - Ensure authselect custom profile
              is used if authselect is present'
            block:

            - name: 'Limit Password Reuse: password-auth - Check integrity of authselect
                current profile'
              ansible.builtin.command:
                cmd: authselect check
              register: result_authselect_check_cmd
              changed_when: false
              check_mode: false
              failed_when: false

            - name: 'Limit Password Reuse: password-auth - Informative message based on
                the authselect integrity check result'
              ansible.builtin.assert:
                that:
                - ansible_check_mode or result_authselect_check_cmd.rc == 0
                fail_msg:
                - authselect integrity check failed. Remediation aborted!
                - This remediation could not be applied because an authselect profile
                  was not selected or the selected profile is not intact.
                - It is not recommended to manually edit the PAM files when authselect
                  tool is available.
                - In cases where the default authselect profile does not cover a specific
                  demand, a custom authselect profile is recommended.
                success_msg:
                - authselect integrity check passed

            - name: 'Limit Password Reuse: password-auth - Get authselect current profile'
              ansible.builtin.shell:
                cmd: authselect current -r | awk '{ print $1 }'
              register: result_authselect_profile
              changed_when: false
              when:
              - result_authselect_check_cmd is success

            - name: 'Limit Password Reuse: password-auth - Define the current authselect
                profile as a local fact'
              ansible.builtin.set_fact:
                authselect_current_profile: '{{ result_authselect_profile.stdout }}'
                authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
              when:
              - result_authselect_profile is not skipped
              - result_authselect_profile.stdout is match("custom/")

            - name: 'Limit Password Reuse: password-auth - Define the new authselect custom
                profile as a local fact'
              ansible.builtin.set_fact:
                authselect_current_profile: '{{ result_authselect_profile.stdout }}'
                authselect_custom_profile: custom/hardening
              when:
              - result_authselect_profile is not skipped
              - result_authselect_profile.stdout is not match("custom/")

            - 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 }'
              register: result_authselect_features
              changed_when: false
              check_mode: false
              when:
              - result_authselect_profile is not skipped
              - authselect_current_profile is not match("custom/")

            - name: 'Limit Password Reuse: password-auth - Check if any custom profile
                with the same name was already created'
              ansible.builtin.stat:
                path: /etc/authselect/{{ authselect_custom_profile }}
              register: result_authselect_custom_profile_present
              changed_when: false
              when:
              - result_authselect_profile is not skipped
              - authselect_current_profile is not match("custom/")

            - name: 'Limit Password Reuse: password-auth - Create an authselect custom
                profile based on the current profile'
              ansible.builtin.command:
                cmd: authselect create-profile hardening -b {{ authselect_current_profile
                  }}
              when:
              - result_authselect_profile is not skipped
              - result_authselect_check_cmd is success
              - authselect_current_profile is not match("^(custom/|local)")
              - not result_authselect_custom_profile_present.stat.exists

            - name: 'Limit Password Reuse: password-auth - Create an authselect custom
                profile based on sssd profile'
              ansible.builtin.command:
                cmd: authselect create-profile hardening -b sssd
              when:
              - result_authselect_profile is not skipped
              - result_authselect_check_cmd is success
              - authselect_current_profile is match("local")
              - not result_authselect_custom_profile_present.stat.exists

            - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are
                applied'
              ansible.builtin.command:
                cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
              when:
              - result_authselect_check_cmd is success
              - result_authselect_profile is not skipped
              - authselect_current_profile is not match("custom/")
              - authselect_custom_profile is not match(authselect_current_profile)

            - name: 'Limit Password Reuse: password-auth - Ensure the authselect custom
                profile is selected'
              ansible.builtin.command:
                cmd: authselect select {{ authselect_custom_profile }}
              register: result_pam_authselect_select_profile
              when:
              - result_authselect_check_cmd is success
              - result_authselect_profile is not skipped
              - authselect_current_profile is not match("custom/")
              - authselect_custom_profile is not match(authselect_current_profile)

            - name: 'Limit Password Reuse: password-auth - Restore the authselect features
                in the custom profile'
              ansible.builtin.command:
                cmd: authselect enable-feature {{ item }}
              loop: '{{ result_authselect_features.stdout_lines }}'
              register: result_pam_authselect_restore_features
              when:
              - result_authselect_profile is not skipped
              - result_authselect_features is not skipped
              - result_pam_authselect_select_profile is not skipped

            - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are
                applied'
              ansible.builtin.command:
                cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
              when:
              - result_authselect_check_cmd is success
              - result_authselect_profile is not skipped
              - result_pam_authselect_restore_features is not skipped

            - name: 'Limit Password Reuse: password-auth - Change the PAM file to be edited
                according to the custom authselect profile'
              ansible.builtin.set_fact:
                pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
                  | basename }}
              when:
              - authselect_custom_profile is defined
            when:
            - result_authselect_present.stat.exists

          - name: 'Limit Password Reuse: password-auth - Define a fact for control already
              filtered in case filters are used'
            ansible.builtin.set_fact:
              pam_module_control: ''

          - name: 'Limit Password Reuse: password-auth - Check if {{ pam_file_path }}
              file is present'
            ansible.builtin.stat:
              path: '{{ pam_file_path }}'
            register: result_pam_file_present

          - name: 'Limit Password Reuse: password-auth - Ensure the "remember" option
              from "pam_pwhistory.so" is not present in {{ pam_file_path }}'
            ansible.builtin.replace:
              dest: '{{ pam_file_path }}'
              regexp: (.*password.*pam_pwhistory.so.*)\bremember\b=?[0-9a-zA-Z]*(.*)
              replace: \1\2
            register: result_pam_option_removal
            when:
            - result_pam_file_present.stat.exists

          - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are
              applied'
            ansible.builtin.command:
              cmd: authselect apply-changes -b
            when:
            - result_authselect_present.stat.exists
            - result_pam_option_removal is changed
          when:
          - result_pam_password_auth_file_present.stat.exists
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      - result_pwhistory_conf_check.stat.exists
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_pwhistory_remember_password_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: 'Limit Password Reuse: password-auth - pam_pwhistory.so parameters are configured
        in PAM files'
      block:

      - name: 'Limit Password Reuse: password-auth - Define the PAM file to be edited
          as a local fact'
        ansible.builtin.set_fact:
          pam_file_path: /etc/pam.d/password-auth

      - name: 'Limit Password Reuse: password-auth - Check if system relies on authselect
          tool'
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

      - name: 'Limit Password Reuse: password-auth - Ensure authselect custom profile
          is used if authselect is present'
        block:

        - name: 'Limit Password Reuse: password-auth - Check integrity of authselect current
            profile'
          ansible.builtin.command:
            cmd: authselect check
          register: result_authselect_check_cmd
          changed_when: false
          check_mode: false
          failed_when: false

        - name: 'Limit Password Reuse: password-auth - Informative message based on the
            authselect integrity check result'
          ansible.builtin.assert:
            that:
            - ansible_check_mode or result_authselect_check_cmd.rc == 0
            fail_msg:
            - authselect integrity check failed. Remediation aborted!
            - This remediation could not be applied because an authselect profile was
              not selected or the selected profile is not intact.
            - It is not recommended to manually edit the PAM files when authselect tool
              is available.
            - In cases where the default authselect profile does not cover a specific
              demand, a custom authselect profile is recommended.
            success_msg:
            - authselect integrity check passed

        - name: 'Limit Password Reuse: password-auth - Get authselect current profile'
          ansible.builtin.shell:
            cmd: authselect current -r | awk '{ print $1 }'
          register: result_authselect_profile
          changed_when: false
          when:
          - result_authselect_check_cmd is success

        - name: 'Limit Password Reuse: password-auth - Define the current authselect profile
            as a local fact'
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is match("custom/")

        - name: 'Limit Password Reuse: password-auth - Define the new authselect custom
            profile as a local fact'
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: custom/hardening
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is not match("custom/")

        - 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 }'
          register: result_authselect_features
          changed_when: false
          check_mode: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: 'Limit Password Reuse: password-auth - Check if any custom profile with
            the same name was already created'
          ansible.builtin.stat:
            path: /etc/authselect/{{ authselect_custom_profile }}
          register: result_authselect_custom_profile_present
          changed_when: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: 'Limit Password Reuse: password-auth - Create an authselect custom profile
            based on the current profile'
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b {{ authselect_current_profile
              }}
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is not match("^(custom/|local)")
          - not result_authselect_custom_profile_present.stat.exists

        - name: 'Limit Password Reuse: password-auth - Create an authselect custom profile
            based on sssd profile'
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b sssd
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is match("local")
          - not result_authselect_custom_profile_present.stat.exists

        - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: 'Limit Password Reuse: password-auth - Ensure the authselect custom profile
            is selected'
          ansible.builtin.command:
            cmd: authselect select {{ authselect_custom_profile }}
          register: result_pam_authselect_select_profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: 'Limit Password Reuse: password-auth - Restore the authselect features
            in the custom profile'
          ansible.builtin.command:
            cmd: authselect enable-feature {{ item }}
          loop: '{{ result_authselect_features.stdout_lines }}'
          register: result_pam_authselect_restore_features
          when:
          - result_authselect_profile is not skipped
          - result_authselect_features is not skipped
          - result_pam_authselect_select_profile is not skipped

        - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - result_pam_authselect_restore_features is not skipped

        - name: 'Limit Password Reuse: password-auth - Change the PAM file to be edited
            according to the custom authselect profile'
          ansible.builtin.set_fact:
            pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
              | basename }}
          when:
          - authselect_custom_profile is defined
        when:
        - result_authselect_present.stat.exists

      - name: 'Limit Password Reuse: password-auth - Define a fact for control already
          filtered in case filters are used'
        ansible.builtin.set_fact:
          pam_module_control: requisite

      - name: 'Limit Password Reuse: password-auth - Check if expected PAM module line
          is present in {{ pam_file_path }}'
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s*.*
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_line_present

      - name: 'Limit Password Reuse: password-auth - Include or update the PAM module
          line in {{ pam_file_path }}'
        block:

        - name: 'Limit Password Reuse: password-auth - Check if required PAM module line
            is present in {{ pam_file_path }} with different control'
          ansible.builtin.lineinfile:
            path: '{{ pam_file_path }}'
            regexp: ^\s*password\s+.*\s+pam_pwhistory.so\s*
            state: absent
          check_mode: true
          changed_when: false
          register: result_pam_line_other_control_present

        - name: 'Limit Password Reuse: password-auth - Ensure the correct control for
            the required PAM module line in {{ pam_file_path }}'
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: ^(\s*password\s+).*(\bpam_pwhistory.so.*)
            replace: \1{{ pam_module_control }} \2
          register: result_pam_module_edit
          when:
          - result_pam_line_other_control_present.found == 1

        - name: 'Limit Password Reuse: password-auth - Ensure the required PAM module
            line is included in {{ pam_file_path }}'
          ansible.builtin.lineinfile:
            dest: '{{ pam_file_path }}'
            line: password    {{ pam_module_control }}    pam_pwhistory.so
          register: result_pam_module_add
          when:
          - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found
            > 1

        - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
          ansible.builtin.command:
            cmd: authselect apply-changes -b
          when:
          - result_authselect_present is defined
          - result_authselect_present.stat.exists
          - |-
            (result_pam_module_add is defined and result_pam_module_add.changed)
             or (result_pam_module_edit is defined and result_pam_module_edit.changed)
        when:
        - result_pam_line_present.found is defined
        - result_pam_line_present.found == 0

      - name: 'Limit Password Reuse: password-auth - Define a fact for control already
          filtered in case filters are used'
        ansible.builtin.set_fact:
          pam_module_control: requisite

      - name: 'Limit Password Reuse: password-auth - Check if the required PAM module
          option is present in {{ pam_file_path }}'
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s*.*\sremember\b
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_module_accounts_password_pam_pwhistory_remember_password_auth_option_present

      - name: 'Limit Password Reuse: password-auth - Ensure the "remember" PAM option
          for "pam_pwhistory.so" is included in {{ pam_file_path }}'
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          backrefs: true
          regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so.*)
          line: \1 remember={{ var_password_pam_remember }}
          state: present
        register: result_pam_accounts_password_pam_pwhistory_remember_password_auth_add
        when:
        - result_pam_module_accounts_password_pam_pwhistory_remember_password_auth_option_present.found
          is defined
        - result_pam_module_accounts_password_pam_pwhistory_remember_password_auth_option_present.found
          == 0

      - name: 'Limit Password Reuse: password-auth - Ensure the required value for "remember"
          PAM option from "pam_pwhistory.so" in {{ pam_file_path }}'
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          backrefs: true
          regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s+.*)(remember)=[0-9a-zA-Z]*\s*(.*)
          line: \1\2={{ var_password_pam_remember }} \3
        register: result_pam_accounts_password_pam_pwhistory_remember_password_auth_edit
        when:
        - result_pam_module_accounts_password_pam_pwhistory_remember_password_auth_option_present.found
          > 0

      - name: 'Limit Password Reuse: password-auth - Ensure authselect changes are applied'
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_present.stat.exists
        - (result_pam_remember_add is defined and result_pam_remember_add.changed) or
          (result_pam_remember_edit is defined and result_pam_remember_edit.changed)
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      - not result_pwhistory_conf_check.stat.exists
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_pwhistory_remember_password_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
#### Rule Limit Password Reuse: system-auth [ref]
Do not allow users to reuse recent passwords. This can be accomplished by using the `remember` option for the `pam_pwhistory` PAM module. On systems with newer versions of `authselect`, the `pam_pwhistory` PAM module can be enabled via authselect feature:
    authselect enable-feature with-pwhistory
Otherwise, it should be enabled using an authselect custom profile. Newer systems also have the `/etc/security/pwhistory.conf` file for setting `pam_pwhistory` module options. This file should be used whenever available. Otherwise, the `pam_pwhistory` module options can be set in PAM files. The value for `remember` option must be equal or greater than 20 Warning: If the system relies on `authselect` tool to manage PAM settings, the remediation will also use `authselect` tool. However, if any manual modification was made in PAM files, the `authselect` integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. Warning: Newer versions of `authselect` contain an authselect feature to easily and properly enable `pam_pwhistory.so` module. If this feature is not yet available in your system, an authselect custom profile must be used to avoid integrity issues in PAM files.
Rationale: | Preventing reuse of previous passwords helps ensure that a compromised password is not reused by a user.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_system_auth
References: |
cis-csc | 1, 12, 15, 16, 5
cjis | 5.6.2.1.1
cobit5 | DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
cui | 3.5.8
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1
iso27001-2013 | A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3
nist | IA-5(f), IA-5(1)(e)
nist-csf | PR.AC-1, PR.AC-6, PR.AC-7
pcidss | Req-8.2.5
os-srg | SRG-OS-000077-GPOS-00045
pcidss4 | 8.3.7, 8.3
cis | 5.3.3.3.1
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q pam; }; then

    var_password_pam_remember='20'
    var_password_pam_remember_control_flag='requisite'

    var_password_pam_remember_control_flag="$(echo $var_password_pam_remember_control_flag | cut -d \, -f 1)"

    if [ -f /usr/bin/authselect ]; then
        if authselect list-features sssd | grep -q with-pwhistory; then
            if ! authselect check; then
            echo "
            authselect integrity check failed. Remediation aborted!
            This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
            It is not recommended to manually edit the PAM files when authselect tool is available.
            In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
            exit 1
            fi
            authselect enable-feature with-pwhistory

            authselect apply-changes -b
        else

            if ! authselect check; then
            echo "
            authselect integrity check failed. Remediation aborted!
            This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
            It is not recommended to manually edit the PAM files when authselect tool is available.
            In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
            exit 1
            fi

            CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
            # If not already in use, a custom profile is created preserving the enabled features.
            if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                # The "local" profile does not contain essential security features required by multiple Benchmarks.
                # If currently used, it is replaced by "sssd", which is the best option in this case.
                if [[ $CURRENT_PROFILE == local ]]; then
                    CURRENT_PROFILE="sssd"
                fi
                authselect create-profile hardening -b $CURRENT_PROFILE
                CURRENT_PROFILE="custom/hardening"

                authselect apply-changes -b --backup=before-hardening-custom-profile
                authselect select $CURRENT_PROFILE
                for feature in $ENABLED_FEATURES; do
                    authselect enable-feature $feature;
                done

                authselect apply-changes -b --backup=after-hardening-custom-profile
            fi
            PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b

            if ! grep -qP "^\s*password\s+\$var_password_pam_remember_control_flag\s+pam_pwhistory.so\s*.*" "$PAM_FILE_PATH"; then
                # Line matching group + control + module was not found. Check group + module.
                if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then
                    # The control is updated only if one single line matches.
                    sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1$var_password_pam_remember_control_flag \2/" "$PAM_FILE_PATH"
                else
                    LAST_MATCH_LINE=$(grep -nP "^password.*requisite.*pam_pwquality\.so" "$PAM_FILE_PATH" | tail -n 1 | cut -d: -f 1)
                    if [ ! -z $LAST_MATCH_LINE ]; then
                        sed -i --follow-symlinks $LAST_MATCH_LINE" a password     $var_password_pam_remember_control_flag    pam_pwhistory.so" "$PAM_FILE_PATH"
                    else
                        echo "password    $var_password_pam_remember_control_flag    pam_pwhistory.so" >> "$PAM_FILE_PATH"
                    fi
                fi
            fi
        fi
    else

        if ! grep -qP "^\s*password\s+\$var_password_pam_remember_control_flag\s+pam_pwhistory.so\s*.*" "/etc/pam.d/system-auth"; then
            # Line matching group + control + module was not found. Check group + module.
            if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "/etc/pam.d/system-auth")" -eq 1 ]; then
                # The control is updated only if one single line matches.
                sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1$var_password_pam_remember_control_flag \2/" "/etc/pam.d/system-auth"
            else
                LAST_MATCH_LINE=$(grep -nP "^password.*requisite.*pam_pwquality\.so" "/etc/pam.d/system-auth" | tail -n 1 | cut -d: -f 1)
                if [ ! -z $LAST_MATCH_LINE ]; then
                    sed -i --follow-symlinks $LAST_MATCH_LINE" a password     $var_password_pam_remember_control_flag    pam_pwhistory.so" "/etc/pam.d/system-auth"
                else
                    echo "password    $var_password_pam_remember_control_flag    pam_pwhistory.so" >> "/etc/pam.d/system-auth"
                fi
            fi
        fi

    fi

    PWHISTORY_CONF="/etc/security/pwhistory.conf"
    if [ -f $PWHISTORY_CONF ]; then
        regex="^\s*remember\s*="
        line="remember = $var_password_pam_remember"
        if ! grep -q $regex $PWHISTORY_CONF; then
            echo $line >> $PWHISTORY_CONF
        else
            sed -i --follow-symlinks 's|^\s*\(remember\s*=\s*\)\(\S\+\)|\1'"$var_password_pam_remember"'|g' $PWHISTORY_CONF
        fi
        if [ -e "/etc/pam.d/system-auth" ] ; then
            PAM_FILE_PATH="/etc/pam.d/system-auth"
            if [ -f /usr/bin/authselect ]; then

                if ! authselect check; then
                echo "
                authselect integrity check failed. Remediation aborted!
                This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
                It is not recommended to manually edit the PAM files when authselect tool is available.
                In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
                exit 1
                fi

                CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
                # If not already in use, a custom profile is created preserving the enabled features.
                if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                    ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                    # The "local" profile does not contain essential security features required by multiple Benchmarks.
                    # If currently used, it is replaced by "sssd", which is the best option in this case.
                    if [[ $CURRENT_PROFILE == local ]]; then
                        CURRENT_PROFILE="sssd"
                    fi
                    authselect create-profile hardening -b $CURRENT_PROFILE
                    CURRENT_PROFILE="custom/hardening"

                    authselect apply-changes -b --backup=before-hardening-custom-profile
                    authselect select $CURRENT_PROFILE
                    for feature in $ENABLED_FEATURES; do
                        authselect enable-feature $feature;
                    done

                    authselect apply-changes -b --backup=after-hardening-custom-profile
                fi
                PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
                PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

                authselect apply-changes -b
            fi

        if grep -qP "^\s*password\s.*\bpam_pwhistory.so\s.*\bremember\b" "$PAM_FILE_PATH"; then
            sed -i -E --follow-symlinks "s/(.*password.*pam_pwhistory.so.*)\bremember\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH"
        fi
            if [ -f /usr/bin/authselect ]; then

                authselect apply-changes -b
            fi
        else
            echo "/etc/pam.d/system-auth was not found" >&2
        fi
    else
        PAM_FILE_PATH="/etc/pam.d/system-auth"
        if [ -f /usr/bin/authselect ]; then

            if ! authselect check; then
            echo "
            authselect integrity check failed. Remediation aborted!
            This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
            It is not recommended to manually edit the PAM files when authselect tool is available.
            In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
            exit 1
            fi

            CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
            # If not already in use, a custom profile is created preserving the enabled features.
            if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                # The "local" profile does not contain essential security features required by multiple Benchmarks.
                # If currently used, it is replaced by "sssd", which is the best option in this case.
                if [[ $CURRENT_PROFILE == local ]]; then
                    CURRENT_PROFILE="sssd"
                fi
                authselect create-profile hardening -b $CURRENT_PROFILE
                CURRENT_PROFILE="custom/hardening"

                authselect apply-changes -b --backup=before-hardening-custom-profile
                authselect select $CURRENT_PROFILE
                for feature in $ENABLED_FEATURES; do
                    authselect enable-feature $feature;
                done

                authselect apply-changes -b --backup=after-hardening-custom-profile
            fi
            PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b
        fi

        if ! grep -qP "^\s*password\s+requisite\s+pam_pwhistory.so\s*.*" "$PAM_FILE_PATH"; then
            # Line matching group + control + module was not found. Check group + module.
            if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then
                # The control is updated only if one single line matches.
                sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1requisite \2/" "$PAM_FILE_PATH"
            else
                echo "password    requisite    pam_pwhistory.so" >> "$PAM_FILE_PATH"
            fi
        fi
        # Check the option
        if ! grep -qP "^\s*password\s+requisite\s+pam_pwhistory.so\s*.*\sremember\b" "$PAM_FILE_PATH"; then
            sed -i -E --follow-symlinks "/\s*password\s+requisite\s+pam_pwhistory.so.*/ s/$/ remember=$var_password_pam_remember/" "$PAM_FILE_PATH"
        else
            sed -i -E --follow-symlinks "s/(\s*password\s+requisite\s+pam_pwhistory.so\s+.*)(remember=)[[:alnum:]]*\s*(.*)/\1\2$var_password_pam_remember \3/" "$PAM_FILE_PATH"
        fi
        if [ -f /usr/bin/authselect ]; then

            authselect apply-changes -b
        fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_pwhistory_remember_system_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
    - name: XCCDF Value var_password_pam_remember # promote to variable
      set_fact:
        var_password_pam_remember: !!str 20
      tags:
        - always
    - name: XCCDF Value var_password_pam_remember_control_flag # promote to variable
      set_fact:
        var_password_pam_remember_control_flag: !!str requisite
      tags:
        - always

    - name: 'Limit Password Reuse: system-auth - Check if system relies on authselect
        tool'
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_pwhistory_remember_system_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: 'Limit Password Reuse: system-auth - Collect the available authselect features'
      ansible.builtin.command:
        cmd: authselect list-features sssd
      register: result_authselect_available_features
      changed_when: false
      check_mode: false
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      - result_authselect_present.stat.exists
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_pwhistory_remember_system_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: 'Limit Password Reuse: system-auth - Enable pam_pwhistory.so using authselect
        feature'
      block:

      - name: 'Limit Password Reuse: system-auth - Check integrity of authselect current
          profile'
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        check_mode: false
        failed_when: false

      - name: 'Limit Password Reuse: system-auth - Informative message based on the authselect
          integrity check result'
        ansible.builtin.assert:
          that:
          - ansible_check_mode or result_authselect_check_cmd.rc == 0
          fail_msg:
          - authselect integrity check failed. Remediation aborted!
          - This remediation could not be applied because an authselect profile was not
            selected or the selected profile is not intact.
          - It is not recommended to manually edit the PAM files when authselect tool
            is available.
          - In cases where the default authselect profile does not cover a specific demand,
            a custom authselect profile is recommended.
          success_msg:
          - authselect integrity check passed

      - name: 'Limit Password Reuse: system-auth - Get authselect current features'
        ansible.builtin.shell:
          cmd: authselect current | tail -n+3 | awk '{ print $2 }'
        register: result_authselect_features
        changed_when: false
        check_mode: false
        when:
        - result_authselect_check_cmd is success

      - name: 'Limit Password Reuse: system-auth - Ensure "with-pwhistory" feature is
          enabled using authselect tool'
        ansible.builtin.command:
          cmd: authselect enable-feature with-pwhistory
        register: result_authselect_enable_feature_cmd
        when:
        - result_authselect_check_cmd is success
        - result_authselect_features.stdout is not search("with-pwhistory")

      - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_enable_feature_cmd is not skipped
        - result_authselect_enable_feature_cmd is success
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      - result_authselect_present.stat.exists
      - result_authselect_available_features.stdout is search("with-pwhistory")
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_pwhistory_remember_system_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: 'Limit Password Reuse: system-auth - Enable pam_pwhistory.so in appropriate
        PAM files'
      block:

      - name: 'Limit Password Reuse: system-auth - Define the PAM file to be edited as
          a local fact'
        ansible.builtin.set_fact:
          pam_file_path: /etc/pam.d/system-auth

      - name: 'Limit Password Reuse: system-auth - Check if system relies on authselect
          tool'
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

      - name: 'Limit Password Reuse: system-auth - Ensure authselect custom profile is
          used if authselect is present'
        block:

        - name: 'Limit Password Reuse: system-auth - Check integrity of authselect current
            profile'
          ansible.builtin.command:
            cmd: authselect check
          register: result_authselect_check_cmd
          changed_when: false
          check_mode: false
          failed_when: false

        - name: 'Limit Password Reuse: system-auth - Informative message based on the
            authselect integrity check result'
          ansible.builtin.assert:
            that:
            - ansible_check_mode or result_authselect_check_cmd.rc == 0
            fail_msg:
            - authselect integrity check failed. Remediation aborted!
            - This remediation could not be applied because an authselect profile was
              not selected or the selected profile is not intact.
            - It is not recommended to manually edit the PAM files when authselect tool
              is available.
            - In cases where the default authselect profile does not cover a specific
              demand, a custom authselect profile is recommended.
            success_msg:
            - authselect integrity check passed

        - name: 'Limit Password Reuse: system-auth - Get authselect current profile'
          ansible.builtin.shell:
            cmd: authselect current -r | awk '{ print $1 }'
          register: result_authselect_profile
          changed_when: false
          when:
          - result_authselect_check_cmd is success

        - name: 'Limit Password Reuse: system-auth - Define the current authselect profile
            as a local fact'
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is match("custom/")

        - name: 'Limit Password Reuse: system-auth - Define the new authselect custom
            profile as a local fact'
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: custom/hardening
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is not match("custom/")

        - 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 }'
          register: result_authselect_features
          changed_when: false
          check_mode: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: 'Limit Password Reuse: system-auth - Check if any custom profile with
            the same name was already created'
          ansible.builtin.stat:
            path: /etc/authselect/{{ authselect_custom_profile }}
          register: result_authselect_custom_profile_present
          changed_when: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: 'Limit Password Reuse: system-auth - Create an authselect custom profile
            based on the current profile'
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b {{ authselect_current_profile
              }}
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is not match("^(custom/|local)")
          - not result_authselect_custom_profile_present.stat.exists

        - name: 'Limit Password Reuse: system-auth - Create an authselect custom profile
            based on sssd profile'
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b sssd
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is match("local")
          - not result_authselect_custom_profile_present.stat.exists

        - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: 'Limit Password Reuse: system-auth - Ensure the authselect custom profile
            is selected'
          ansible.builtin.command:
            cmd: authselect select {{ authselect_custom_profile }}
          register: result_pam_authselect_select_profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: 'Limit Password Reuse: system-auth - Restore the authselect features in
            the custom profile'
          ansible.builtin.command:
            cmd: authselect enable-feature {{ item }}
          loop: '{{ result_authselect_features.stdout_lines }}'
          register: result_pam_authselect_restore_features
          when:
          - result_authselect_profile is not skipped
          - result_authselect_features is not skipped
          - result_pam_authselect_select_profile is not skipped

        - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - result_pam_authselect_restore_features is not skipped

        - name: 'Limit Password Reuse: system-auth - Change the PAM file to be edited
            according to the custom authselect profile'
          ansible.builtin.set_fact:
            pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
              | basename }}
          when:
          - authselect_custom_profile is defined
        when:
        - result_authselect_present.stat.exists

      - name: 'Limit Password Reuse: system-auth - Define a fact for control already filtered
          in case filters are used'
        ansible.builtin.set_fact:
          pam_module_control: '{{ var_password_pam_remember_control_flag.split(",")[0]
            }}'

      - name: 'Limit Password Reuse: system-auth - Check if expected PAM module line is
          present in {{ pam_file_path }}'
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s*.*
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_line_present

      - name: 'Limit Password Reuse: system-auth - Include or update the PAM module line
          in {{ pam_file_path }}'
        block:

        - name: 'Limit Password Reuse: system-auth - Check if required PAM module line
            is present in {{ pam_file_path }} with different control'
          ansible.builtin.lineinfile:
            path: '{{ pam_file_path }}'
            regexp: ^\s*password\s+.*\s+pam_pwhistory.so\s*
            state: absent
          check_mode: true
          changed_when: false
          register: result_pam_line_other_control_present

        - name: 'Limit Password Reuse: system-auth - Ensure the correct control for the
            required PAM module line in {{ pam_file_path }}'
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: ^(\s*password\s+).*(\bpam_pwhistory.so.*)
            replace: \1{{ pam_module_control }} \2
          register: result_pam_module_edit
          when:
          - result_pam_line_other_control_present.found == 1

        - name: 'Limit Password Reuse: system-auth - Ensure the required PAM module line
            is included in {{ pam_file_path }}'
          ansible.builtin.lineinfile:
            dest: '{{ pam_file_path }}'
            insertafter: ^password.*requisite.*pam_pwquality\.so
            line: password    {{ pam_module_control }}    pam_pwhistory.so
          register: result_pam_module_add
          when:
          - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found
            > 1

        - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
          ansible.builtin.command:
            cmd: authselect apply-changes -b
          when:
          - result_authselect_present is defined
          - result_authselect_present.stat.exists
          - |-
            (result_pam_module_add is defined and result_pam_module_add.changed)
             or (result_pam_module_edit is defined and result_pam_module_edit.changed)
        when:
        - result_pam_line_present.found is defined
        - result_pam_line_present.found == 0
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      - |
        (result_authselect_available_features.stdout is defined and result_authselect_available_features.stdout is not search("with-pwhistory")) or result_authselect_available_features is not defined
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_pwhistory_remember_system_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: 'Limit Password Reuse: system-auth - Check the presence of /etc/security/pwhistory.conf
        file'
      ansible.builtin.stat:
        path: /etc/security/pwhistory.conf
      register: result_pwhistory_conf_check
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_pwhistory_remember_system_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: 'Limit Password Reuse: system-auth - pam_pwhistory.so parameters are configured
        in /etc/security/pwhistory.conf file'
      block:

      - name: 'Limit Password Reuse: system-auth - Ensure the pam_pwhistory.so remember
          parameter in /etc/security/pwhistory.conf'
        ansible.builtin.lineinfile:
          path: /etc/security/pwhistory.conf
          regexp: ^\s*remember\s*=
          line: remember = {{ var_password_pam_remember }}
          state: present

      - name: 'Limit Password Reuse: system-auth - Ensure the pam_pwhistory.so remember
          parameter is removed from PAM files'
        block:

        - name: 'Limit Password Reuse: system-auth - Check if /etc/pam.d/system-auth file
            is present'
          ansible.builtin.stat:
            path: /etc/pam.d/system-auth
          register: result_pam_auth_file_present

        - name: 'Limit Password Reuse: system-auth - Check the proper remediation for
            the system'
          block:

          - name: 'Limit Password Reuse: system-auth - Define the PAM file to be edited
              as a local fact'
            ansible.builtin.set_fact:
              pam_file_path: /etc/pam.d/system-auth

          - name: 'Limit Password Reuse: system-auth - Check if system relies on authselect
              tool'
            ansible.builtin.stat:
              path: /usr/bin/authselect
            register: result_authselect_present

          - name: 'Limit Password Reuse: system-auth - Ensure authselect custom profile
              is used if authselect is present'
            block:

            - name: 'Limit Password Reuse: system-auth - Check integrity of authselect
                current profile'
              ansible.builtin.command:
                cmd: authselect check
              register: result_authselect_check_cmd
              changed_when: false
              check_mode: false
              failed_when: false

            - name: 'Limit Password Reuse: system-auth - Informative message based on
                the authselect integrity check result'
              ansible.builtin.assert:
                that:
                - ansible_check_mode or result_authselect_check_cmd.rc == 0
                fail_msg:
                - authselect integrity check failed. Remediation aborted!
                - This remediation could not be applied because an authselect profile
                  was not selected or the selected profile is not intact.
                - It is not recommended to manually edit the PAM files when authselect
                  tool is available.
                - In cases where the default authselect profile does not cover a specific
                  demand, a custom authselect profile is recommended.
                success_msg:
                - authselect integrity check passed

            - name: 'Limit Password Reuse: system-auth - Get authselect current profile'
              ansible.builtin.shell:
                cmd: authselect current -r | awk '{ print $1 }'
              register: result_authselect_profile
              changed_when: false
              when:
              - result_authselect_check_cmd is success

            - name: 'Limit Password Reuse: system-auth - Define the current authselect
                profile as a local fact'
              ansible.builtin.set_fact:
                authselect_current_profile: '{{ result_authselect_profile.stdout }}'
                authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
              when:
              - result_authselect_profile is not skipped
              - result_authselect_profile.stdout is match("custom/")

            - name: 'Limit Password Reuse: system-auth - Define the new authselect custom
                profile as a local fact'
              ansible.builtin.set_fact:
                authselect_current_profile: '{{ result_authselect_profile.stdout }}'
                authselect_custom_profile: custom/hardening
              when:
              - result_authselect_profile is not skipped
              - result_authselect_profile.stdout is not match("custom/")

            - 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 }'
              register: result_authselect_features
              changed_when: false
              check_mode: false
              when:
              - result_authselect_profile is not skipped
              - authselect_current_profile is not match("custom/")

            - name: 'Limit Password Reuse: system-auth - Check if any custom profile with
                the same name was already created'
              ansible.builtin.stat:
                path: /etc/authselect/{{ authselect_custom_profile }}
              register: result_authselect_custom_profile_present
              changed_when: false
              when:
              - result_authselect_profile is not skipped
              - authselect_current_profile is not match("custom/")

            - name: 'Limit Password Reuse: system-auth - Create an authselect custom profile
                based on the current profile'
              ansible.builtin.command:
                cmd: authselect create-profile hardening -b {{ authselect_current_profile
                  }}
              when:
              - result_authselect_profile is not skipped
              - result_authselect_check_cmd is success
              - authselect_current_profile is not match("^(custom/|local)")
              - not result_authselect_custom_profile_present.stat.exists

            - name: 'Limit Password Reuse: system-auth - Create an authselect custom profile
                based on sssd profile'
              ansible.builtin.command:
                cmd: authselect create-profile hardening -b sssd
              when:
              - result_authselect_profile is not skipped
              - result_authselect_check_cmd is success
              - authselect_current_profile is match("local")
              - not result_authselect_custom_profile_present.stat.exists

            - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are
                applied'
              ansible.builtin.command:
                cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
              when:
              - result_authselect_check_cmd is success
              - result_authselect_profile is not skipped
              - authselect_current_profile is not match("custom/")
              - authselect_custom_profile is not match(authselect_current_profile)

            - name: 'Limit Password Reuse: system-auth - Ensure the authselect custom
                profile is selected'
              ansible.builtin.command:
                cmd: authselect select {{ authselect_custom_profile }}
              register: result_pam_authselect_select_profile
              when:
              - result_authselect_check_cmd is success
              - result_authselect_profile is not skipped
              - authselect_current_profile is not match("custom/")
              - authselect_custom_profile is not match(authselect_current_profile)

            - name: 'Limit Password Reuse: system-auth - Restore the authselect features
                in the custom profile'
              ansible.builtin.command:
                cmd: authselect enable-feature {{ item }}
              loop: '{{ result_authselect_features.stdout_lines }}'
              register: result_pam_authselect_restore_features
              when:
              - result_authselect_profile is not skipped
              - result_authselect_features is not skipped
              - result_pam_authselect_select_profile is not skipped

            - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are
                applied'
              ansible.builtin.command:
                cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
              when:
              - result_authselect_check_cmd is success
              - result_authselect_profile is not skipped
              - result_pam_authselect_restore_features is not skipped

            - name: 'Limit Password Reuse: system-auth - Change the PAM file to be edited
                according to the custom authselect profile'
              ansible.builtin.set_fact:
                pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
                  | basename }}
              when:
              - authselect_custom_profile is defined
            when:
            - result_authselect_present.stat.exists

          - name: 'Limit Password Reuse: system-auth - Define a fact for control already
              filtered in case filters are used'
            ansible.builtin.set_fact:
              pam_module_control: ''

          - name: 'Limit Password Reuse: system-auth - Check if {{ pam_file_path }} file
              is present'
            ansible.builtin.stat:
              path: '{{ pam_file_path }}'
            register: result_pam_file_present

          - name: 'Limit Password Reuse: system-auth - Ensure the "remember" option from
              "pam_pwhistory.so" is not present in {{ pam_file_path }}'
            ansible.builtin.replace:
              dest: '{{ pam_file_path }}'
              regexp: (.*password.*pam_pwhistory.so.*)\bremember\b=?[0-9a-zA-Z]*(.*)
              replace: \1\2
            register: result_pam_option_removal
            when:
            - result_pam_file_present.stat.exists

          - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
            ansible.builtin.command:
              cmd: authselect apply-changes -b
            when:
            - result_authselect_present.stat.exists
            - result_pam_option_removal is changed
          when:
          - result_pam_auth_file_present.stat.exists
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      - result_pwhistory_conf_check.stat.exists
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_pwhistory_remember_system_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: 'Limit Password Reuse: system-auth - pam_pwhistory.so parameters are configured
        in PAM files'
      block:

      - name: 'Limit Password Reuse: system-auth - Define the PAM file to be edited as
          a local fact'
        ansible.builtin.set_fact:
          pam_file_path: /etc/pam.d/system-auth

      - name: 'Limit Password Reuse: system-auth - Check if system relies on authselect
          tool'
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

      - name: 'Limit Password Reuse: system-auth - Ensure authselect custom profile is
          used if authselect is present'
        block:

        - name: 'Limit Password Reuse: system-auth - Check integrity of authselect current
            profile'
          ansible.builtin.command:
            cmd: authselect check
          register: result_authselect_check_cmd
          changed_when: false
          check_mode: false
          failed_when: false

        - name: 'Limit Password Reuse: system-auth - Informative message based on the
            authselect integrity check result'
          ansible.builtin.assert:
            that:
            - ansible_check_mode or result_authselect_check_cmd.rc == 0
            fail_msg:
            - authselect integrity check failed. Remediation aborted!
            - This remediation could not be applied because an authselect profile was
              not selected or the selected profile is not intact.
            - It is not recommended to manually edit the PAM files when authselect tool
              is available.
            - In cases where the default authselect profile does not cover a specific
              demand, a custom authselect profile is recommended.
            success_msg:
            - authselect integrity check passed

        - name: 'Limit Password Reuse: system-auth - Get authselect current profile'
          ansible.builtin.shell:
            cmd: authselect current -r | awk '{ print $1 }'
          register: result_authselect_profile
          changed_when: false
          when:
          - result_authselect_check_cmd is success

        - name: 'Limit Password Reuse: system-auth - Define the current authselect profile
            as a local fact'
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is match("custom/")

        - name: 'Limit Password Reuse: system-auth - Define the new authselect custom
            profile as a local fact'
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: custom/hardening
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is not match("custom/")

        - 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 }'
          register: result_authselect_features
          changed_when: false
          check_mode: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: 'Limit Password Reuse: system-auth - Check if any custom profile with
            the same name was already created'
          ansible.builtin.stat:
            path: /etc/authselect/{{ authselect_custom_profile }}
          register: result_authselect_custom_profile_present
          changed_when: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: 'Limit Password Reuse: system-auth - Create an authselect custom profile
            based on the current profile'
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b {{ authselect_current_profile
              }}
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is not match("^(custom/|local)")
          - not result_authselect_custom_profile_present.stat.exists

        - name: 'Limit Password Reuse: system-auth - Create an authselect custom profile
            based on sssd profile'
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b sssd
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is match("local")
          - not result_authselect_custom_profile_present.stat.exists

        - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: 'Limit Password Reuse: system-auth - Ensure the authselect custom profile
            is selected'
          ansible.builtin.command:
            cmd: authselect select {{ authselect_custom_profile }}
          register: result_pam_authselect_select_profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: 'Limit Password Reuse: system-auth - Restore the authselect features in
            the custom profile'
          ansible.builtin.command:
            cmd: authselect enable-feature {{ item }}
          loop: '{{ result_authselect_features.stdout_lines }}'
          register: result_pam_authselect_restore_features
          when:
          - result_authselect_profile is not skipped
          - result_authselect_features is not skipped
          - result_pam_authselect_select_profile is not skipped

        - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - result_pam_authselect_restore_features is not skipped

        - name: 'Limit Password Reuse: system-auth - Change the PAM file to be edited
            according to the custom authselect profile'
          ansible.builtin.set_fact:
            pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
              | basename }}
          when:
          - authselect_custom_profile is defined
        when:
        - result_authselect_present.stat.exists

      - name: 'Limit Password Reuse: system-auth - Define a fact for control already filtered
          in case filters are used'
        ansible.builtin.set_fact:
          pam_module_control: requisite

      - name: 'Limit Password Reuse: system-auth - Check if expected PAM module line is
          present in {{ pam_file_path }}'
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s*.*
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_line_present

      - name: 'Limit Password Reuse: system-auth - Include or update the PAM module line
          in {{ pam_file_path }}'
        block:

        - name: 'Limit Password Reuse: system-auth - Check if required PAM module line
            is present in {{ pam_file_path }} with different control'
          ansible.builtin.lineinfile:
            path: '{{ pam_file_path }}'
            regexp: ^\s*password\s+.*\s+pam_pwhistory.so\s*
            state: absent
          check_mode: true
          changed_when: false
          register: result_pam_line_other_control_present

        - name: 'Limit Password Reuse: system-auth - Ensure the correct control for the
            required PAM module line in {{ pam_file_path }}'
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: ^(\s*password\s+).*(\bpam_pwhistory.so.*)
            replace: \1{{ pam_module_control }} \2
          register: result_pam_module_edit
          when:
          - result_pam_line_other_control_present.found == 1

        - name: 'Limit Password Reuse: system-auth - Ensure the required PAM module line
            is included in {{ pam_file_path }}'
          ansible.builtin.lineinfile:
            dest: '{{ pam_file_path }}'
            line: password    {{ pam_module_control }}    pam_pwhistory.so
          register: result_pam_module_add
          when:
          - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found
            > 1

        - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
          ansible.builtin.command:
            cmd: authselect apply-changes -b
          when:
          - result_authselect_present is defined
          - result_authselect_present.stat.exists
          - |-
            (result_pam_module_add is defined and result_pam_module_add.changed)
             or (result_pam_module_edit is defined and result_pam_module_edit.changed)
        when:
        - result_pam_line_present.found is defined
        - result_pam_line_present.found == 0

      - name: 'Limit Password Reuse: system-auth - Define a fact for control already filtered
          in case filters are used'
        ansible.builtin.set_fact:
          pam_module_control: requisite

      - name: 'Limit Password Reuse: system-auth - Check if the required PAM module option
          is present in {{ pam_file_path }}'
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s*.*\sremember\b
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_module_accounts_password_pam_pwhistory_remember_system_auth_option_present

      - name: 'Limit Password Reuse: system-auth - Ensure the "remember" PAM option for
          "pam_pwhistory.so" is included in {{ pam_file_path }}'
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          backrefs: true
          regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so.*)
          line: \1 remember={{ var_password_pam_remember }}
          state: present
        register: result_pam_accounts_password_pam_pwhistory_remember_system_auth_add
        when:
        - result_pam_module_accounts_password_pam_pwhistory_remember_system_auth_option_present.found
          is defined
        - result_pam_module_accounts_password_pam_pwhistory_remember_system_auth_option_present.found
          == 0

      - name: 'Limit Password Reuse: system-auth - Ensure the required value for "remember"
          PAM option from "pam_pwhistory.so" in {{ pam_file_path }}'
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          backrefs: true
          regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_pwhistory.so\s+.*)(remember)=[0-9a-zA-Z]*\s*(.*)
          line: \1\2={{ var_password_pam_remember }} \3
        register: result_pam_accounts_password_pam_pwhistory_remember_system_auth_edit
        when:
        - result_pam_module_accounts_password_pam_pwhistory_remember_system_auth_option_present.found
          > 0

      - name: 'Limit Password Reuse: system-auth - Ensure authselect changes are applied'
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_present.stat.exists
        - (result_pam_remember_add is defined and result_pam_remember_add.changed) or
          (result_pam_remember_edit is defined and result_pam_remember_edit.changed)
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      - not result_pwhistory_conf_check.stat.exists
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_pwhistory_remember_system_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
Group Set Password Quality Requirements Group contains 1 group and 6 rules
[ref] The default `pam_pwquality` PAM module provides strength checking for passwords. It performs a number of checks, such as making sure passwords are not similar to dictionary words, are of at least a certain length, are not the previous password reversed, and are not simply a change of case from the previous password. It can also require passwords to be in certain character classes. The `pam_pwquality` module is the preferred way of configuring password requirements. The man pages `pam_pwquality(8)` provide information on the capabilities and configuration of each.
Group Set Password Quality Requirements with pam_pwquality Group contains 6 rules
[ref] The `pam_pwquality` PAM module can be configured to meet requirements for a variety of policies. For example, to configure `pam_pwquality` to require at least one uppercase character, lowercase character, digit, and other (special) character, make sure that `pam_pwquality` exists in `/etc/pam.d/system-auth`:
    password    requisite     pam_pwquality.so try_first_pass local_users_only retry=3 authtok_type=
If no such line exists, add one as the first line of the password section in `/etc/pam.d/system-auth`. Next, modify the settings in `/etc/security/pwquality.conf` to match the following:
    difok = 4
    minlen = 14
    dcredit = -1
    ucredit = -1
    lcredit = -1
    ocredit = -1
    maxrepeat = 3
The arguments can be modified to ensure compliance with your organization's security policy. Discussion of each parameter follows.
#### Rule Ensure PAM Enforces Password Requirements - Minimum Digit Characters [ref]
The pam_pwquality module's `dcredit` parameter controls requirements for usage of digits in a password. When set to a negative number, any password will be required to contain that many digits. When set to a positive number, pam_pwquality will grant +1 additional length credit for each digit. Modify the `dcredit` setting in `/etc/security/pwquality.conf` to require the use of a digit in passwords.
Rationale: | Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. Requiring digits makes password guessing attacks more difficult by ensuring a larger search space.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_dcredit
References: |
cis-csc | 1, 12, 15, 16, 5
cobit5 | DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1
iso27001-2013 | A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3
nist | IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4)
nist-csf | PR.AC-1, PR.AC-6, PR.AC-7
ospp | FMT_SMF_EXT.1
pcidss | Req-8.2.3
os-srg | SRG-OS-000071-GPOS-00039
anssi | R31
ism | 0421, 0422, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561
pcidss4 | 8.3.6, 8.3
stigref | SV-258103r1045210_rule
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q libpwquality; }; then

    var_password_pam_dcredit='-1'

    if grep -sq dcredit /etc/security/pwquality.conf.d/*.conf ; then
        sed -i "/dcredit/d" /etc/security/pwquality.conf.d/*.conf
    fi

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^dcredit")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_dcredit"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^dcredit\\>" "/etc/security/pwquality.conf"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^dcredit\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
    else
        if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/security/pwquality.conf" >> "/etc/security/pwquality.conf"
        printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-611070
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_dcredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_password_pam_dcredit # promote to variable
      set_fact:
        var_password_pam_dcredit: !!str -1
      tags:
        - always

    - name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters - Find
        pwquality.conf.d files
      ansible.builtin.find:
        paths: /etc/security/pwquality.conf.d/
        patterns: '*.conf'
      register: pwquality_conf_d_files
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-611070
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_dcredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters - Ensure
        dcredit is not set in pwquality.conf.d
      ansible.builtin.lineinfile:
        path: '{{ item.path }}'
        regexp: ^\s*\bdcredit\b.*
        state: absent
      with_items: '{{ pwquality_conf_d_files.files }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-611070
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_dcredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters - Ensure
        PAM variable dcredit is set accordingly
      ansible.builtin.lineinfile:
        create: true
        dest: /etc/security/pwquality.conf
        regexp: ^#?\s*dcredit
        line: dcredit = {{ var_password_pam_dcredit }}
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-611070
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_dcredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters [ref]
The pam_pwquality module's `lcredit` parameter controls requirements for usage of lowercase letters in a password. When set to a negative number, any password will be required to contain that many lowercase characters. When set to a positive number, pam_pwquality will grant +1 additional length credit for each lowercase character. Modify the `lcredit` setting in `/etc/security/pwquality.conf` to require the use of a lowercase character in passwords.
Rationale: | Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. Requiring a minimum number of lowercase characters makes password guessing attacks more difficult by ensuring a larger search space.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_lcredit
References: |
cis-csc | 1, 12, 15, 16, 5
cobit5 | DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1
iso27001-2013 | A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3
nist | IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4)
nist-csf | PR.AC-1, PR.AC-6, PR.AC-7
ospp | FMT_SMF_EXT.1
pcidss | Req-8.2.3
os-srg | SRG-OS-000070-GPOS-00038
anssi | R31
ism | 0421, 0422, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561
pcidss4 | 8.3.6, 8.3
stigref | SV-258102r1045207_rule
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q libpwquality; }; then

    var_password_pam_lcredit='-1'

    if grep -sq lcredit /etc/security/pwquality.conf.d/*.conf ; then
        sed -i "/lcredit/d" /etc/security/pwquality.conf.d/*.conf
    fi

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^lcredit")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_lcredit"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^lcredit\\>" "/etc/security/pwquality.conf"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^lcredit\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
    else
        if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/security/pwquality.conf" >> "/etc/security/pwquality.conf"
        printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-611065
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_lcredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_password_pam_lcredit # promote to variable
      set_fact:
        var_password_pam_lcredit: !!str -1
      tags:
        - always

    - name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters -
        Find pwquality.conf.d files
      ansible.builtin.find:
        paths: /etc/security/pwquality.conf.d/
        patterns: '*.conf'
      register: pwquality_conf_d_files
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-611065
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_lcredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters -
        Ensure lcredit is not set in pwquality.conf.d
      ansible.builtin.lineinfile:
        path: '{{ item.path }}'
        regexp: ^\s*\blcredit\b.*
        state: absent
      with_items: '{{ pwquality_conf_d_files.files }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-611065
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_lcredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters -
        Ensure PAM variable lcredit is set accordingly
      ansible.builtin.lineinfile:
        create: true
        dest: /etc/security/pwquality.conf
        regexp: ^#?\s*lcredit
        line: lcredit = {{ var_password_pam_lcredit }}
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-611065
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_lcredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Ensure PAM Enforces Password Requirements - Minimum Length [ref]
The pam_pwquality module's `minlen` parameter controls requirements for minimum characters required in a password. Add `minlen=12` after pam_pwquality to set minimum password length requirements.
Rationale: | The shorter the password, the lower the number of possible combinations that need to be tested before the password is compromised. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password length is one factor of several that helps to determine strength and how long it takes to crack a password. Use of more characters in a password helps to exponentially increase the time and/or resources required to compromise the password.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_minlen
References: |
cis-csc | 1, 12, 15, 16, 5
cjis | 5.6.2.1.1
cobit5 | DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1
iso27001-2013 | A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3
nist | IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4)
nist-csf | PR.AC-1, PR.AC-6, PR.AC-7
ospp | FMT_SMF_EXT.1
pcidss | Req-8.2.3
os-srg | SRG-OS-000078-GPOS-00046
anssi | R31, R68
ism | 0421, 0422, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561
pcidss4 | 8.3.6, 8.3
cis | 5.3.3.2.2
stigref | SV-258107r1045218_rule
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q libpwquality; }; then

    var_password_pam_minlen='12'

    if grep -sq minlen /etc/security/pwquality.conf.d/*.conf ; then
        sed -i "/minlen/d" /etc/security/pwquality.conf.d/*.conf
    fi

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^minlen")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_minlen"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^minlen\\>" "/etc/security/pwquality.conf"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^minlen\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
    else
        if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/security/pwquality.conf" >> "/etc/security/pwquality.conf"
        printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.1.1
      - DISA-STIG-RHEL-09-611090
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_minlen
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_password_pam_minlen # promote to variable
      set_fact:
        var_password_pam_minlen: !!str 12
      tags:
        - always

    - name: Ensure PAM Enforces Password Requirements - Minimum Length - Find pwquality.conf.d
        files
      ansible.builtin.find:
        paths: /etc/security/pwquality.conf.d/
        patterns: '*.conf'
      register: pwquality_conf_d_files
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1.1
      - DISA-STIG-RHEL-09-611090
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_minlen
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure PAM Enforces Password Requirements - Minimum Length - Ensure minlen
        is not set in pwquality.conf.d
      ansible.builtin.lineinfile:
        path: '{{ item.path }}'
        regexp: ^\s*\bminlen\b.*
        state: absent
      with_items: '{{ pwquality_conf_d_files.files }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1.1
      - DISA-STIG-RHEL-09-611090
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_minlen
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure PAM Enforces Password Requirements - Minimum Length - Ensure PAM variable
        minlen is set accordingly
      ansible.builtin.lineinfile:
        create: true
        dest: /etc/security/pwquality.conf
        regexp: ^#?\s*minlen
        line: minlen = {{ var_password_pam_minlen }}
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1.1
      - DISA-STIG-RHEL-09-611090
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_minlen
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Ensure PAM Enforces Password Requirements - Minimum Special Characters [ref]
The pam_pwquality module's `ocredit=` parameter controls requirements for usage of special (or "other") characters in a password. When set to a negative number, any password will be required to contain that many special characters. When set to a positive number, pam_pwquality will grant +1 additional length credit for each special character. Modify the `ocredit` setting in `/etc/security/pwquality.conf` to equal -1 to require use of a special character in passwords.
Rationale: | Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. Requiring a minimum number of special characters makes password guessing attacks more difficult by ensuring a larger search space.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_ocredit
References: |
cis-csc | 1, 12, 15, 16, 5
cobit5 | DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1
iso27001-2013 | A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3
nist | IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4)
nist-csf | PR.AC-1, PR.AC-6, PR.AC-7
ospp | FMT_SMF_EXT.1
os-srg | SRG-OS-000266-GPOS-00101
anssi | R31
ism | 0421, 0422, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561
stigref | SV-258109r1045220_rule
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q libpwquality; }; then

    var_password_pam_ocredit='-1'

    if grep -sq ocredit /etc/security/pwquality.conf.d/*.conf ; then
        sed -i "/ocredit/d" /etc/security/pwquality.conf.d/*.conf
    fi

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^ocredit")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_ocredit"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^ocredit\\>" "/etc/security/pwquality.conf"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^ocredit\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
    else
        if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/security/pwquality.conf" >> "/etc/security/pwquality.conf"
        printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-611100
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - accounts_password_pam_ocredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_password_pam_ocredit # promote to variable
      set_fact:
        var_password_pam_ocredit: !!str -1
      tags:
        - always

    - name: Ensure PAM Enforces Password Requirements - Minimum Special Characters - Find
        pwquality.conf.d files
      ansible.builtin.find:
        paths: /etc/security/pwquality.conf.d/
        patterns: '*.conf'
      register: pwquality_conf_d_files
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-611100
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - accounts_password_pam_ocredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure PAM Enforces Password Requirements - Minimum Special Characters - Ensure
        ocredit is not set in pwquality.conf.d
      ansible.builtin.lineinfile:
        path: '{{ item.path }}'
        regexp: ^\s*\bocredit\b.*
        state: absent
      with_items: '{{ pwquality_conf_d_files.files }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-611100
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - accounts_password_pam_ocredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure PAM Enforces Password Requirements - Minimum Special Characters - Ensure
        PAM variable ocredit is set accordingly
      ansible.builtin.lineinfile:
        create: true
        dest: /etc/security/pwquality.conf
        regexp: ^#?\s*ocredit
        line: ocredit = {{ var_password_pam_ocredit }}
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-611100
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - accounts_password_pam_ocredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted Per-Session [ref]
To configure the number of retry prompts that are permitted per-session: Edit the `/etc/security/pwquality.conf` to include `retry=3` , or a lower value if site policy is more restrictive. The profile requirement is a maximum of `retry=3` prompts per session.
Rationale: | Setting the password retry prompts that are permitted on a per-session basis to a low value requires some software, such as SSH, to re-connect. This can slow down and draw additional attention to some types of password-guessing attacks. Note that this is different from account lockout, which is provided by the pam_faillock module.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_retry
References: |
cis-csc | 1, 11, 12, 15, 16, 3, 5, 9
cjis | 5.5.3
cobit5 | BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 7.6
iso27001-2013 | A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3
nist | CM-6(a), AC-7(a), IA-5(4)
nist-csf | PR.AC-1, PR.AC-6, PR.AC-7, PR.IP-1
os-srg | SRG-OS-000069-GPOS-00037, SRG-OS-000480-GPOS-00227
anssi | R68
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q libpwquality; }; then

    var_password_pam_retry='3'

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^retry")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_retry"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^retry\\>" "/etc/security/pwquality.conf"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^retry\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
    else
        if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/security/pwquality.conf" >> "/etc/security/pwquality.conf"
        printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
    fi

    		if [ -e "/etc/pam.d/password-auth" ] ; then
        PAM_FILE_PATH="/etc/pam.d/password-auth"
        if [ -f /usr/bin/authselect ]; then

            if ! authselect check; then
            echo "
            authselect integrity check failed. Remediation aborted!
            This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
            It is not recommended to manually edit the PAM files when authselect tool is available.
            In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
            exit 1
            fi

            CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
            # If not already in use, a custom profile is created preserving the enabled features.
            if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                # The "local" profile does not contain essential security features required by multiple Benchmarks.
                # If currently used, it is replaced by "sssd", which is the best option in this case.
                if [[ $CURRENT_PROFILE == local ]]; then
                    CURRENT_PROFILE="sssd"
                fi
                authselect create-profile hardening -b $CURRENT_PROFILE
                CURRENT_PROFILE="custom/hardening"

                authselect apply-changes -b --backup=before-hardening-custom-profile
                authselect select $CURRENT_PROFILE
                for feature in $ENABLED_FEATURES; do
                    authselect enable-feature $feature;
                done

                authselect apply-changes -b --backup=after-hardening-custom-profile
            fi
            PAM_FILE_NAME=$(basename "/etc/pam.d/password-auth")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b
        fi

    if grep -qP "^\s*password\s.*\bpam_pwquality.so\s.*\bretry\b" "$PAM_FILE_PATH"; then
        sed -i -E --follow-symlinks "s/(.*password.*pam_pwquality.so.*)\bretry\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH"
    fi
        if [ -f /usr/bin/authselect ]; then

            authselect apply-changes -b
        fi
    else
        echo "/etc/pam.d/password-auth was not found" >&2
    fi

    		if [ -e "/etc/pam.d/system-auth" ] ; then
        PAM_FILE_PATH="/etc/pam.d/system-auth"
        if [ -f /usr/bin/authselect ]; then

            if ! authselect check; then
            echo "
            authselect integrity check failed. Remediation aborted!
            This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
            It is not recommended to manually edit the PAM files when authselect tool is available.
            In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
            exit 1
            fi

            CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
            # If not already in use, a custom profile is created preserving the enabled features.
            if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                # The "local" profile does not contain essential security features required by multiple Benchmarks.
                # If currently used, it is replaced by "sssd", which is the best option in this case.
                if [[ $CURRENT_PROFILE == local ]]; then
                    CURRENT_PROFILE="sssd"
                fi
                authselect create-profile hardening -b $CURRENT_PROFILE
                CURRENT_PROFILE="custom/hardening"

                authselect apply-changes -b --backup=before-hardening-custom-profile
                authselect select $CURRENT_PROFILE
                for feature in $ENABLED_FEATURES; do
                    authselect enable-feature $feature;
                done

                authselect apply-changes -b --backup=after-hardening-custom-profile
            fi
            PAM_FILE_NAME=$(basename "/etc/pam.d/system-auth")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b
        fi

    if grep -qP "^\s*password\s.*\bpam_pwquality.so\s.*\bretry\b" "$PAM_FILE_PATH"; then
        sed -i -E --follow-symlinks "s/(.*password.*pam_pwquality.so.*)\bretry\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH"
    fi
        if [ -f /usr/bin/authselect ]; then

            authselect apply-changes -b
        fi
    else
        echo "/etc/pam.d/system-auth was not found" >&2
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.5.3
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(4)
      - accounts_password_pam_retry
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
    - name: XCCDF Value var_password_pam_retry # promote to variable
      set_fact:
        var_password_pam_retry: !!str 3
      tags:
        - always

    - name: Ensure PAM variable retry is set accordingly
      ansible.builtin.lineinfile:
        create: true
        dest: /etc/security/pwquality.conf
        regexp: ^\s*retry
        line: retry = {{ var_password_pam_retry }}
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - CJIS-5.5.3
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(4)
      - accounts_password_pam_retry
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted
        Per-Session - Check if /etc/pam.d/password-auth file is present
      ansible.builtin.stat:
        path: /etc/pam.d/password-auth
      register: result_pam_password_auth_file_present
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - CJIS-5.5.3
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(4)
      - accounts_password_pam_retry
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted
        Per-Session - Check the proper remediation for the system
      block:

      - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
          Permitted Per-Session - Define the PAM file to be edited as a local fact
        ansible.builtin.set_fact:
          pam_file_path: /etc/pam.d/password-auth

      - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
          Permitted Per-Session - Check if system relies on authselect tool
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

      - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
          Permitted Per-Session - Ensure authselect custom profile is used if authselect
          is present
        block:

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Check integrity of authselect current profile
          ansible.builtin.command:
            cmd: authselect check
          register: result_authselect_check_cmd
          changed_when: false
          check_mode: false
          failed_when: false

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Informative message based on the authselect integrity
            check result
          ansible.builtin.assert:
            that:
            - ansible_check_mode or result_authselect_check_cmd.rc == 0
            fail_msg:
            - authselect integrity check failed. Remediation aborted!
            - This remediation could not be applied because an authselect profile was
              not selected or the selected profile is not intact.
            - It is not recommended to manually edit the PAM files when authselect tool
              is available.
            - In cases where the default authselect profile does not cover a specific
              demand, a custom authselect profile is recommended.
            success_msg:
            - authselect integrity check passed

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Get authselect current profile
          ansible.builtin.shell:
            cmd: authselect current -r | awk '{ print $1 }'
          register: result_authselect_profile
          changed_when: false
          when:
          - result_authselect_check_cmd is success

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Define the current authselect profile as a local fact
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is match("custom/")

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Define the new authselect custom profile as a local
            fact
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: custom/hardening
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is not match("custom/")

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Get authselect current features to also enable them
            in the custom profile
          ansible.builtin.shell:
            cmd: authselect current | tail -n+3 | awk '{ print $2 }'
          register: result_authselect_features
          changed_when: false
          check_mode: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Check if any custom profile with the same name was
            already created
          ansible.builtin.stat:
            path: /etc/authselect/{{ authselect_custom_profile }}
          register: result_authselect_custom_profile_present
          changed_when: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Create an authselect custom profile based on the current
            profile
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b {{ authselect_current_profile
              }}
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is not match("^(custom/|local)")
          - not result_authselect_custom_profile_present.stat.exists

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Create an authselect custom profile based on sssd
            profile
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b sssd
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is match("local")
          - not result_authselect_custom_profile_present.stat.exists

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Ensure authselect changes are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Ensure the authselect custom profile is selected
          ansible.builtin.command:
            cmd: authselect select {{ authselect_custom_profile }}
          register: result_pam_authselect_select_profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Restore the authselect features in the custom profile
          ansible.builtin.command:
            cmd: authselect enable-feature {{ item }}
          loop: '{{ result_authselect_features.stdout_lines }}'
          register: result_pam_authselect_restore_features
          when:
          - result_authselect_profile is not skipped
          - result_authselect_features is not skipped
          - result_pam_authselect_select_profile is not skipped

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Ensure authselect changes are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - result_pam_authselect_restore_features is not skipped

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Change the PAM file to be edited according to the
            custom authselect profile
          ansible.builtin.set_fact:
            pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
              | basename }}
          when:
          - authselect_custom_profile is defined
        when:
        - result_authselect_present.stat.exists

      - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
          Permitted Per-Session - Define a fact for control already filtered in case filters
          are used
        ansible.builtin.set_fact:
          pam_module_control: ''

      - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
          Permitted Per-Session - Check if {{ pam_file_path }} file is present
        ansible.builtin.stat:
          path: '{{ pam_file_path }}'
        register: result_pam_file_present

      - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
          Permitted Per-Session - Ensure the "retry" option from "pam_pwquality.so" is
          not present in {{ pam_file_path }}
        ansible.builtin.replace:
          dest: '{{ pam_file_path }}'
          regexp: (.*password.*pam_pwquality.so.*)\bretry\b=?[0-9a-zA-Z]*(.*)
          replace: \1\2
        register: result_pam_option_removal
        when:
        - result_pam_file_present.stat.exists

      - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
          Permitted Per-Session - Ensure authselect changes are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_present.stat.exists
        - result_pam_option_removal is changed
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      - result_pam_password_auth_file_present.stat.exists
      tags:
      - CJIS-5.5.3
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(4)
      - accounts_password_pam_retry
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted
        Per-Session - Check if /etc/pam.d/system-auth file is present
      ansible.builtin.stat:
        path: /etc/pam.d/system-auth
      register: result_pam_auth_file_present
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - CJIS-5.5.3
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(4)
      - accounts_password_pam_retry
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts Permitted
        Per-Session - Check the proper remediation for the system
      block:

      - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
          Permitted Per-Session - Define the PAM file to be edited as a local fact
        ansible.builtin.set_fact:
          pam_file_path: /etc/pam.d/system-auth

      - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
          Permitted Per-Session - Check if system relies on authselect tool
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

      - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
          Permitted Per-Session - Ensure authselect custom profile is used if authselect
          is present
        block:

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Check integrity of authselect current profile
          ansible.builtin.command:
            cmd: authselect check
          register: result_authselect_check_cmd
          changed_when: false
          check_mode: false
          failed_when: false

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Informative message based on the authselect integrity
            check result
          ansible.builtin.assert:
            that:
            - ansible_check_mode or result_authselect_check_cmd.rc == 0
            fail_msg:
            - authselect integrity check failed. Remediation aborted!
            - This remediation could not be applied because an authselect profile was
              not selected or the selected profile is not intact.
            - It is not recommended to manually edit the PAM files when authselect tool
              is available.
            - In cases where the default authselect profile does not cover a specific
              demand, a custom authselect profile is recommended.
            success_msg:
            - authselect integrity check passed

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Get authselect current profile
          ansible.builtin.shell:
            cmd: authselect current -r | awk '{ print $1 }'
          register: result_authselect_profile
          changed_when: false
          when:
          - result_authselect_check_cmd is success

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Define the current authselect profile as a local fact
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is match("custom/")

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Define the new authselect custom profile as a local
            fact
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: custom/hardening
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is not match("custom/")

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Get authselect current features to also enable them
            in the custom profile
          ansible.builtin.shell:
            cmd: authselect current | tail -n+3 | awk '{ print $2 }'
          register: result_authselect_features
          changed_when: false
          check_mode: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Check if any custom profile with the same name was
            already created
          ansible.builtin.stat:
            path: /etc/authselect/{{ authselect_custom_profile }}
          register: result_authselect_custom_profile_present
          changed_when: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Create an authselect custom profile based on the current
            profile
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b {{ authselect_current_profile
              }}
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is not match("^(custom/|local)")
          - not result_authselect_custom_profile_present.stat.exists

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Create an authselect custom profile based on sssd
            profile
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b sssd
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is match("local")
          - not result_authselect_custom_profile_present.stat.exists

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Ensure authselect changes are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Ensure the authselect custom profile is selected
          ansible.builtin.command:
            cmd: authselect select {{ authselect_custom_profile }}
          register: result_pam_authselect_select_profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Restore the authselect features in the custom profile
          ansible.builtin.command:
            cmd: authselect enable-feature {{ item }}
          loop: '{{ result_authselect_features.stdout_lines }}'
          register: result_pam_authselect_restore_features
          when:
          - result_authselect_profile is not skipped
          - result_authselect_features is not skipped
          - result_pam_authselect_select_profile is not skipped

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Ensure authselect changes are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - result_pam_authselect_restore_features is not skipped

        - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
            Permitted Per-Session - Change the PAM file to be edited according to the
            custom authselect profile
          ansible.builtin.set_fact:
            pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
              | basename }}
          when:
          - authselect_custom_profile is defined
        when:
        - result_authselect_present.stat.exists

      - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
          Permitted Per-Session - Define a fact for control already filtered in case filters
          are used
        ansible.builtin.set_fact:
          pam_module_control: ''

      - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
          Permitted Per-Session - Check if {{ pam_file_path }} file is present
        ansible.builtin.stat:
          path: '{{ pam_file_path }}'
        register: result_pam_file_present

      - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
          Permitted Per-Session - Ensure the "retry" option from "pam_pwquality.so" is
          not present in {{ pam_file_path }}
        ansible.builtin.replace:
          dest: '{{ pam_file_path }}'
          regexp: (.*password.*pam_pwquality.so.*)\bretry\b=?[0-9a-zA-Z]*(.*)
          replace: \1\2
        register: result_pam_option_removal
        when:
        - result_pam_file_present.stat.exists

      - name: Ensure PAM Enforces Password Requirements - Authentication Retry Prompts
          Permitted Per-Session - Ensure authselect changes are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_present.stat.exists
        - result_pam_option_removal is changed
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      - result_pam_auth_file_present.stat.exists
      tags:
      - CJIS-5.5.3
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(4)
      - accounts_password_pam_retry
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
#### Rule Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters [ref]
The pam_pwquality module's `ucredit=` parameter controls requirements for usage of uppercase letters in a password. When set to a negative number, any password will be required to contain that many uppercase characters. When set to a positive number, pam_pwquality will grant +1 additional length credit for each uppercase character. Modify the `ucredit` setting in `/etc/security/pwquality.conf` to require the use of an uppercase character in passwords.
Rationale: | Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_ucredit
References: |
cis-csc | 1, 12, 15, 16, 5
cobit5 | DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1
iso27001-2013 | A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3
nist | IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4)
nist-csf | PR.AC-1, PR.AC-6, PR.AC-7
ospp | FMT_SMF_EXT.1
pcidss | Req-8.2.3
os-srg | SRG-OS-000069-GPOS-00037, SRG-OS-000070-GPOS-00038
anssi | R31
ism | 0421, 0422, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561
stigref | SV-258111r1045226_rule
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q libpwquality; }; then

    var_password_pam_ucredit='-1'

    if grep -sq ucredit /etc/security/pwquality.conf.d/*.conf ; then
        sed -i "/ucredit/d" /etc/security/pwquality.conf.d/*.conf
    fi

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^ucredit")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_ucredit"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^ucredit\\>" "/etc/security/pwquality.conf"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^ucredit\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
    else
        if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/security/pwquality.conf" >> "/etc/security/pwquality.conf"
        printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-611110
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - accounts_password_pam_ucredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_password_pam_ucredit # promote to variable
      set_fact:
        var_password_pam_ucredit: !!str -1
      tags:
        - always

    - name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters -
        Find pwquality.conf.d files
      ansible.builtin.find:
        paths: /etc/security/pwquality.conf.d/
        patterns: '*.conf'
      register: pwquality_conf_d_files
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-611110
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - accounts_password_pam_ucredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters -
        Ensure ucredit is not set in pwquality.conf.d
      ansible.builtin.lineinfile:
        path: '{{ item.path }}'
        regexp: ^\s*\bucredit\b.*
        state: absent
      with_items: '{{ pwquality_conf_d_files.files }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-611110
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - accounts_password_pam_ucredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters -
        Ensure PAM variable ucredit is set accordingly
      ansible.builtin.lineinfile:
        create: true
        dest: /etc/security/pwquality.conf
        regexp: ^#?\s*ucredit
        line: ucredit = {{ var_password_pam_ucredit }}
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"libpwquality" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-611110
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - accounts_password_pam_ucredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
Group Set Password Hashing Algorithm Group contains 3 rules
[ref] The system's default algorithm for storing password hashes in `/etc/shadow` is SHA-512. This can be configured in several locations.
#### Rule Set Password Hashing Algorithm in /etc/login.defs [ref]
In `/etc/login.defs`, add or update the following line to ensure the system will use SHA512 as the hashing algorithm:
    ENCRYPT_METHOD SHA512
Rationale: | Passwords need to be protected at all times, and encryption is the standard method for protecting passwords. If passwords are not encrypted, they can be plainly read (i.e., clear text) and easily compromised. Passwords that are encrypted with a weak algorithm are no more protected than if they are kept in plain text. Using a stronger hashing algorithm makes password cracking attacks more difficult.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_set_password_hashing_algorithm_logindefs
References: |
cis-csc | 1, 12, 15, 16, 5
cjis | 5.6.2.2
cobit5 | DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
cui | 3.13.11
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1
iso27001-2013 | A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3
nist | IA-5(c), IA-5(1)(c), CM-6(a)
nist-csf | PR.AC-1, PR.AC-6, PR.AC-7
pcidss | Req-8.2.1
os-srg | SRG-OS-000073-GPOS-00041
ism | 0418, 1055, 1402
pcidss4 | 8.3.2, 8.3
cis | 5.4.1.4
stigref | SV-258117r1015116_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q shadow-utils; }; then

    var_password_hashing_algorithm='SHA512'

    # Allow multiple algorithms, but choose the first one for remediation
    #
    var_password_hashing_algorithm="$(echo $var_password_hashing_algorithm | cut -d \| -f 1)"

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^ENCRYPT_METHOD")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s %s" "$stripped_key" "$var_password_hashing_algorithm"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^ENCRYPT_METHOD\\>" "/etc/login.defs"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^ENCRYPT_METHOD\\>.*/$escaped_formatted_output/gi" "/etc/login.defs"
    else
        if [[ -s "/etc/login.defs" ]] && [[ -n "$(tail -c 1 -- "/etc/login.defs" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/login.defs"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/login.defs" >> "/etc/login.defs"
        printf '%s\n' "$formatted_output" >> "/etc/login.defs"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.2
      - DISA-STIG-RHEL-09-611140
      - NIST-800-171-3.13.11
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(c)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.1
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.2
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - set_password_hashing_algorithm_logindefs
    - name: XCCDF Value var_password_hashing_algorithm # promote to variable
      set_fact:
        var_password_hashing_algorithm: !!str SHA512
      tags:
        - always

    - name: Set Password Hashing Algorithm in /etc/login.defs
      ansible.builtin.lineinfile:
        dest: /etc/login.defs
        regexp: ^#?ENCRYPT_METHOD
        line: ENCRYPT_METHOD {{ var_password_hashing_algorithm.split('|')[0] }}
        state: present
        create: true
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"shadow-utils" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.2
      - DISA-STIG-RHEL-09-611140
      - NIST-800-171-3.13.11
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(c)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.1
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.2
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - set_password_hashing_algorithm_logindefs
#### Rule Set PAM Password Hashing Algorithm - password-auth [ref]
The PAM system service can be configured to only store encrypted representations of passwords. In `/etc/pam.d/password-auth`, the `password` section of the file controls which PAM modules to execute during a password change. Set the `pam_unix.so` module in the `password` section to include the option `sha512` and no other hashing algorithms as shown below:
    password    sufficient    pam_unix.so sha512
                        other arguments...
This will help ensure that new passwords for local users will be stored using the sha512 algorithm. Warning: The hashing algorithms to be used with pam_unix.so are defined with independent module options. There are at least 7 possible algorithms and likely more algorithms will be introduced along the time. Due the the number of options and its possible combinations, the use of multiple hashing algorithm options may bring unexpected behaviors to the system. For this reason the check will pass only when one hashing algorithm option is defined and is aligned to the "var_password_hashing_algorithm_pam" variable. The remediation will ensure the correct option and remove any other extra hashing algorithm option. Warning: The CIS Red Hat Enterprise Linux 9 Benchmark permits both "sha512" and "yescrypt" algorithms for password hashing. This profile is configured to check for "sha512", but a system using either algorithm is compliant with the CIS Benchmark requirements. To use a different algorithm, create a tailored profile and select your preferred value for the `var_password_hashing_algorithm_pam` variable.
Rationale: | Passwords need to be protected at all times, and encryption is the standard method for protecting passwords. If passwords are not encrypted, they can be plainly read (i.e., clear text) and easily compromised. Passwords that are encrypted with a weak algorithm are no more protected than if they are kept in plain text. This setting ensures user and group account administration utilities are configured to store only encrypted representations of passwords. Additionally, the `crypt_style` configuration option in `/etc/libuser.conf` ensures the use of a strong hashing algorithm that makes password cracking attacks more difficult.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_set_password_hashing_algorithm_passwordauth
References: |
cis-csc | 1, 12, 15, 16, 5
cjis | 5.6.2.2
cobit5 | DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
cui | 3.13.11
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1
iso27001-2013 | A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3
nist | IA-5(c), IA-5(1)(c), CM-6(a)
nist-csf | PR.AC-1, PR.AC-6, PR.AC-7
pcidss | Req-8.2.1
os-srg | SRG-OS-000073-GPOS-00041, SRG-OS-000120-GPOS-00061
ism | 0418, 1055, 1402
cis | 5.3.3.4.3
stigref | SV-258233r1015136_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q pam; }; then

    var_password_hashing_algorithm_pam='sha512'

    # Allow multiple algorithms, but choose the first one for remediation
    var_password_hashing_algorithm_pam="$(echo $var_password_hashing_algorithm_pam | cut -d \| -f 1)"

    PAM_FILE_PATH="/etc/pam.d/password-auth"

    if [ -e "$PAM_FILE_PATH" ] ; then
        PAM_FILE_PATH="$PAM_FILE_PATH"
        if [ -f /usr/bin/authselect ]; then

            if ! authselect check; then
            echo "
            authselect integrity check failed. Remediation aborted!
            This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
            It is not recommended to manually edit the PAM files when authselect tool is available.
            In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
            exit 1
            fi

            CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
            # If not already in use, a custom profile is created preserving the enabled features.
            if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                # The "local" profile does not contain essential security features required by multiple Benchmarks.
                # If currently used, it is replaced by "sssd", which is the best option in this case.
                if [[ $CURRENT_PROFILE == local ]]; then
                    CURRENT_PROFILE="sssd"
                fi
                authselect create-profile hardening -b $CURRENT_PROFILE
                CURRENT_PROFILE="custom/hardening"

                authselect apply-changes -b --backup=before-hardening-custom-profile
                authselect select $CURRENT_PROFILE
                for feature in $ENABLED_FEATURES; do
                    authselect enable-feature $feature;
                done

                authselect apply-changes -b --backup=after-hardening-custom-profile
            fi
            PAM_FILE_NAME=$(basename "$PAM_FILE_PATH")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b
        fi

            if ! grep -qP "^\s*password\s+sufficient\s+pam_unix.so\s*.*" "$PAM_FILE_PATH"; then
                # Line matching group + control + module was not found. Check group + module.
                if [ "$(grep -cP '^\s*password\s+.*\s+pam_unix.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then
                    # The control is updated only if one single line matches.
                    sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_unix.so.*)/\1sufficient \2/" "$PAM_FILE_PATH"
                else
                    echo "password    sufficient    pam_unix.so" >> "$PAM_FILE_PATH"
                fi
            fi
            # Check the option
            if ! grep -qP "^\s*password\s+sufficient\s+pam_unix.so\s*.*\s$var_password_hashing_algorithm_pam\b" "$PAM_FILE_PATH"; then
                sed -i -E --follow-symlinks "/\s*password\s+sufficient\s+pam_unix.so.*/ s/$/ $var_password_hashing_algorithm_pam/" "$PAM_FILE_PATH"
            fi
        if [ -f /usr/bin/authselect ]; then

            authselect apply-changes -b
        fi
    else
        echo "$PAM_FILE_PATH was not found" >&2
    fi

    # Ensure only the correct hashing algorithm option is used.
    declare -a HASHING_ALGORITHMS_OPTIONS=("sha512" "yescrypt" "gost_yescrypt" "blowfish" "sha256" "md5" "bigcrypt")

    for hash_option in "${HASHING_ALGORITHMS_OPTIONS[@]}"; do
      if [ "$hash_option" != "$var_password_hashing_algorithm_pam" ]; then
        if grep -qP "^\s*password\s+.*\s+pam_unix.so\s+.*\b$hash_option\b" "$PAM_FILE_PATH"; then
          if [ -e "$PAM_FILE_PATH" ] ; then
        PAM_FILE_PATH="$PAM_FILE_PATH"
        if [ -f /usr/bin/authselect ]; then

            if ! authselect check; then
            echo "
            authselect integrity check failed. Remediation aborted!
            This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
            It is not recommended to manually edit the PAM files when authselect tool is available.
            In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
            exit 1
            fi

            CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
            # If not already in use, a custom profile is created preserving the enabled features.
            if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                # The "local" profile does not contain essential security features required by multiple Benchmarks.
                # If currently used, it is replaced by "sssd", which is the best option in this case.
                if [[ $CURRENT_PROFILE == local ]]; then
                    CURRENT_PROFILE="sssd"
                fi
                authselect create-profile hardening -b $CURRENT_PROFILE
                CURRENT_PROFILE="custom/hardening"

                authselect apply-changes -b --backup=before-hardening-custom-profile
                authselect select $CURRENT_PROFILE
                for feature in $ENABLED_FEATURES; do
                    authselect enable-feature $feature;
                done

                authselect apply-changes -b --backup=after-hardening-custom-profile
            fi
            PAM_FILE_NAME=$(basename "$PAM_FILE_PATH")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b
        fi

    if grep -qP "^\s*password\s+.*\s+pam_unix.so\s.*\b$hash_option\b" "$PAM_FILE_PATH"; then
        sed -i -E --follow-symlinks "s/(.*password.*.*.*pam_unix.so.*)\b$hash_option\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH"
    fi
        if [ -f /usr/bin/authselect ]; then

            authselect apply-changes -b
        fi
    else
        echo "$PAM_FILE_PATH was not found" >&2
    fi
        fi
      fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.2
      - DISA-STIG-RHEL-09-671025
      - NIST-800-171-3.13.11
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(c)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.1
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - set_password_hashing_algorithm_passwordauth
    - name: XCCDF Value var_password_hashing_algorithm_pam # promote to variable
      set_fact:
        var_password_hashing_algorithm_pam: !!str sha512
      tags:
        - always

    - name: Set PAM Password Hashing Algorithm - password-auth - Check if /etc/pam.d/password-auth
        file is present
      ansible.builtin.stat:
        path: /etc/pam.d/password-auth
      register: result_pam_password_auth_file_present
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.2
      - DISA-STIG-RHEL-09-671025
      - NIST-800-171-3.13.11
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(c)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.1
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - set_password_hashing_algorithm_passwordauth

    - name: Set PAM Password Hashing Algorithm - password-auth - Check the proper remediation
        for the system
      block:

      - name: Set PAM Password Hashing Algorithm - password-auth - Define the PAM file
          to be edited as a local fact
        ansible.builtin.set_fact:
          pam_file_path: /etc/pam.d/password-auth

      - name: Set PAM Password Hashing Algorithm - password-auth - Check if system relies
          on authselect tool
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

      - name: Set PAM Password Hashing Algorithm - password-auth - Ensure authselect custom
          profile is used if authselect is present
        block:

        - name: Set PAM Password Hashing Algorithm - password-auth - Check integrity of
            authselect current profile
          ansible.builtin.command:
            cmd: authselect check
          register: result_authselect_check_cmd
          changed_when: false
          check_mode: false
          failed_when: false

        - name: Set PAM Password Hashing Algorithm - password-auth - Informative message
            based on the authselect integrity check result
          ansible.builtin.assert:
            that:
            - ansible_check_mode or result_authselect_check_cmd.rc == 0
            fail_msg:
            - authselect integrity check failed. Remediation aborted!
            - This remediation could not be applied because an authselect profile was
              not selected or the selected profile is not intact.
            - It is not recommended to manually edit the PAM files when authselect tool
              is available.
            - In cases where the default authselect profile does not cover a specific
              demand, a custom authselect profile is recommended.
            success_msg:
            - authselect integrity check passed

        - name: Set PAM Password Hashing Algorithm - password-auth - Get authselect current
            profile
          ansible.builtin.shell:
            cmd: authselect current -r | awk '{ print $1 }'
          register: result_authselect_profile
          changed_when: false
          when:
          - result_authselect_check_cmd is success

        - name: Set PAM Password Hashing Algorithm - password-auth - Define the current
            authselect profile as a local fact
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is match("custom/")

        - name: Set PAM Password Hashing Algorithm - password-auth - Define the new authselect
            custom profile as a local fact
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: custom/hardening
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is not match("custom/")

        - name: Set PAM Password Hashing Algorithm - 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 }'
          register: result_authselect_features
          changed_when: false
          check_mode: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: Set PAM Password Hashing Algorithm - password-auth - Check if any custom
            profile with the same name was already created
          ansible.builtin.stat:
            path: /etc/authselect/{{ authselect_custom_profile }}
          register: result_authselect_custom_profile_present
          changed_when: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: Set PAM Password Hashing Algorithm - password-auth - Create an authselect
            custom profile based on the current profile
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b {{ authselect_current_profile
              }}
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is not match("^(custom/|local)")
          - not result_authselect_custom_profile_present.stat.exists

        - name: Set PAM Password Hashing Algorithm - password-auth - Create an authselect
            custom profile based on sssd profile
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b sssd
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is match("local")
          - not result_authselect_custom_profile_present.stat.exists

        - name: Set PAM Password Hashing Algorithm - password-auth - Ensure authselect
            changes are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: Set PAM Password Hashing Algorithm - password-auth - Ensure the authselect
            custom profile is selected
          ansible.builtin.command:
            cmd: authselect select {{ authselect_custom_profile }}
          register: result_pam_authselect_select_profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: Set PAM Password Hashing Algorithm - password-auth - Restore the authselect
            features in the custom profile
          ansible.builtin.command:
            cmd: authselect enable-feature {{ item }}
          loop: '{{ result_authselect_features.stdout_lines }}'
          register: result_pam_authselect_restore_features
          when:
          - result_authselect_profile is not skipped
          - result_authselect_features is not skipped
          - result_pam_authselect_select_profile is not skipped

        - name: Set PAM Password Hashing Algorithm - password-auth - Ensure authselect
            changes are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - result_pam_authselect_restore_features is not skipped

        - name: Set PAM Password Hashing Algorithm - password-auth - Change the PAM file
            to be edited according to the custom authselect profile
          ansible.builtin.set_fact:
            pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
              | basename }}
          when:
          - authselect_custom_profile is defined
        when:
        - result_authselect_present.stat.exists

      - name: Set PAM Password Hashing Algorithm - password-auth - Define a fact for control
          already filtered in case filters are used
        ansible.builtin.set_fact:
          pam_module_control: sufficient

      - name: Set PAM Password Hashing Algorithm - password-auth - Check if expected PAM
          module line is present in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so\s*.*
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_line_present

      - name: Set PAM Password Hashing Algorithm - password-auth - Include or update the
          PAM module line in {{ pam_file_path }}
        block:

        - name: Set PAM Password Hashing Algorithm - password-auth - Check if required
            PAM module line is present in {{ pam_file_path }} with different control
          ansible.builtin.lineinfile:
            path: '{{ pam_file_path }}'
            regexp: ^\s*password\s+.*\s+pam_unix.so\s*
            state: absent
          check_mode: true
          changed_when: false
          register: result_pam_line_other_control_present

        - name: Set PAM Password Hashing Algorithm - password-auth - Ensure the correct
            control for the required PAM module line in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: ^(\s*password\s+).*(\bpam_unix.so.*)
            replace: \1{{ pam_module_control }} \2
          register: result_pam_module_edit
          when:
          - result_pam_line_other_control_present.found == 1

        - name: Set PAM Password Hashing Algorithm - password-auth - Ensure the required
            PAM module line is included in {{ pam_file_path }}
          ansible.builtin.lineinfile:
            dest: '{{ pam_file_path }}'
            line: password    {{ pam_module_control }}    pam_unix.so
          register: result_pam_module_add
          when:
          - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found
            > 1

        - name: Set PAM Password Hashing Algorithm - password-auth - Ensure authselect
            changes are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b
          when:
          - result_authselect_present is defined
          - result_authselect_present.stat.exists
          - |-
            (result_pam_module_add is defined and result_pam_module_add.changed)
             or (result_pam_module_edit is defined and result_pam_module_edit.changed)
        when:
        - result_pam_line_present.found is defined
        - result_pam_line_present.found == 0

      - name: Set PAM Password Hashing Algorithm - password-auth - Define a fact for control
          already filtered in case filters are used
        ansible.builtin.set_fact:
          pam_module_control: sufficient

      - name: Set PAM Password Hashing Algorithm - password-auth - Check if the required
          PAM module option is present in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so\s*.*\s{{
            var_password_hashing_algorithm_pam.split("|")[0] }}\b
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_module_set_password_hashing_algorithm_passwordauth_option_present

      - name: Set PAM Password Hashing Algorithm - password-auth - Ensure the "{{ var_password_hashing_algorithm_pam.split("|")[0]
          }}" PAM option for "pam_unix.so" is included in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          backrefs: true
          regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so.*)
          line: \1 {{ var_password_hashing_algorithm_pam.split("|")[0] }}
          state: present
        register: result_pam_set_password_hashing_algorithm_passwordauth_add
        when:
        - result_pam_module_set_password_hashing_algorithm_passwordauth_option_present.found
          is defined
        - result_pam_module_set_password_hashing_algorithm_passwordauth_option_present.found
          == 0

      - name: Set PAM Password Hashing Algorithm - password-auth - Ensure authselect changes
          are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_present.stat.exists
        - |-
          (result_pam_set_password_hashing_algorithm_passwordauth_add is defined and result_pam_set_password_hashing_algorithm_passwordauth_add.changed)
           or (result_pam_set_password_hashing_algorithm_passwordauth_edit is defined and result_pam_set_password_hashing_algorithm_passwordauth_edit.changed)
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      - result_pam_password_auth_file_present.stat.exists
      tags:
      - CJIS-5.6.2.2
      - DISA-STIG-RHEL-09-671025
      - NIST-800-171-3.13.11
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(c)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.1
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - set_password_hashing_algorithm_passwordauth

    - name: Set PAM Password Hashing Algorithm - password-auth - Check if /etc/pam.d/password-auth
        File is Present
      ansible.builtin.stat:
        path: /etc/pam.d/password-auth
      register: result_pam_password_auth_file_present
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.2
      - DISA-STIG-RHEL-09-671025
      - NIST-800-171-3.13.11
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(c)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.1
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - set_password_hashing_algorithm_passwordauth

    - name: Set PAM Password Hashing Algorithm - password-auth - Check The Proper Remediation
        For The System
      block:

      - name: Set PAM Password Hashing Algorithm - password-auth - Define the PAM file
          to be edited as a local fact
        ansible.builtin.set_fact:
          pam_file_path: /etc/pam.d/password-auth

      - name: Set PAM Password Hashing Algorithm - password-auth - Check if system relies
          on authselect tool
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

      - name: Set PAM Password Hashing Algorithm - password-auth - Ensure authselect custom
          profile is used if authselect is present
        block:

        - name: Set PAM Password Hashing Algorithm - password-auth - Check integrity of
            authselect current profile
          ansible.builtin.command:
            cmd: authselect check
          register: result_authselect_check_cmd
          changed_when: false
          check_mode: false
          failed_when: false

        - name: Set PAM Password Hashing Algorithm - password-auth - Informative message
            based on the authselect integrity check result
          ansible.builtin.assert:
            that:
            - ansible_check_mode or result_authselect_check_cmd.rc == 0
            fail_msg:
            - authselect integrity check failed. Remediation aborted!
            - This remediation could not be applied because an authselect profile was
              not selected or the selected profile is not intact.
            - It is not recommended to manually edit the PAM files when authselect tool
              is available.
            - In cases where the default authselect profile does not cover a specific
              demand, a custom authselect profile is recommended.
            success_msg:
            - authselect integrity check passed

        - name: Set PAM Password Hashing Algorithm - password-auth - Get authselect current
            profile
          ansible.builtin.shell:
            cmd: authselect current -r | awk '{ print $1 }'
          register: result_authselect_profile
          changed_when: false
          when:
          - result_authselect_check_cmd is success

        - name: Set PAM Password Hashing Algorithm - password-auth - Define the current
            authselect profile as a local fact
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is match("custom/")

        - name: Set PAM Password Hashing Algorithm - password-auth - Define the new authselect
            custom profile as a local fact
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: custom/hardening
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is not match("custom/")

        - name: Set PAM Password Hashing Algorithm - 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 }'
          register: result_authselect_features
          changed_when: false
          check_mode: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: Set PAM Password Hashing Algorithm - password-auth - Check if any custom
            profile with the same name was already created
          ansible.builtin.stat:
            path: /etc/authselect/{{ authselect_custom_profile }}
          register: result_authselect_custom_profile_present
          changed_when: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: Set PAM Password Hashing Algorithm - password-auth - Create an authselect
            custom profile based on the current profile
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b {{ authselect_current_profile
              }}
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is not match("^(custom/|local)")
          - not result_authselect_custom_profile_present.stat.exists

        - name: Set PAM Password Hashing Algorithm - password-auth - Create an authselect
            custom profile based on sssd profile
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b sssd
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is match("local")
          - not result_authselect_custom_profile_present.stat.exists

        - name: Set PAM Password Hashing Algorithm - password-auth - Ensure authselect
            changes are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: Set PAM Password Hashing Algorithm - password-auth - Ensure the authselect
            custom profile is selected
          ansible.builtin.command:
            cmd: authselect select {{ authselect_custom_profile }}
          register: result_pam_authselect_select_profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: Set PAM Password Hashing Algorithm - password-auth - Restore the authselect
            features in the custom profile
          ansible.builtin.command:
            cmd: authselect enable-feature {{ item }}
          loop: '{{ result_authselect_features.stdout_lines }}'
          register: result_pam_authselect_restore_features
          when:
          - result_authselect_profile is not skipped
          - result_authselect_features is not skipped
          - result_pam_authselect_select_profile is not skipped

        - name: Set PAM Password Hashing Algorithm - password-auth - Ensure authselect
            changes are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - result_pam_authselect_restore_features is not skipped

        - name: Set PAM Password Hashing Algorithm - password-auth - Change the PAM file
            to be edited according to the custom authselect profile
          ansible.builtin.set_fact:
            pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
              | basename }}
          when:
          - authselect_custom_profile is defined
        when:
        - result_authselect_present.stat.exists

      - name: Set PAM Password Hashing Algorithm - password-auth - Check if "{{ pam_file_path
          }}" File is Present
        ansible.builtin.stat:
          path: '{{ pam_file_path }}'
        register: pam_file_path_present

      - name: Set PAM Password Hashing Algorithm - password-auth - Ensure That Only the
          Correct Hashing Algorithm Option For pam_unix.so Is Used in {{ pam_file_path
          }}
        ansible.builtin.replace:
          dest: '{{ pam_file_path }}'
          regexp: (^\s*password.*pam_unix\.so.*)\b{{ item }}\b\s*(.*)
          replace: \1\2
        when:
        - item != var_password_hashing_algorithm_pam.split('|')[0]
        - pam_file_path_present.stat.exists
        loop:
        - sha512
        - yescrypt
        - gost_yescrypt
        - blowfish
        - sha256
        - md5
        - bigcrypt
        register: result_pam_hashing_options_removal

      - name: Set PAM Password Hashing Algorithm - password-auth - Ensure authselect changes
          are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_present.stat.exists
        - result_pam_hashing_options_removal is changed
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      - result_pam_password_auth_file_present.stat.exists
      tags:
      - CJIS-5.6.2.2
      - DISA-STIG-RHEL-09-671025
      - NIST-800-171-3.13.11
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(c)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.1
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - set_password_hashing_algorithm_passwordauth
#### Rule Set PAM Password Hashing Algorithm - system-auth [ref]
The PAM system service can be configured to only store encrypted representations of passwords. In "/etc/pam.d/system-auth", the `password` section of the file controls which PAM modules to execute during a password change. Set the `pam_unix.so` module in the `password` section to include the option `sha512` and no other hashing algorithms as shown below:
    password    sufficient    pam_unix.so sha512
                        other arguments...
This will help ensure that new passwords for local users will be stored using the sha512 algorithm. Warning: The hashing algorithms to be used with pam_unix.so are defined with independent module options. There are at least 7 possible algorithms and likely more algorithms will be introduced along the time. Due the the number of options and its possible combinations, the use of multiple hashing algorithm options may bring unexpected behaviors to the system. For this reason the check will pass only when one hashing algorithm option is defined and is aligned to the "var_password_hashing_algorithm_pam" variable. The remediation will ensure the correct option and remove any other extra hashing algorithm option. Warning: The CIS Red Hat Enterprise Linux 9 Benchmark permits both "sha512" and "yescrypt" algorithms for password hashing. This profile is configured to check for "sha512", but a system using either algorithm is compliant with the CIS Benchmark requirements. To use a different algorithm, create a tailored profile and select your preferred value for the `var_password_hashing_algorithm_pam` variable.
Rationale: | Passwords need to be protected at all times, and encryption is the standard method for protecting passwords. If passwords are not encrypted, they can be plainly read (i.e., clear text) and easily compromised. Passwords that are encrypted with a weak algorithm are no more protected than if they are kept in plain text. This setting ensures user and group account administration utilities are configured to store only encrypted representations of passwords. Additionally, the `crypt_style` configuration option in `/etc/libuser.conf` ensures the use of a strong hashing algorithm that makes password cracking attacks more difficult.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_set_password_hashing_algorithm_systemauth
References: |
cis-csc | 1, 12, 15, 16, 5
cjis | 5.6.2.2
cobit5 | DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
cui | 3.13.11
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1
iso27001-2013 | A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3
nist | IA-5(c), IA-5(1)(c), CM-6(a)
nist-csf | PR.AC-1, PR.AC-6, PR.AC-7
pcidss | Req-8.2.1
os-srg | SRG-OS-000073-GPOS-00041, SRG-OS-000120-GPOS-00061
anssi | R68
ism | 0418, 1055, 1402
pcidss4 | 8.3.2, 8.3
cis | 5.3.3.4.3
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q pam; }; then

    var_password_hashing_algorithm_pam='sha512'

    # Allow multiple algorithms, but choose the first one for remediation
    var_password_hashing_algorithm_pam="$(echo $var_password_hashing_algorithm_pam | cut -d \| -f 1)"

    PAM_FILE_PATH="/etc/pam.d/system-auth"

    # Ensure all the hashing algorithm option is removed.
    declare -a HASHING_ALGORITHMS_OPTIONS=("sha512" "yescrypt" "gost_yescrypt" "blowfish" "sha256" "md5" "bigcrypt")

    for hash_option in "${HASHING_ALGORITHMS_OPTIONS[@]}"; do
      if grep -qP "^\s*password\s+.*\s+pam_unix.so\s+.*\b$hash_option\b" "$PAM_FILE_PATH"; then
        if [ -e "$PAM_FILE_PATH" ] ; then
        PAM_FILE_PATH="$PAM_FILE_PATH"
        if [ -f /usr/bin/authselect ]; then

            if ! authselect check; then
            echo "
            authselect integrity check failed. Remediation aborted!
            This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
            It is not recommended to manually edit the PAM files when authselect tool is available.
            In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
            exit 1
            fi

            CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
            # If not already in use, a custom profile is created preserving the enabled features.
            if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                # The "local" profile does not contain essential security features required by multiple Benchmarks.
                # If currently used, it is replaced by "sssd", which is the best option in this case.
                if [[ $CURRENT_PROFILE == local ]]; then
                    CURRENT_PROFILE="sssd"
                fi
                authselect create-profile hardening -b $CURRENT_PROFILE
                CURRENT_PROFILE="custom/hardening"

                authselect apply-changes -b --backup=before-hardening-custom-profile
                authselect select $CURRENT_PROFILE
                for feature in $ENABLED_FEATURES; do
                    authselect enable-feature $feature;
                done

                authselect apply-changes -b --backup=after-hardening-custom-profile
            fi
            PAM_FILE_NAME=$(basename "$PAM_FILE_PATH")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b
        fi

    if grep -qP "^\s*password\s+.*\s+pam_unix.so\s.*\b$hash_option\b" "$PAM_FILE_PATH"; then
        sed -i -E --follow-symlinks "s/(.*password.*.*.*pam_unix.so.*)\b$hash_option\b=?[[:alnum:]]*(.*)/\1\2/g" "$PAM_FILE_PATH"
    fi
        if [ -f /usr/bin/authselect ]; then

            authselect apply-changes -b
        fi
    else
        echo "$PAM_FILE_PATH was not found" >&2
    fi
      fi
    done

    if [ -e "$PAM_FILE_PATH" ] ; then
        PAM_FILE_PATH="$PAM_FILE_PATH"
        if [ -f /usr/bin/authselect ]; then

            if ! authselect check; then
            echo "
            authselect integrity check failed. Remediation aborted!
            This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
            It is not recommended to manually edit the PAM files when authselect tool is available.
            In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
            exit 1
            fi

            CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
            # If not already in use, a custom profile is created preserving the enabled features.
            if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                # The "local" profile does not contain essential security features required by multiple Benchmarks.
                # If currently used, it is replaced by "sssd", which is the best option in this case.
                if [[ $CURRENT_PROFILE == local ]]; then
                    CURRENT_PROFILE="sssd"
                fi
                authselect create-profile hardening -b $CURRENT_PROFILE
                CURRENT_PROFILE="custom/hardening"

                authselect apply-changes -b --backup=before-hardening-custom-profile
                authselect select $CURRENT_PROFILE
                for feature in $ENABLED_FEATURES; do
                    authselect enable-feature $feature;
                done

                authselect apply-changes -b --backup=after-hardening-custom-profile
            fi
            PAM_FILE_NAME=$(basename "$PAM_FILE_PATH")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b
        fi

            if ! grep -qP "^\s*password\s+sufficient\s+pam_unix.so\s*.*" "$PAM_FILE_PATH"; then
                # Line matching group + control + module was not found. Check group + module.
                if [ "$(grep -cP '^\s*password\s+.*\s+pam_unix.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then
                    # The control is updated only if one single line matches.
                    sed -i -E --follow-symlinks "s/^(\s*password\s+).*(\bpam_unix.so.*)/\1sufficient \2/" "$PAM_FILE_PATH"
                else
                    echo "password    sufficient    pam_unix.so" >> "$PAM_FILE_PATH"
                fi
            fi
            # Check the option
            if ! grep -qP "^\s*password\s+sufficient\s+pam_unix.so\s*.*\s$var_password_hashing_algorithm_pam\b" "$PAM_FILE_PATH"; then
                sed -i -E --follow-symlinks "/\s*password\s+sufficient\s+pam_unix.so.*/ s/$/ $var_password_hashing_algorithm_pam/" "$PAM_FILE_PATH"
            fi
        if [ -f /usr/bin/authselect ]; then

            authselect apply-changes -b
        fi
    else
        echo "$PAM_FILE_PATH was not found" >&2
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.2
      - NIST-800-171-3.13.11
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(c)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.1
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - set_password_hashing_algorithm_systemauth
    - name: XCCDF Value var_password_hashing_algorithm_pam # promote to variable
      set_fact:
        var_password_hashing_algorithm_pam: !!str sha512
      tags:
        - always

    - name: Set PAM Password Hashing Algorithm - system-auth - Check if /etc/pam.d/system-auth
        file is present
      ansible.builtin.stat:
        path: /etc/pam.d/system-auth
      register: result_pam_auth_file_present
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.2
      - NIST-800-171-3.13.11
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(c)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.1
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - set_password_hashing_algorithm_systemauth

    - name: Set PAM Password Hashing Algorithm - system-auth - Check the proper remediation
        for the system
      block:

      - name: Set PAM Password Hashing Algorithm - system-auth - Define the PAM file to
          be edited as a local fact
        ansible.builtin.set_fact:
          pam_file_path: /etc/pam.d/system-auth

      - name: Set PAM Password Hashing Algorithm - system-auth - Check if system relies
          on authselect tool
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

      - name: Set PAM Password Hashing Algorithm - system-auth - Ensure authselect custom
          profile is used if authselect is present
        block:

        - name: Set PAM Password Hashing Algorithm - system-auth - Check integrity of
            authselect current profile
          ansible.builtin.command:
            cmd: authselect check
          register: result_authselect_check_cmd
          changed_when: false
          check_mode: false
          failed_when: false

        - name: Set PAM Password Hashing Algorithm - system-auth - Informative message
            based on the authselect integrity check result
          ansible.builtin.assert:
            that:
            - ansible_check_mode or result_authselect_check_cmd.rc == 0
            fail_msg:
            - authselect integrity check failed. Remediation aborted!
            - This remediation could not be applied because an authselect profile was
              not selected or the selected profile is not intact.
            - It is not recommended to manually edit the PAM files when authselect tool
              is available.
            - In cases where the default authselect profile does not cover a specific
              demand, a custom authselect profile is recommended.
            success_msg:
            - authselect integrity check passed

        - name: Set PAM Password Hashing Algorithm - system-auth - Get authselect current
            profile
          ansible.builtin.shell:
            cmd: authselect current -r | awk '{ print $1 }'
          register: result_authselect_profile
          changed_when: false
          when:
          - result_authselect_check_cmd is success

        - name: Set PAM Password Hashing Algorithm - system-auth - Define the current
            authselect profile as a local fact
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is match("custom/")

        - name: Set PAM Password Hashing Algorithm - system-auth - Define the new authselect
            custom profile as a local fact
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: custom/hardening
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is not match("custom/")

        - name: Set PAM Password Hashing Algorithm - 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 }'
          register: result_authselect_features
          changed_when: false
          check_mode: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: Set PAM Password Hashing Algorithm - system-auth - Check if any custom
            profile with the same name was already created
          ansible.builtin.stat:
            path: /etc/authselect/{{ authselect_custom_profile }}
          register: result_authselect_custom_profile_present
          changed_when: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: Set PAM Password Hashing Algorithm - system-auth - Create an authselect
            custom profile based on the current profile
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b {{ authselect_current_profile
              }}
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is not match("^(custom/|local)")
          - not result_authselect_custom_profile_present.stat.exists

        - name: Set PAM Password Hashing Algorithm - system-auth - Create an authselect
            custom profile based on sssd profile
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b sssd
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is match("local")
          - not result_authselect_custom_profile_present.stat.exists

        - name: Set PAM Password Hashing Algorithm - system-auth - Ensure authselect changes
            are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: Set PAM Password Hashing Algorithm - system-auth - Ensure the authselect
            custom profile is selected
          ansible.builtin.command:
            cmd: authselect select {{ authselect_custom_profile }}
          register: result_pam_authselect_select_profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: Set PAM Password Hashing Algorithm - system-auth - Restore the authselect
            features in the custom profile
          ansible.builtin.command:
            cmd: authselect enable-feature {{ item }}
          loop: '{{ result_authselect_features.stdout_lines }}'
          register: result_pam_authselect_restore_features
          when:
          - result_authselect_profile is not skipped
          - result_authselect_features is not skipped
          - result_pam_authselect_select_profile is not skipped

        - name: Set PAM Password Hashing Algorithm - system-auth - Ensure authselect changes
            are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - result_pam_authselect_restore_features is not skipped

        - name: Set PAM Password Hashing Algorithm - system-auth - Change the PAM file
            to be edited according to the custom authselect profile
          ansible.builtin.set_fact:
            pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
              | basename }}
          when:
          - authselect_custom_profile is defined
        when:
        - result_authselect_present.stat.exists

      - name: Set PAM Password Hashing Algorithm - system-auth - Define a fact for control
          already filtered in case filters are used
        ansible.builtin.set_fact:
          pam_module_control: sufficient

      - name: Set PAM Password Hashing Algorithm - system-auth - Check if expected PAM
          module line is present in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so\s*.*
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_line_present

      - name: Set PAM Password Hashing Algorithm - system-auth - Include or update the
          PAM module line in {{ pam_file_path }}
        block:

        - name: Set PAM Password Hashing Algorithm - system-auth - Check if required PAM
            module line is present in {{ pam_file_path }} with different control
          ansible.builtin.lineinfile:
            path: '{{ pam_file_path }}'
            regexp: ^\s*password\s+.*\s+pam_unix.so\s*
            state: absent
          check_mode: true
          changed_when: false
          register: result_pam_line_other_control_present

        - name: Set PAM Password Hashing Algorithm - system-auth - Ensure the correct
            control for the required PAM module line in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: ^(\s*password\s+).*(\bpam_unix.so.*)
            replace: \1{{ pam_module_control }} \2
          register: result_pam_module_edit
          when:
          - result_pam_line_other_control_present.found == 1

        - name: Set PAM Password Hashing Algorithm - system-auth - Ensure the required
            PAM module line is included in {{ pam_file_path }}
          ansible.builtin.lineinfile:
            dest: '{{ pam_file_path }}'
            line: password    {{ pam_module_control }}    pam_unix.so
          register: result_pam_module_add
          when:
          - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found
            > 1

        - name: Set PAM Password Hashing Algorithm - system-auth - Ensure authselect changes
            are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b
          when:
          - result_authselect_present is defined
          - result_authselect_present.stat.exists
          - |-
            (result_pam_module_add is defined and result_pam_module_add.changed)
             or (result_pam_module_edit is defined and result_pam_module_edit.changed)
        when:
        - result_pam_line_present.found is defined
        - result_pam_line_present.found == 0

      - name: Set PAM Password Hashing Algorithm - system-auth - Define a fact for control
          already filtered in case filters are used
        ansible.builtin.set_fact:
          pam_module_control: sufficient

      - name: Set PAM Password Hashing Algorithm - system-auth - Check if the required
          PAM module option is present in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so\s*.*\s{{
            var_password_hashing_algorithm_pam.split("|")[0] }}\b
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_module_set_password_hashing_algorithm_systemauth_option_present

      - name: Set PAM Password Hashing Algorithm - system-auth - Ensure the "{{ var_password_hashing_algorithm_pam.split("|")[0]
          }}" PAM option for "pam_unix.so" is included in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          backrefs: true
          regexp: ^(\s*password\s+{{ pam_module_control | regex_escape() }}\s+pam_unix.so.*)
          line: \1 {{ var_password_hashing_algorithm_pam.split("|")[0] }}
          state: present
        register: result_pam_set_password_hashing_algorithm_systemauth_add
        when:
        - result_pam_module_set_password_hashing_algorithm_systemauth_option_present.found
          is defined
        - result_pam_module_set_password_hashing_algorithm_systemauth_option_present.found
          == 0

      - name: Set PAM Password Hashing Algorithm - system-auth - Ensure authselect changes
          are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_present.stat.exists
        - |-
          (result_pam_set_password_hashing_algorithm_systemauth_add is defined and result_pam_set_password_hashing_algorithm_systemauth_add.changed)
           or (result_pam_set_password_hashing_algorithm_systemauth_edit is defined and result_pam_set_password_hashing_algorithm_systemauth_edit.changed)
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      - result_pam_auth_file_present.stat.exists
      tags:
      - CJIS-5.6.2.2
      - NIST-800-171-3.13.11
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(c)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.1
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - set_password_hashing_algorithm_systemauth

    - name: Set PAM Password Hashing Algorithm - system-auth - Check if /etc/pam.d/system-auth
        File is Present
      ansible.builtin.stat:
        path: /etc/pam.d/system-auth
      register: result_pam_auth_file_present
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.2
      - NIST-800-171-3.13.11
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(c)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.1
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - set_password_hashing_algorithm_systemauth

    - name: Set PAM Password Hashing Algorithm - system-auth - Check The Proper Remediation
        For The System
      block:

      - name: Set PAM Password Hashing Algorithm - system-auth - Define the PAM file to
          be edited as a local fact
        ansible.builtin.set_fact:
          pam_file_path: /etc/pam.d/system-auth

      - name: Set PAM Password Hashing Algorithm - system-auth - Check if system relies
          on authselect tool
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

      - name: Set PAM Password Hashing Algorithm - system-auth - Ensure authselect custom
          profile is used if authselect is present
        block:

        - name: Set PAM Password Hashing Algorithm - system-auth - Check integrity of
            authselect current profile
          ansible.builtin.command:
            cmd: authselect check
          register: result_authselect_check_cmd
          changed_when: false
          check_mode: false
          failed_when: false

        - name: Set PAM Password Hashing Algorithm - system-auth - Informative message
            based on the authselect integrity check result
          ansible.builtin.assert:
            that:
            - ansible_check_mode or result_authselect_check_cmd.rc == 0
            fail_msg:
            - authselect integrity check failed. Remediation aborted!
            - This remediation could not be applied because an authselect profile was
              not selected or the selected profile is not intact.
            - It is not recommended to manually edit the PAM files when authselect tool
              is available.
            - In cases where the default authselect profile does not cover a specific
              demand, a custom authselect profile is recommended.
            success_msg:
            - authselect integrity check passed

        - name: Set PAM Password Hashing Algorithm - system-auth - Get authselect current
            profile
          ansible.builtin.shell:
            cmd: authselect current -r | awk '{ print $1 }'
          register: result_authselect_profile
          changed_when: false
          when:
          - result_authselect_check_cmd is success

        - name: Set PAM Password Hashing Algorithm - system-auth - Define the current
            authselect profile as a local fact
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is match("custom/")

        - name: Set PAM Password Hashing Algorithm - system-auth - Define the new authselect
            custom profile as a local fact
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: custom/hardening
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is not match("custom/")

        - name: Set PAM Password Hashing Algorithm - 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 }'
          register: result_authselect_features
          changed_when: false
          check_mode: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: Set PAM Password Hashing Algorithm - system-auth - Check if any custom
            profile with the same name was already created
          ansible.builtin.stat:
            path: /etc/authselect/{{ authselect_custom_profile }}
          register: result_authselect_custom_profile_present
          changed_when: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: Set PAM Password Hashing Algorithm - system-auth - Create an authselect
            custom profile based on the current profile
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b {{ authselect_current_profile
              }}
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is not match("^(custom/|local)")
          - not result_authselect_custom_profile_present.stat.exists

        - name: Set PAM Password Hashing Algorithm - system-auth - Create an authselect
            custom profile based on sssd profile
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b sssd
          when:
          - result_authselect_profile is not skipped
          - result_authselect_check_cmd is success
          - authselect_current_profile is match("local")
          - not result_authselect_custom_profile_present.stat.exists

        - name: Set PAM Password Hashing Algorithm - system-auth - Ensure authselect changes
            are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: Set PAM Password Hashing Algorithm - system-auth - Ensure the authselect
            custom profile is selected
          ansible.builtin.command:
            cmd: authselect select {{ authselect_custom_profile }}
          register: result_pam_authselect_select_profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: Set PAM Password Hashing Algorithm - system-auth - Restore the authselect
            features in the custom profile
          ansible.builtin.command:
            cmd: authselect enable-feature {{ item }}
          loop: '{{ result_authselect_features.stdout_lines }}'
          register: result_pam_authselect_restore_features
          when:
          - result_authselect_profile is not skipped
          - result_authselect_features is not skipped
          - result_pam_authselect_select_profile is not skipped

        - name: Set PAM Password Hashing Algorithm - system-auth - Ensure authselect changes
            are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - result_pam_authselect_restore_features is not skipped

        - name: Set PAM Password Hashing Algorithm - system-auth - Change the PAM file
            to be edited according to the custom authselect profile
          ansible.builtin.set_fact:
            pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
              | basename }}
          when:
          - authselect_custom_profile is defined
        when:
        - result_authselect_present.stat.exists

      - name: Set PAM Password Hashing Algorithm - system-auth - Check if "{{ pam_file_path
          }}" File is Present
        ansible.builtin.stat:
          path: '{{ pam_file_path }}'
        register: pam_file_path_present

      - name: Set PAM Password Hashing Algorithm - system-auth - Ensure That Only the
          Correct Hashing Algorithm Option For pam_unix.so Is Used in {{ pam_file_path
          }}
        ansible.builtin.replace:
          dest: '{{ pam_file_path }}'
          regexp: (^\s*password.*pam_unix\.so.*)\b{{ item }}\b\s*(.*)
          replace: \1\2
        when:
        - item != var_password_hashing_algorithm_pam.split('|')[0]
        - pam_file_path_present.stat.exists
        loop:
        - sha512
        - yescrypt
        - gost_yescrypt
        - blowfish
        - sha256
        - md5
        - bigcrypt
        register: result_pam_hashing_options_removal

      - name: Set PAM Password Hashing Algorithm - system-auth - Ensure authselect changes
          are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_present.stat.exists
        - result_pam_hashing_options_removal is changed
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"pam" in ansible_facts.packages'
      - result_pam_auth_file_present.stat.exists
      tags:
      - CJIS-5.6.2.2
      - NIST-800-171-3.13.11
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(c)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.1
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - set_password_hashing_algorithm_systemauth
Group Protect Accounts by Restricting Password-Based Login Group contains 1 group and 7 rules
[ref] Conventionally, Unix shell accounts are accessed by providing a username and password to a login program, which tests these values for correctness using the `/etc/passwd` and `/etc/shadow` files. Password-based login is vulnerable to guessing of weak passwords, and to sniffing and man-in-the-middle attacks against passwords entered over a network or at an insecure console. Therefore, mechanisms for accessing accounts by entering usernames and passwords should be restricted to those which are operationally necessary.
Group Set Password Expiration Parameters Group contains 7 rules
[ref] The file `/etc/login.defs` controls several password-related settings. Programs such as `passwd`, `su`, and `login` consult `/etc/login.defs` to determine behavior with regard to password aging, expiration warnings, and length. See the man page `login.defs(5)` for more information. Users should be forced to change their passwords, in order to decrease the utility of compromised passwords. However, the need to change passwords often should be balanced against the risk that users will reuse or write down passwords if forced to change them too often. Forcing password changes every 90-360 days, depending on the environment, is recommended. Set the appropriate value as `PASS_MAX_DAYS` and apply it to existing accounts with the `-M` flag. The `PASS_MIN_DAYS` (`-m`) setting prevents password changes for 7 days after the first change, to discourage password cycling. If you use this setting, train users to contact an administrator for an emergency password change in case a new password becomes compromised. The `PASS_WARN_AGE` (`-W`) setting gives users 7 days of warnings at login time that their passwords are about to expire. For example, for each existing human user USER, expiration parameters could be adjusted to a 180 day maximum password age, 7 day minimum password age, and 7 day warning period with the following command:
    $ sudo chage -M 180 -m 7 -W 7 USER
#### Rule Set Password Maximum Age [ref]
To specify password maximum age for new accounts, edit the file `/etc/login.defs` and add or correct the following line:
    PASS_MAX_DAYS 45
The profile requirement is `45`.
Rationale: | Any password, no matter how complex, can eventually be cracked. Therefore, passwords need to be changed periodically. If the operating system does not limit the lifetime of passwords and force users to change their passwords, there is the risk that the operating system passwords could be compromised. Setting the password maximum age ensures users are required to periodically change their passwords. Requiring shorter password lifetimes increases the risk of users writing down the password in a convenient location subject to physical compromise.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_maximum_age_login_defs
References: |
cis-csc | 1, 12, 15, 16, 5
cjis | 5.6.2.1
cobit5 | DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
cui | 3.5.6
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1
iso27001-2013 | A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3
nist | IA-5(f), IA-5(1)(d), CM-6(a)
nist-csf | PR.AC-1, PR.AC-6, PR.AC-7
pcidss | Req-8.2.4
os-srg | SRG-OS-000076-GPOS-00044
ism | 0418, 1055, 1402
pcidss4 | 8.3.9, 8.3
cis | 5.4.1.1
stigref | SV-258041r1038967_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q shadow-utils; }; then

    var_accounts_maximum_age_login_defs='45'

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^PASS_MAX_DAYS")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s %s" "$stripped_key" "$var_accounts_maximum_age_login_defs"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^PASS_MAX_DAYS\\>" "/etc/login.defs"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^PASS_MAX_DAYS\\>.*/$escaped_formatted_output/gi" "/etc/login.defs"
    else
        if [[ -s "/etc/login.defs" ]] && [[ -n "$(tail -c 1 -- "/etc/login.defs" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/login.defs"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/login.defs" >> "/etc/login.defs"
        printf '%s\n' "$formatted_output" >> "/etc/login.defs"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.1
      - DISA-STIG-RHEL-09-411010
      - NIST-800-171-3.5.6
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.4
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.9
      - accounts_maximum_age_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_maximum_age_login_defs # promote to variable
      set_fact:
        var_accounts_maximum_age_login_defs: !!str 45
      tags:
        - always

    - name: Set Password Maximum Age
      ansible.builtin.lineinfile:
        create: true
        dest: /etc/login.defs
        regexp: ^#?PASS_MAX_DAYS
        line: PASS_MAX_DAYS {{ var_accounts_maximum_age_login_defs }}
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"shadow-utils" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1
      - DISA-STIG-RHEL-09-411010
      - NIST-800-171-3.5.6
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.4
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.9
      - accounts_maximum_age_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Set Password Minimum Age [ref]
To specify password minimum age for new accounts, edit the file `/etc/login.defs` and add or correct the following line:
    PASS_MIN_DAYS 2
A value of 1 day is considered sufficient for many environments. The profile requirement is `2`.
Rationale: | Enforcing a minimum password lifetime helps to prevent repeated password changes to defeat the password reuse or history enforcement requirement. If users are allowed to immediately and continually change their password, then the password could be repeatedly changed in a short period of time to defeat the organization's policy regarding password reuse. Setting the minimum password age protects against users cycling back to a favorite password after satisfying the password reuse requirement.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_minimum_age_login_defs
References: |
cis-csc | 1, 12, 15, 16, 5
cjis | 5.6.2.1.1
cobit5 | DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
cui | 3.5.8
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1
iso27001-2013 | A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3
nist | IA-5(f), IA-5(1)(d), CM-6(a)
nist-csf | PR.AC-1, PR.AC-6, PR.AC-7
os-srg | SRG-OS-000075-GPOS-00043
ism | 0418, 1055, 1402
cis | 5.4.1.2
stigref | SV-258104r1015104_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q shadow-utils; }; then

    var_accounts_minimum_age_login_defs='2'

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^PASS_MIN_DAYS")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s %s" "$stripped_key" "$var_accounts_minimum_age_login_defs"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^PASS_MIN_DAYS\\>" "/etc/login.defs"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^PASS_MIN_DAYS\\>.*/$escaped_formatted_output/gi" "/etc/login.defs"
    else
        if [[ -s "/etc/login.defs" ]] && [[ -n "$(tail -c 1 -- "/etc/login.defs" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/login.defs"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/login.defs" >> "/etc/login.defs"
        printf '%s\n' "$formatted_output" >> "/etc/login.defs"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.1.1
      - DISA-STIG-RHEL-09-611075
      - NIST-800-171-3.5.8
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - accounts_minimum_age_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_minimum_age_login_defs # promote to variable
      set_fact:
        var_accounts_minimum_age_login_defs: !!str 2
      tags:
        - always

    - name: Set Password Minimum Age
      ansible.builtin.lineinfile:
        create: true
        dest: /etc/login.defs
        regexp: ^#?PASS_MIN_DAYS
        line: PASS_MIN_DAYS {{ var_accounts_minimum_age_login_defs }}
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"shadow-utils" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1.1
      - DISA-STIG-RHEL-09-611075
      - NIST-800-171-3.5.8
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - accounts_minimum_age_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Set Password Minimum Length in login.defs [ref]
To specify password length requirements for new accounts, edit the file `/etc/login.defs` and add or correct the following line:
    PASS_MIN_LEN 12
The profile requirement is `12`. If a program consults `/etc/login.defs` and also another PAM module (such as `pam_pwquality`) during a password change operation, then the most restrictive must be satisfied. See PAM section for more information about enforcing password quality requirements.
Rationale: | Requiring a minimum password length makes password cracking attacks more difficult by ensuring a larger search space. However, any security benefit from an onerous requirement must be carefully weighed against usability problems, support costs, or counterproductive behavior that may result.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_minlen_login_defs
References: |
cis-csc | 1, 12, 15, 16, 5
cjis | 5.6.2.1
cobit5 | DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
cui | 3.5.7
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1
iso27001-2013 | A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3
nist | IA-5(f), IA-5(1)(a), CM-6(a)
nist-csf | PR.AC-1, PR.AC-6, PR.AC-7
os-srg | SRG-OS-000078-GPOS-00046
anssi | R31
ism | 0421, 0422, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q shadow-utils; }; then

    var_accounts_password_minlen_login_defs='12'

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^PASS_MIN_LEN")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s %s" "$stripped_key" "$var_accounts_password_minlen_login_defs"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^PASS_MIN_LEN\\>" "/etc/login.defs"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^PASS_MIN_LEN\\>.*/$escaped_formatted_output/gi" "/etc/login.defs"
    else
        if [[ -s "/etc/login.defs" ]] && [[ -n "$(tail -c 1 -- "/etc/login.defs" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/login.defs"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/login.defs" >> "/etc/login.defs"
        printf '%s\n' "$formatted_output" >> "/etc/login.defs"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.1
      - NIST-800-171-3.5.7
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(f)
      - accounts_password_minlen_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_password_minlen_login_defs # promote to variable
      set_fact:
        var_accounts_password_minlen_login_defs: !!str 12
      tags:
        - always

    - name: Set Password Minimum Length in login.defs
      ansible.builtin.lineinfile:
        dest: /etc/login.defs
        regexp: ^PASS_MIN_LEN *[0-9]*
        state: present
        line: PASS_MIN_LEN        {{ var_accounts_password_minlen_login_defs }}
        create: true
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"shadow-utils" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1
      - NIST-800-171-3.5.7
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(f)
      - accounts_password_minlen_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Set Existing Passwords Maximum Age [ref]
Configure non-compliant accounts to enforce a 45-day maximum password lifetime restriction by running the following command:
    $ sudo chage -M 45
                        USER
Rationale: | Any password, no matter how complex, can eventually be cracked. Therefore, passwords need to be changed periodically. If the operating system does not limit the lifetime of passwords and force users to change their passwords, there is the risk that the operating system passwords could be compromised.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_set_max_life_existing
References: |
nist | IA-5(f), IA-5(1)(d), CM-6(a)
os-srg | SRG-OS-000076-GPOS-00044
pcidss4 | 8.3.9, 8.3
cis | 5.4.1.1
stigref | SV-258042r1045133_rule
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    var_accounts_maximum_age_login_defs='45'

    while IFS= read -r i; do

        chage -M $var_accounts_maximum_age_login_defs $i

    done <   <(awk -v var="$var_accounts_maximum_age_login_defs" -F: '(/^[^:]+:[^!*]/ && ($5 > var || $5 == "")) {print $1}' /etc/shadow)

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-411015
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.9
      - accounts_password_set_max_life_existing
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_maximum_age_login_defs # promote to variable
      set_fact:
        var_accounts_maximum_age_login_defs: !!str 45
      tags:
        - always

    - name: Collect users with not correct maximum time period between password changes
      ansible.builtin.command:
        cmd: awk -F':' '(/^[^:]+:[^!*]/ && ($5 > {{ var_accounts_maximum_age_login_defs
          }} || $5 == "")) {print $1}' /etc/shadow
      register: user_names
      changed_when: false
      check_mode: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-411015
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.9
      - accounts_password_set_max_life_existing
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Change the maximum time period between password changes
      ansible.builtin.user:
        user: '{{ item }}'
        password_expire_max: '{{ var_accounts_maximum_age_login_defs }}'
      with_items: '{{ user_names.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - user_names.stdout_lines | length > 0
      tags:
      - DISA-STIG-RHEL-09-411015
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.9
      - accounts_password_set_max_life_existing
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Set Existing Passwords Minimum Age [ref]
Configure non-compliant accounts to enforce a 24 hours/1 day minimum password lifetime by running the following command:
    $ sudo chage -m 1 USER
Rationale: | Enforcing a minimum password lifetime helps to prevent repeated password changes to defeat the password reuse or history enforcement requirement. If users are allowed to immediately and continually change their password, the password could be repeatedly changed in a short period of time to defeat the organization's policy regarding password reuse.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_set_min_life_existing
References: |
nist | IA-5(f), IA-5(1)(d), CM-6(a)
os-srg | SRG-OS-000075-GPOS-00043
cis | 5.4.1.2
stigref | SV-258105r1045212_rule
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    var_accounts_minimum_age_login_defs='2'

    while IFS= read -r i; do

        chage -m $var_accounts_minimum_age_login_defs $i

    done <   <(awk -v var="$var_accounts_minimum_age_login_defs" -F: '(/^[^:]+:[^!*]/ && ($4 < var || $4 == "")) {print $1}' /etc/shadow)

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-611080
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - accounts_password_set_min_life_existing
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_minimum_age_login_defs # promote to variable
      set_fact:
        var_accounts_minimum_age_login_defs: !!str 2
      tags:
        - always

    - name: Collect users with not correct minimum time period between password changes
      ansible.builtin.command: |
        awk -F':' '(/^[^:]+:[^!*]/ && ($4 < {{ var_accounts_minimum_age_login_defs }} || $4 == "")) {print $1}' /etc/shadow
      register: user_names
      changed_when: false
      check_mode: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-611080
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - accounts_password_set_min_life_existing
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Change the minimum time period between password changes
      ansible.builtin.command: |
        chage -m {{ var_accounts_minimum_age_login_defs }} {{ item }}
      with_items: '{{ user_names.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - user_names.stdout_lines | length > 0
      tags:
      - DISA-STIG-RHEL-09-611080
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - accounts_password_set_min_life_existing
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Set Existing Passwords Warning Age [ref]
To configure how many days prior to password expiration that a warning will be issued to users, run the command:
    $ sudo chage --warndays 10
                        USER
This profile requirement is `10`.
Rationale: | Providing an advance warning that a password will be expiring gives users time to think of a secure password. Users caught unaware may choose a simple password or write it down where it may be discovered.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_set_warn_age_existing
References: |
nist | IA-5(f), IA-5(1)(d), CM-6(a)
pcidss4 | 8.3.9, 8.3
cis | 5.4.1.3
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    var_accounts_password_warn_age_login_defs='10'

    while IFS= read -r i; do
        chage --warndays $var_accounts_password_warn_age_login_defs $i
    done <   <(awk -v var="$var_accounts_password_warn_age_login_defs" -F: '(($6 < var || $6 == "") && $2 ~ /^\$/) {print $1}' /etc/shadow)

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.9
      - accounts_password_set_warn_age_existing
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
    - name: XCCDF Value var_accounts_password_warn_age_login_defs # promote to variable
      set_fact:
        var_accounts_password_warn_age_login_defs: !!str 10
      tags:
        - always

    - name: Set Existing Passwords Warning Age - Collect Users With Incorrect Number of
        Days of Warning Before Password Expires
      ansible.builtin.command:
        cmd: awk -F':' '(($6 < {{ var_accounts_password_warn_age_login_defs }} || $6 ==
          "") && $2 ~ /^\$/) {print $1}' /etc/shadow
      register: result_pass_warn_age_user_names
      changed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.9
      - accounts_password_set_warn_age_existing
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Set Existing Passwords Warning Age - Ensure the Number of Days of Warning
        Before Password Expires
      ansible.builtin.command:
        cmd: chage --warndays {{ var_accounts_password_warn_age_login_defs }} {{ item
          }}
      with_items: '{{ result_pass_warn_age_user_names.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - result_pass_warn_age_user_names is not skipped and result_pass_warn_age_user_names.stdout_lines
        | length > 0
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.9
      - accounts_password_set_warn_age_existing
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
#### Rule Set Password Warning Age [ref]
To specify how many days prior to password expiration that a warning will be issued to users, edit the file `/etc/login.defs` and add or correct the following line:
    PASS_WARN_AGE 10
The profile requirement is `10`.
Rationale: | Setting the password warning age enables users to make the change at a practical time.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_warn_age_login_defs
References: |
cis-csc | 1, 12, 13, 14, 15, 16, 18, 3, 5, 7, 8
cobit5 | DSS01.03, DSS03.05, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
cui | 3.5.8
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 6.2
iso27001-2013 | A.12.4.1, A.12.4.3, A.18.1.4, A.6.1.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5
nist | IA-5(f), IA-5(1)(d), CM-6(a)
nist-csf | DE.CM-1, DE.CM-3, PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7
pcidss | Req-8.2.4
ism | 0418, 1055, 1402
pcidss4 | 8.3.9, 8.3
cis | 5.4.1.3
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q shadow-utils; }; then

    var_accounts_password_warn_age_login_defs='10'

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^PASS_WARN_AGE")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s %s" "$stripped_key" "$var_accounts_password_warn_age_login_defs"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^PASS_WARN_AGE\\>" "/etc/login.defs"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^PASS_WARN_AGE\\>.*/$escaped_formatted_output/gi" "/etc/login.defs"
    else
        if [[ -s "/etc/login.defs" ]] && [[ -n "$(tail -c 1 -- "/etc/login.defs" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/login.defs"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/login.defs" >> "/etc/login.defs"
        printf '%s\n' "$formatted_output" >> "/etc/login.defs"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-171-3.5.8
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.4
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.9
      - accounts_password_warn_age_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_password_warn_age_login_defs # promote to variable
      set_fact:
        var_accounts_password_warn_age_login_defs: !!str 10
      tags:
        - always

    - name: Set Password Warning Age
      ansible.builtin.lineinfile:
        dest: /etc/login.defs
        regexp: ^PASS_WARN_AGE *[0-9]*
        state: present
        line: PASS_WARN_AGE        {{ var_accounts_password_warn_age_login_defs }}
        create: true
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"shadow-utils" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.5.8
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.4
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.9
      - accounts_password_warn_age_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Enable authselect [ref]
Configure user authentication setup to use the `authselect` tool. If authselect profile is selected, the rule will enable the sssd profile. Warning: If the `sudo authselect select` command returns an error informing that the chosen profile cannot be selected, it is probably because PAM files have already been modified by the administrator. If this is the case, in order to not overwrite the desired changes made by the administrator, the current PAM settings should be investigated before forcing the selection of the chosen authselect profile.
Rationale: | Authselect is a successor to authconfig. It is a tool to select system authentication and identity sources from a list of supported profiles instead of letting the administrator manually build the PAM stack. That way, it avoids potential breakage of configuration, as it ships several tested profiles that are well tested and supported to solve different use-cases.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_enable_authselect
References: |
hipaa | 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii)
nist | AC-3
ospp | FIA_UAU.1, FIA_AFL.1
os-srg | SRG-OS-000480-GPOS-00227
anssi | R31
ism | 1409
pcidss4 | 8.3.4, 8.3
ccn | enable_authselect
cis | enable_authselect
stigid | needed_rules
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    var_authselect_profile='sssd'

    authselect current

    if test "$?" -ne 0; then
        if { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; }; then
            authselect select --force "$var_authselect_profile"
        else
            authselect select "$var_authselect_profile"
        fi

        if test "$?" -ne 0; then
            if rpm --quiet --verify pam; then
                authselect select --force "$var_authselect_profile"
            else
    	        echo "authselect is not used but files from the 'pam' package have been altered, so the authselect configuration won't be forced." >&2
            fi
        fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-needed_rules
      - NIST-800-53-AC-3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.4
      - configure_strategy
      - enable_authselect
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
    - name: XCCDF Value var_authselect_profile # promote to variable
      set_fact:
        var_authselect_profile: !!str sssd
      tags:
        - always

    - name: Enable authselect - Check Current authselect Profile
      ansible.builtin.command:
        cmd: authselect current
      register: result_authselect_current
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-needed_rules
      - NIST-800-53-AC-3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.4
      - configure_strategy
      - enable_authselect
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Enable authselect - Try to Select an authselect Profile
      ansible.builtin.command:
        cmd: authselect select "{{ var_authselect_profile }}"
      register: result_authselect_select
      changed_when: result_authselect_select.rc == 0
      failed_when: false
      when:
      - '"kernel-core" in ansible_facts.packages'
      - result_authselect_current.rc != 0
      tags:
      - DISA-STIG-needed_rules
      - NIST-800-53-AC-3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.4
      - configure_strategy
      - enable_authselect
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Enable authselect - Verify If pam Has Been Altered
      ansible.builtin.command:
        cmd: rpm -qV pam
      register: result_altered_authselect
      changed_when: false
      failed_when: false
      when:
      - '"kernel-core" in ansible_facts.packages'
      - result_authselect_select is not skipped
      - result_authselect_select.rc != 0
      tags:
      - DISA-STIG-needed_rules
      - NIST-800-53-AC-3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.4
      - configure_strategy
      - enable_authselect
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Enable authselect - Informative Message Based on authselect Integrity Check
      ansible.builtin.assert:
        that:
        - result_authselect_current.rc == 0 or result_altered_authselect is skipped or
          result_altered_authselect.rc == 0
        fail_msg:
        - authselect is not used but files from the 'pam' package have been altered, so
          the authselect configuration won't be forced.
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-needed_rules
      - NIST-800-53-AC-3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.4
      - configure_strategy
      - enable_authselect
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Enable authselect - Force authselect Profile Selection
      ansible.builtin.command:
        cmd: authselect select --force "{{ var_authselect_profile }}"
      when:
      - '"kernel-core" in ansible_facts.packages'
      - result_authselect_current.rc != 0
      - result_authselect_select.rc != 0
      - result_altered_authselect.rc == 0
      tags:
      - DISA-STIG-needed_rules
      - NIST-800-53-AC-3
      - PCI-DSSv4-8.3
      - PCI-DSSv4-8.3.4
      - configure_strategy
      - enable_authselect
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
Group GRUB2 bootloader configuration Group contains 1 group and 7 rules
[ref] During the boot process, the boot loader is responsible for starting the execution of the kernel and passing options to it. The boot loader allows for the selection of different kernels - possibly on different partitions or media. The default Red Hat Enterprise Linux 9 boot loader for x86 systems is called GRUB2. Options it can pass to the kernel include single-user mode, which provides root access without any authentication, and the ability to disable SELinux. To prevent local users from modifying the boot parameters and endangering security, protect the boot loader configuration with a password and ensure its configuration file's permissions are set properly.
Group Non-UEFI GRUB2 bootloader configuration Group contains 7 rules
[ref] Non-UEFI GRUB2 bootloader configuration
#### Rule Verify /boot/grub2/grub.cfg Group Ownership [ref]
The file `/boot/grub2/grub.cfg` should be group-owned by the `root` group to prevent destruction or modification of the file. To properly set the group owner of `/boot/grub2/grub.cfg`, run the command:
    $ sudo chgrp root /boot/grub2/grub.cfg
Rationale: | The `root` group is a highly-privileged group. Furthermore, the group-owner of this file should not have any access privileges anyway.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_groupowner_grub2_cfg
References: |
cis-csc | 12, 13, 14, 15, 16, 18, 3, 5
cjis | 5.5.2.2
cobit5 | APO01.06, DSS05.04, DSS05.07, DSS06.02
cui | 3.4.5
hipaa | 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii)
isa-62443-2009 | 4.3.3.7.3
isa-62443-2013 | SR 2.1, SR 5.2
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nist | CM-6(a), AC-6(1)
nist-csf | PR.AC-4, PR.DS-5
pcidss | Req-7.1
os-srg | SRG-OS-000480-GPOS-00227
anssi | R29
pcidss4 | 2.2.6, 2.2
bsi | SYS.1.3.A14
cis | 1.4.2
stigref | SV-257790r991589_rule
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    # Remediation is applicable only in certain platforms
    if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel-core ) && { ( ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); }; then

    newgroup=""
    if getent group "0" >/dev/null 2>&1; then
      newgroup="0"
    fi

    if [[ -z "${newgroup}" ]]; then
      >&2 echo "0 is not a defined group on the system"
    else
    if ! stat -c "%g %G" "/boot/grub2/grub.cfg" | grep -E -w -q "0"; then
        chgrp --no-dereference "$newgroup" /boot/grub2/grub.cfg
    fi

    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.5.2.2
      - DISA-STIG-RHEL-09-212025
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-7.1
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_groupowner_grub2_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Set the file_groupowner_grub2_cfg_newgroup variable if represented by gid
      ansible.builtin.set_fact:
        file_groupowner_grub2_cfg_newgroup: '0'
      when:
      - ( "grub2-common" in ansible_facts.packages and "kernel-core" in ansible_facts.packages
        )
      - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"]
        )
      tags:
      - CJIS-5.5.2.2
      - DISA-STIG-RHEL-09-212025
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-7.1
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_groupowner_grub2_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Test for existence /boot/grub2/grub.cfg
      ansible.builtin.stat:
        path: /boot/grub2/grub.cfg
      register: file_exists
      when:
      - ( "grub2-common" in ansible_facts.packages and "kernel-core" in ansible_facts.packages
        )
      - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"]
        )
      tags:
      - CJIS-5.5.2.2
      - DISA-STIG-RHEL-09-212025
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-7.1
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_groupowner_grub2_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure group owner on /boot/grub2/grub.cfg
      ansible.builtin.file:
        path: /boot/grub2/grub.cfg
        follow: false
        group: '{{ file_groupowner_grub2_cfg_newgroup }}'
      when:
      - ( "grub2-common" in ansible_facts.packages and "kernel-core" in ansible_facts.packages
        )
      - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"]
        )
      - file_exists.stat is defined and file_exists.stat.exists
      tags:
      - CJIS-5.5.2.2
      - DISA-STIG-RHEL-09-212025
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-7.1
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_groupowner_grub2_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
#### Rule Verify /boot/grub2/user.cfg Group Ownership [ref]
The file `/boot/grub2/user.cfg` should be group-owned by the `root` group to prevent reading or modification of the file. To properly set the group owner of `/boot/grub2/user.cfg`, run the command:
    $ sudo chgrp root /boot/grub2/user.cfg
Rationale: | The `root` group is a highly-privileged group. Furthermore, the group-owner of this file should not have any access privileges anyway. Non-root users who read the boot parameters may be able to identify weaknesses in security upon boot and be able to exploit them.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_groupowner_user_cfg
References: |
cis-csc | 12, 13, 14, 15, 16, 18, 3, 5
cjis | 5.5.2.2
cobit5 | APO01.06, DSS05.04, DSS05.07, DSS06.02
cui | 3.4.5
hipaa | 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii)
isa-62443-2009 | 4.3.3.7.3
isa-62443-2013 | SR 2.1, SR 5.2
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nist | CM-6(a), AC-6(1)
nist-csf | PR.AC-4, PR.DS-5
pcidss | Req-7.1
os-srg | SRG-OS-000480-GPOS-00227
anssi | R29
pcidss4 | 2.2.6, 2.2
bsi | SYS.1.3.A14
cis | 1.4.2
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    # Remediation is applicable only in certain platforms
    if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel-core ) && { ( ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); }; then

    newgroup=""
    if getent group "0" >/dev/null 2>&1; then
      newgroup="0"
    fi

    if [[ -z "${newgroup}" ]]; then
      >&2 echo "0 is not a defined group on the system"
    else
    if ! stat -c "%g %G" "/boot/grub2/user.cfg" | grep -E -w -q "0"; then
        chgrp --no-dereference "$newgroup" /boot/grub2/user.cfg
    fi

    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.5.2.2
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-7.1
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_groupowner_user_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Set the file_groupowner_user_cfg_newgroup variable if represented by gid
      ansible.builtin.set_fact:
        file_groupowner_user_cfg_newgroup: '0'
      when:
      - ( "grub2-common" in ansible_facts.packages and "kernel-core" in ansible_facts.packages
        )
      - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"]
        )
      tags:
      - CJIS-5.5.2.2
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-7.1
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_groupowner_user_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Test for existence /boot/grub2/user.cfg
      ansible.builtin.stat:
        path: /boot/grub2/user.cfg
      register: file_exists
      when:
      - ( "grub2-common" in ansible_facts.packages and "kernel-core" in ansible_facts.packages
        )
      - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"]
        )
      tags:
      - CJIS-5.5.2.2
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-7.1
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_groupowner_user_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure group owner on /boot/grub2/user.cfg
      ansible.builtin.file:
        path: /boot/grub2/user.cfg
        follow: false
        group: '{{ file_groupowner_user_cfg_newgroup }}'
      when:
      - ( "grub2-common" in ansible_facts.packages and "kernel-core" in ansible_facts.packages
        )
      - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"]
        )
      - file_exists.stat is defined and file_exists.stat.exists
      tags:
      - CJIS-5.5.2.2
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-7.1
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_groupowner_user_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
#### Rule Verify /boot/grub2/grub.cfg User Ownership [ref]
The file `/boot/grub2/grub.cfg` should be owned by the `root` user to prevent destruction or modification of the file. To properly set the owner of `/boot/grub2/grub.cfg`, run the command:
    $ sudo chown root /boot/grub2/grub.cfg
Rationale: | Only root should be able to modify important boot parameters.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_owner_grub2_cfg
References: |
cis-csc | 12, 13, 14, 15, 16, 18, 3, 5
cjis | 5.5.2.2
cobit5 | APO01.06, DSS05.04, DSS05.07, DSS06.02
cui | 3.4.5
hipaa | 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii)
isa-62443-2009 | 4.3.3.7.3
isa-62443-2013 | SR 2.1, SR 5.2
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nist | CM-6(a), AC-6(1)
nist-csf | PR.AC-4, PR.DS-5
pcidss | Req-7.1
os-srg | SRG-OS-000480-GPOS-00227
anssi | R29
pcidss4 | 2.2.6, 2.2
bsi | SYS.1.3.A14
cis | 1.4.2
stigref | SV-257791r991589_rule
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    # Remediation is applicable only in certain platforms
    if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel-core ) && { ( ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); }; then

    newown=""
    if id "0" >/dev/null 2>&1; then
      newown="0"
    fi

    if [[ -z "$newown" ]]; then
      >&2 echo "0 is not a defined user on the system"
    else
    if ! stat -c "%u %U" "/boot/grub2/grub.cfg" | grep -E -w -q "0"; then
        chown --no-dereference "$newown" /boot/grub2/grub.cfg
    fi

    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.5.2.2
      - DISA-STIG-RHEL-09-212030
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-7.1
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_owner_grub2_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Set the file_owner_grub2_cfg_newown variable if represented by uid
      ansible.builtin.set_fact:
        file_owner_grub2_cfg_newown: '0'
      when:
      - ( "grub2-common" in ansible_facts.packages and "kernel-core" in ansible_facts.packages
        )
      - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"]
        )
      tags:
      - CJIS-5.5.2.2
      - DISA-STIG-RHEL-09-212030
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-7.1
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_owner_grub2_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Test for existence /boot/grub2/grub.cfg
      ansible.builtin.stat:
        path: /boot/grub2/grub.cfg
      register: file_exists
      when:
      - ( "grub2-common" in ansible_facts.packages and "kernel-core" in ansible_facts.packages
        )
      - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"]
        )
      tags:
      - CJIS-5.5.2.2
      - DISA-STIG-RHEL-09-212030
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-7.1
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_owner_grub2_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure owner on /boot/grub2/grub.cfg
      ansible.builtin.file:
        path: /boot/grub2/grub.cfg
        follow: false
        owner: '{{ file_owner_grub2_cfg_newown }}'
      when:
      - ( "grub2-common" in ansible_facts.packages and "kernel-core" in ansible_facts.packages
        )
      - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"]
        )
      - file_exists.stat is defined and file_exists.stat.exists
      tags:
      - CJIS-5.5.2.2
      - DISA-STIG-RHEL-09-212030
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-7.1
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_owner_grub2_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
#### Rule Verify /boot/grub2/user.cfg User Ownership [ref]
The file `/boot/grub2/user.cfg` should be owned by the `root` user to prevent reading or modification of the file. To properly set the owner of `/boot/grub2/user.cfg`, run the command:
    $ sudo chown root /boot/grub2/user.cfg
Rationale: | Only root should be able to modify important boot parameters. Also, non-root users who read the boot parameters may be able to identify weaknesses in security upon boot and be able to exploit them.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_owner_user_cfg
References: |
cis-csc | 12, 13, 14, 15, 16, 18, 3, 5
cjis | 5.5.2.2
cobit5 | APO01.06, DSS05.04, DSS05.07, DSS06.02
cui | 3.4.5
hipaa | 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii)
isa-62443-2009 | 4.3.3.7.3
isa-62443-2013 | SR 2.1, SR 5.2
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nist | CM-6(a), AC-6(1)
nist-csf | PR.AC-4, PR.DS-5
pcidss | Req-7.1
anssi | R29
pcidss4 | 2.2.6, 2.2
bsi | SYS.1.3.A14
cis | 1.4.2
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    # Remediation is applicable only in certain platforms
    if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel-core ) && { ( ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); }; then

    newown=""
    if id "0" >/dev/null 2>&1; then
      newown="0"
    fi

    if [[ -z "$newown" ]]; then
      >&2 echo "0 is not a defined user on the system"
    else
    if ! stat -c "%u %U" "/boot/grub2/user.cfg" | grep -E -w -q "0"; then
        chown --no-dereference "$newown" /boot/grub2/user.cfg
    fi

    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.5.2.2
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-7.1
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_owner_user_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Set the file_owner_user_cfg_newown variable if represented by uid
      ansible.builtin.set_fact:
        file_owner_user_cfg_newown: '0'
      when:
      - ( "grub2-common" in ansible_facts.packages and "kernel-core" in ansible_facts.packages
        )
      - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"]
        )
      tags:
      - CJIS-5.5.2.2
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-7.1
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_owner_user_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Test for existence /boot/grub2/user.cfg
      ansible.builtin.stat:
        path: /boot/grub2/user.cfg
      register: file_exists
      when:
      - ( "grub2-common" in ansible_facts.packages and "kernel-core" in ansible_facts.packages
        )
      - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"]
        )
      tags:
      - CJIS-5.5.2.2
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-7.1
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_owner_user_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure owner on /boot/grub2/user.cfg
      ansible.builtin.file:
        path: /boot/grub2/user.cfg
        follow: false
        owner: '{{ file_owner_user_cfg_newown }}'
      when:
      - ( "grub2-common" in ansible_facts.packages and "kernel-core" in ansible_facts.packages
        )
      - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"]
        )
      - file_exists.stat is defined and file_exists.stat.exists
      tags:
      - CJIS-5.5.2.2
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-7.1
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_owner_user_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
#### Rule Verify /boot/grub2/grub.cfg Permissions [ref]
File permissions for `/boot/grub2/grub.cfg` should be set to 600. To properly set the permissions of `/boot/grub2/grub.cfg`, run the command:
    $ sudo chmod 600 /boot/grub2/grub.cfg
Rationale: | Proper permissions ensure that only the root user can modify important boot parameters.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_permissions_grub2_cfg
References: |
cis-csc | 12, 13, 14, 15, 16, 18, 3, 5
cobit5 | APO01.06, DSS05.04, DSS05.07, DSS06.02
cui | 3.4.5
hipaa | 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii)
isa-62443-2009 | 4.3.3.7.3
isa-62443-2013 | SR 2.1, SR 5.2
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nist | CM-6(a), AC-6(1)
nist-csf | PR.AC-4, PR.DS-5
anssi | R29
pcidss4 | 2.2.6, 2.2
bsi | SYS.1.3.A14
cis | 1.4.2
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    # Remediation is applicable only in certain platforms
    if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel-core ) && { ( ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); }; then

    chmod u-xs,g-xwrs,o-xwrt /boot/grub2/grub.cfg

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_permissions_grub2_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Test for existence /boot/grub2/grub.cfg
      ansible.builtin.stat:
        path: /boot/grub2/grub.cfg
      register: file_exists
      when:
      - ( "grub2-common" in ansible_facts.packages and "kernel-core" in ansible_facts.packages
        )
      - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"]
        )
      tags:
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_permissions_grub2_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure permission u-xs,g-xwrs,o-xwrt on /boot/grub2/grub.cfg
      ansible.builtin.file:
        path: /boot/grub2/grub.cfg
        mode: u-xs,g-xwrs,o-xwrt
      when:
      - ( "grub2-common" in ansible_facts.packages and "kernel-core" in ansible_facts.packages
        )
      - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"]
        )
      - file_exists.stat is defined and file_exists.stat.exists
      tags:
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_permissions_grub2_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
#### Rule Verify /boot/grub2/user.cfg Permissions [ref]
File permissions for `/boot/grub2/user.cfg` should be set to 600. To properly set the permissions of `/boot/grub2/user.cfg`, run the command:
    $ sudo chmod 600 /boot/grub2/user.cfg
Rationale: | Proper permissions ensure that only the root user can read or modify important boot parameters.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_permissions_user_cfg
References: |
cis-csc | 12, 13, 14, 15, 16, 18, 3, 5
cobit5 | APO01.06, DSS05.04, DSS05.07, DSS06.02
cui | 3.4.5
hipaa | 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii)
isa-62443-2009 | 4.3.3.7.3
isa-62443-2013 | SR 2.1, SR 5.2
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nist | CM-6(a), AC-6(1)
nist-csf | PR.AC-4, PR.DS-5
anssi | R29
pcidss4 | 2.2.6, 2.2
bsi | SYS.1.3.A14
cis | 1.4.2
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    # Remediation is applicable only in certain platforms
    if ( rpm --quiet -q grub2-common && rpm --quiet -q kernel-core ) && { ( ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); }; then

    chmod u-xs,g-xwrs,o-xwrt /boot/grub2/user.cfg

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_permissions_user_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Test for existence /boot/grub2/user.cfg
      ansible.builtin.stat:
        path: /boot/grub2/user.cfg
      register: file_exists
      when:
      - ( "grub2-common" in ansible_facts.packages and "kernel-core" in ansible_facts.packages
        )
      - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"]
        )
      tags:
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_permissions_user_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure permission u-xs,g-xwrs,o-xwrt on /boot/grub2/user.cfg
      ansible.builtin.file:
        path: /boot/grub2/user.cfg
        mode: u-xs,g-xwrs,o-xwrt
      when:
      - ( "grub2-common" in ansible_facts.packages and "kernel-core" in ansible_facts.packages
        )
      - not ( ansible_virtualization_type in ["docker", "lxc", "openvz", "podman", "container"]
        )
      - file_exists.stat is defined and file_exists.stat.exists
      tags:
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_permissions_user_cfg
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
#### Rule Set Boot Loader Password in grub2 [ref]
The grub2 boot loader should have a superuser account and password protection enabled to protect boot-time settings. Since plaintext passwords are a security risk, generate a hash for the password by running the following command:
    # grub2-setpassword
When prompted, enter the password that was selected. Warning: To prevent hard-coded passwords, automatic remediation of this control is not available. Remediation must be automated as a component of machine provisioning, or followed manually as outlined above. Also, do NOT manually add the superuser account and password to the `grub.cfg` file as the grub2-mkconfig command overwrites this file.
Rationale: | Password protection on the boot loader configuration ensures users with physical access cannot trivially alter important bootloader settings. These include which kernel to use, and whether to enter single-user mode.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_grub2_password
References: |
cis-csc | 1, 11, 12, 14, 15, 16, 18, 3, 5
cobit5 | DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.06, DSS06.10
cui | 3.4.5
hipaa | 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii)
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7
iso27001-2013 | A.18.1.4, A.6.1.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5
nist | CM-6(a)
nist-csf | PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.PT-3
ospp | FIA_UAU.1
os-srg | SRG-OS-000080-GPOS-00048
anssi | R5
cis | 1.4.1
stigref | SV-257787r1137691_rule
Group Network Configuration and Firewalls Group contains 9 groups and 28 rules
[ref] Most systems must be connected to a network of some sort, and this brings with it the substantial risk of network attack. This section discusses the security impact of decisions about networking which must be made when configuring a system. This section also discusses firewalls, network access controls, and other network security frameworks, which allow system-level rules to be written that can limit an attackers' ability to connect to your system. These rules can specify that network traffic should be allowed or denied from certain IP addresses, hosts, and networks. The rules can also specify which of the system's network services are available to particular hosts or networks.
Group firewalld Group contains 2 groups and 5 rules
[ref] The dynamic firewall daemon `firewalld` provides a dynamically managed firewall with support for network “zones” to assign a level of trust to a network and its associated connections and interfaces. It has support for IPv4 and IPv6 firewall settings. It supports Ethernet bridges and has a separation of runtime and permanent configuration options. It also has an interface for services or applications to add firewall rules directly. A graphical configuration tool, `firewall-config`, is used to configure `firewalld`, which in turn uses `iptables` tool to communicate with `Netfilter` in the kernel which implements packet filtering. The firewall service provided by `firewalld` is dynamic rather than static because changes to the configuration can be made at anytime and are immediately implemented. There is no need to save or apply the changes. No unintended disruption of existing network connections occurs as no part of the firewall has to be reloaded.
Group Inspect and Activate Default firewalld Rules Group contains 2 rules
[ref] Firewalls can be used to separate networks into different zones based on the level of trust the user has decided to place on the devices and traffic within that network. `NetworkManager` informs firewalld to which zone an interface belongs. An interface's assigned zone can be changed by `NetworkManager` or via the `firewall-config` tool. The zone settings in `/etc/firewalld/` are a range of preset settings which can be quickly applied to a network interface. These are the zones provided by firewalld sorted according to the default trust level of the zones from untrusted to trusted: `drop` Any incoming network packets are dropped, there is no reply. Only outgoing network connections are possible. `block` Any incoming network connections are rejected with an `icmp-host-prohibited` message for IPv4 and `icmp6-adm-prohibited` for IPv6. Only network connections initiated from within the system are possible. `public` For use in public areas. You do not trust the other computers on the network to not harm your computer. Only selected incoming connections are accepted. `external` For use on external networks with masquerading enabled especially for routers. You do not trust the other computers on the network to not harm your computer. Only selected incoming connections are accepted. `dmz` For computers in your demilitarized zone that are publicly-accessible with limited access to your internal network. Only selected incoming connections are accepted. `work` For use in work areas. You mostly trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted. `home` For use in home areas. You mostly trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted. `internal` For use on internal networks. You mostly trust the other computers on the networks to not harm your computer. Only selected incoming connections are accepted. `trusted` All network connections are accepted. It is possible to designate one of these zones to be the default zone. When interface connections are added to `NetworkManager`, they are assigned to the default zone. On installation, the default zone in firewalld is set to be the public zone. To find out all the settings of a zone, for example the `public zone,` enter the following command as root:
    # firewall-cmd --zone=public --list-all
Example output of this command might look like the following:
    # firewall-cmd --zone=public --list-all
    public
      interfaces:
      services: mdns dhcpv6-client ssh
      ports:
      forward-ports:
      icmp-blocks: source-quench
To view the network zones currently active, enter the following command as root:
    # firewall-cmd --get-service
The following listing displays the result of this command on common Red Hat Enterprise Linux 9 system:
    # firewall-cmd --get-service
    amanda-client bacula bacula-client dhcp dhcpv6 dhcpv6-client dns ftp
    high-availability http https imaps ipp ipp-client ipsec kerberos kpasswd
    ldap ldaps libvirt libvirt-tls mdns mountd ms-wbt mysql nfs ntp openvpn
    pmcd pmproxy pmwebapi pmwebapis pop3s postgresql proxy-dhcp radius rpc-bind
    samba samba-client smtp ssh telnet tftp tftp-client transmission-client
    vnc-server wbem-https
Finally to view the network zones that will be active after the next firewalld service reload, enter the following command as root:
    # firewall-cmd --get-service --permanent
#### Rule Install firewalld Package [ref]
The `firewalld` package can be installed with the following command:
    $ sudo dnf install firewalld
Rationale: | "Firewalld" provides an easy and effective way to block/limit remote access to the system via ports, services, and protocols. Remote access services, such as those providing remote access to network devices and information systems, which lack automated control capabilities, increase risk and make remote user access management difficult at best. Remote access is access to nonpublic information systems by an authorized user (or an information system) communicating through an external, non-organization-controlled network. Remote access methods include, for example, dial-up, broadband, and wireless. Red Hat Enterprise Linux 9 functionality (e.g., SSH) must be capable of taking enforcement action if the audit reveals unauthorized activity. Automated control of remote access sessions allows organizations to ensure ongoing compliance with remote access policies by enforcing connection rules of remote access applications on a variety of information system components (e.g., servers, workstations, notebook computers, smartphones, and tablets)."
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_firewalld_installed
References: |
nist | CM-6(a)
ospp | FMT_SMF_EXT.1
os-srg | SRG-OS-000096-GPOS-00050, SRG-OS-000297-GPOS-00115, SRG-OS-000298-GPOS-00116, SRG-OS-000480-GPOS-00227, SRG-OS-000480-GPOS-00232
ism | 1409
pcidss4 | 1.2.1, 1.2
bsi | SYS.1.1.A19
cis | 4.1.2
stigref | SV-257935r1044994_rule
Remediation script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    dnf install firewalld
Remediation OSBuild Blueprint snippet ⇲
    [[packages]]
    name = "firewalld"
    version = "*"
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    if ! rpm -q --quiet "firewalld" ; then
        dnf install -y "firewalld"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    package install firewalld
Remediation Anaconda snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    package --add=firewalld
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-251010
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-1.2
      - PCI-DSSv4-1.2.1
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_firewalld_installed

    - name: Ensure firewalld is installed
      ansible.builtin.package:
        name: firewalld
        state: present
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-251010
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-1.2
      - PCI-DSSv4-1.2.1
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_firewalld_installed
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    include install_firewalld

    class install_firewalld {
      package { 'firewalld':
        ensure => 'installed',
      }
    }
#### Rule Verify firewalld Enabled [ref]
The `firewalld` service can be enabled with the following command:
    $ sudo systemctl enable firewalld.service
Rationale: | Access control methods provide the ability to enhance system security posture by restricting services and known good IP addresses and address ranges. This prevents connections from unknown hosts and protocols.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_firewalld_enabled
References: |
cis-csc | 11, 3, 9
cobit5 | BAI10.01, BAI10.02, BAI10.03, BAI10.05
cui | 3.1.3, 3.4.7
isa-62443-2009 | 4.3.4.3.2, 4.3.4.3.3
isa-62443-2013 | SR 7.6
iso27001-2013 | A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4
nerc-cip | CIP-003-8 R4, CIP-003-8 R5, CIP-004-6 R3
nist | AC-4, CM-7(b), CA-3(5), SC-7(21), CM-6(a)
nist-csf | PR.IP-1
ospp | FMT_SMF_EXT.1
os-srg | SRG-OS-000096-GPOS-00050, SRG-OS-000297-GPOS-00115, SRG-OS-000480-GPOS-00227, SRG-OS-000480-GPOS-00231, SRG-OS-000480-GPOS-00232
bsi | SYS.1.6.A5, SYS.1.6.A21, SYS.1.1.A19
ism | 1409
pcidss4 | 1.2.1, 1.2
cis | 4.1.2
stigref | SV-257936r1044995_rule
Remediation OSBuild Blueprint snippet ⇲
    [customizations.services]
    enabled = ["firewalld"]
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core && { rpm --quiet -q firewalld; }; then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" unmask 'firewalld.service'
    if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then
      "$SYSTEMCTL_EXEC" start 'firewalld.service'
    fi
    "$SYSTEMCTL_EXEC" enable 'firewalld.service'

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable
    service enable firewalld
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-251015
      - NIST-800-171-3.1.3
      - NIST-800-171-3.4.7
      - NIST-800-53-AC-4
      - NIST-800-53-CA-3(5)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(21)
      - PCI-DSSv4-1.2
      - PCI-DSSv4-1.2.1
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_firewalld_enabled

    - name: Verify firewalld Enabled - Enable service firewalld
      block:

      - name: Gather the package facts
        ansible.builtin.package_facts:
          manager: auto

      - name: Verify firewalld Enabled - Enable Service firewalld
        ansible.builtin.systemd:
          name: firewalld
          enabled: true
          state: started
          masked: false
        when:
        - '"firewalld" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-251015
      - NIST-800-171-3.1.3
      - NIST-800-171-3.4.7
      - NIST-800-53-AC-4
      - NIST-800-53-CA-3(5)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(21)
      - PCI-DSSv4-1.2
      - PCI-DSSv4-1.2.1
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_firewalld_enabled
      - special_service_block
      when:
      - '"kernel-core" in ansible_facts.packages'
      - '"firewalld" in ansible_facts.packages'
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    include enable_firewalld

    class enable_firewalld {
      service {'firewalld':
        enable => true,
        ensure => 'running',
      }
    }
Group Strengthen the Default Ruleset Group contains 3 rules
[ref] The default rules can be strengthened. The system scripts that activate the firewall rules expect them to be defined in configuration files under the `/etc/firewalld/services` and `/etc/firewalld/zones` directories. The following recommendations describe how to strengthen the default ruleset configuration file. An alternative to editing this configuration file is to create a shell script that makes calls to the `firewall-cmd` program to load in rules under the `/etc/firewalld/services` and `/etc/firewalld/zones` directories. Instructions apply to both unless otherwise noted. Language and address conventions for regular firewalld rules are used throughout this section. Warning: The program `firewall-config` allows additional services to penetrate the default firewall rules and automatically adjusts the `firewalld` ruleset(s).
#### Rule Configure Firewalld to Restrict Loopback Traffic [ref]
Configure `firewalld` to restrict loopback traffic to the `lo` interface. The loopback traffic must be trusted by assigning the `lo` interface to the `firewalld` `trusted` zone. However, the loopback traffic must be restricted to the loopback interface as an anti-spoofing measure. To configure `firewalld` to restrict loopback traffic to the `lo` interface, run the following commands:
    sudo firewall-cmd --permanent --zone=trusted --add-rich-rule='rule family=ipv4 source address="127.0.0.1" destination not address="127.0.0.1" drop'
    sudo firewall-cmd --permanent --zone=trusted --add-rich-rule='rule family=ipv6 source address="::1" destination not address="::1" drop'
To ensure `firewalld` settings are applied in runtime, run the following command:
    firewall-cmd --reload
Rationale: | Loopback traffic is generated between processes on machine and is typically critical to operation of the system. The loopback interface is the only place that loopback network traffic should be seen, all other interfaces should ignore traffic on this network as an anti-spoofing measure.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_firewalld_loopback_traffic_restricted
References: |
pcidss4 | 1.4.1, 1.4
cis | 4.2.2
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    if ! rpm -q --quiet "firewalld" ; then
        dnf install -y "firewalld"
    fi

    ipv4_rule='rule family=ipv4 source address="127.0.0.1" destination not address="127.0.0.1" drop'
    ipv6_rule='rule family=ipv6 source address="::1" destination not address="::1" drop'

    if test "$(stat -c %d:%i /)" != "$(stat -c %d:%i /proc/1/root/.)" || { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; }; then
        firewall-offline-cmd --zone=trusted --add-rich-rule="${ipv4_rule}"
        firewall-offline-cmd --zone=trusted --add-rich-rule="${ipv6_rule}"
    elif systemctl is-active firewalld; then
        firewall-cmd --permanent --zone=trusted --add-rich-rule="${ipv4_rule}"
        firewall-cmd --permanent --zone=trusted --add-rich-rule="${ipv6_rule}"
        firewall-cmd --reload
    else
        echo "
        firewalld service is not active. Remediation aborted!
        This remediation could not be applied because it depends on firewalld service running.
        The service is not started by this remediation in order to prevent connection issues."
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.1
      - configure_strategy
      - firewalld_loopback_traffic_restricted
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Configure Firewalld to Restrict Loopback Traffic - Ensure firewalld Package
        is Installed
      ansible.builtin.package:
        name: '{{ item }}'
        state: present
      with_items:
      - firewalld
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.1
      - configure_strategy
      - firewalld_loopback_traffic_restricted
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Configure Firewalld to Restrict Loopback Traffic - Collect Facts About System
        Services
      ansible.builtin.service_facts: null
      register: result_services_states
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.1
      - configure_strategy
      - firewalld_loopback_traffic_restricted
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Configure Firewalld to Restrict Loopback Traffic - Remediation is Applicable
        if firewalld Service is Running
      block:

      - name: Configure Firewalld to Restrict Loopback Traffic - Ensure firewalld trusted
          Zone Restricts IPv4 Loopback Traffic
        ansible.builtin.command:
          cmd: firewall-cmd --permanent --zone=trusted --add-rich-rule='rule family=ipv4
            source address="127.0.0.1" destination not address="127.0.0.1" drop'
        register: result_trusted_ipv4_restriction
        changed_when:
        - '''ALREADY_ENABLED'' not in result_trusted_ipv4_restriction.stderr'

      - name: Configure Firewalld to Restrict Loopback Traffic - Ensure firewalld trusted
          Zone Restricts IPv6 Loopback Traffic
        ansible.builtin.command:
          cmd: firewall-cmd --permanent --zone=trusted --add-rich-rule='rule family=ipv6
            source address="::1" destination not address="::1" drop'
        register: result_trusted_ipv6_restriction
        changed_when:
        - '''ALREADY_ENABLED'' not in result_trusted_ipv6_restriction.stderr'

      - name: Configure Firewalld to Restrict Loopback Traffic - Ensure firewalld Changes
          are Applied
        ansible.builtin.service:
          name: firewalld
          state: reloaded
        when:
        - result_trusted_ipv4_restriction is changed or result_trusted_ipv6_restriction
          is changed
      when:
      - '"kernel-core" in ansible_facts.packages'
      - ansible_facts.services['firewalld.service'].state == 'running'
      tags:
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.1
      - configure_strategy
      - firewalld_loopback_traffic_restricted
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Configure Firewalld to Restrict Loopback Traffic - Informative Message Based
        on Service State
      ansible.builtin.assert:
        that:
        - ansible_check_mode or ansible_facts.services['firewalld.service'].state == 'running'
        fail_msg:
        - firewalld service is not active. Remediation aborted!
        - This remediation could not be applied because it depends on firewalld service
          running.
        - The service is not started by this remediation in order to prevent connection
          issues.
        success_msg:
        - Configure Firewalld to Restrict Loopback Traffic remediation successfully executed
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.1
      - configure_strategy
      - firewalld_loopback_traffic_restricted
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
#### Rule Configure Firewalld to Trust Loopback Traffic [ref]
Assign loopback interface to the `firewalld` `trusted` zone in order to explicitly allow the loopback traffic in the system. To configure `firewalld` to trust loopback traffic, run the following command:
    sudo firewall-cmd --permanent --zone=trusted --add-interface=lo
To ensure `firewalld` settings are applied in runtime, run the following command:
    firewall-cmd --reload
Rationale: | Loopback traffic is generated between processes on machine and is typically critical to operation of the system. The loopback interface is the only place that loopback network traffic should be seen, all other interfaces should ignore traffic on this network as an anti-spoofing measure.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_firewalld_loopback_traffic_trusted
References: |
pcidss4 | 1.4.1, 1.4
cis | 4.2.2
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    if ! rpm -q --quiet "firewalld" ; then
        dnf install -y "firewalld"
    fi

    if test "$(stat -c %d:%i /)" != "$(stat -c %d:%i /proc/1/root/.)" || { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; }; then
        firewall-offline-cmd --zone=trusted --add-interface=lo
    elif systemctl is-active firewalld; then
        firewall-cmd --permanent --zone=trusted --add-interface=lo
        firewall-cmd --reload
    else
        echo "
        firewalld service is not active. Remediation aborted!
        This remediation could not be applied because it depends on firewalld service running.
        The service is not started by this remediation in order to prevent connection issues."
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.1
      - configure_strategy
      - firewalld_loopback_traffic_trusted
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Configure Firewalld to Trust Loopback Traffic - Ensure firewalld Package is
        Installed
      ansible.builtin.package:
        name: '{{ item }}'
        state: present
      with_items:
      - firewalld
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.1
      - configure_strategy
      - firewalld_loopback_traffic_trusted
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Configure Firewalld to Trust Loopback Traffic - Collect Facts About System
        Services
      ansible.builtin.service_facts: null
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.1
      - configure_strategy
      - firewalld_loopback_traffic_trusted
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Configure Firewalld to Trust Loopback Traffic - Remediation is Applicable
        if firewalld Service is Running
      block:

      - name: Configure Firewalld to Trust Loopback Traffic - Ensure firewalld trusted
          Zone Includes lo Interface
        ansible.builtin.command:
          cmd: firewall-cmd --permanent --zone=trusted --add-interface=lo
        register: result_lo_interface_assignment
        changed_when:
        - '''ALREADY_ENABLED'' not in result_lo_interface_assignment.stderr'

      - name: Configure Firewalld to Trust Loopback Traffic - Ensure firewalld Changes
          are Applied
        ansible.builtin.service:
          name: firewalld
          state: reloaded
        when:
        - result_lo_interface_assignment is changed
      when:
      - '"kernel-core" in ansible_facts.packages'
      - ansible_facts.services['firewalld.service'].state == 'running'
      tags:
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.1
      - configure_strategy
      - firewalld_loopback_traffic_trusted
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Configure Firewalld to Trust Loopback Traffic - Informative Message Based
        on Service State
      ansible.builtin.assert:
        that:
        - ansible_check_mode or ansible_facts.services['firewalld.service'].state == 'running'
        fail_msg:
        - firewalld service is not active. Remediation aborted!
        - This remediation could not be applied because it depends on firewalld service
          running.
        - The service is not started by this remediation in order to prevent connection
          issues.
        success_msg:
        - Configure Firewalld to Trust Loopback Traffic remediation successfully executed
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.1
      - configure_strategy
      - firewalld_loopback_traffic_trusted
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
#### Rule Set Default firewalld Zone for Incoming Packets [ref]
To set the default zone to `drop` for the built-in default zone which processes incoming IPv4 and IPv6 packets, modify the following line in `/etc/firewalld/firewalld.conf` to be:
    DefaultZone=drop
Warning: To prevent denying any access to the system, automatic remediation of this control is not available. Remediation must be automated as a component of machine provisioning, or followed manually as outlined above.
Rationale: | In `firewalld` the default zone is applied only after all the applicable rules in the table are examined for a match. Setting the default zone to `drop` implements proper design for a firewall, i.e. any packets which are not explicitly permitted should not be accepted.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_set_firewalld_default_zone
References: |
cis-csc | 11, 14, 3, 9
cjis | 5.10.1
cobit5 | BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06
cui | 3.1.3, 3.4.7, 3.13.6
isa-62443-2009 | 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6
iso27001-2013 | A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2
nist | CA-3(5), CM-7(b), SC-7(23), CM-6(a)
nist-csf | PR.IP-1, PR.PT-3
pcidss | Req-1.4
os-srg | SRG-OS-000480-GPOS-00227
ism | 1416
pcidss4 | 1.3.1, 1.3
Group IPv6 Group contains 1 group and 6 rules
[ref] The system includes support for Internet Protocol version 6. A major and often-mentioned improvement over IPv4 is its enormous increase in the number of available addresses. Another important feature is its support for automatic configuration of many network settings.
Group Configure IPv6 Settings if Necessary Group contains 6 rules
[ref] A major feature of IPv6 is the extent to which systems implementing it can automatically configure their networking devices using information from the network. From a security perspective, manually configuring important configuration information is preferable to accepting it from the network in an unauthenticated fashion.
#### Rule Configure Accepting Router Advertisements on All IPv6 Interfaces [ref]
To set the runtime status of the `net.ipv6.conf.all.accept_ra` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv6.conf.all.accept_ra=0
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv6.conf.all.accept_ra = 0
Rationale: | An illicit router advertisement message could result in a man-in-the-middle attack.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv6_conf_all_accept_ra
References: |
cis-csc | 11, 14, 3, 9
cobit5 | BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6
iso27001-2013 | A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2
nist | CM-7(a), CM-7(b), CM-6(a)
nist-csf | PR.IP-1, PR.PT-3
os-srg | SRG-OS-000480-GPOS-00227
cis | 3.3.11
stigref | SV-257971r1155754_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv6.conf.all.accept_ra%3D0%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv6_conf_all_accept_ra.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv6.conf.all.accept_ra from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.accept_ra.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv6.conf.all.accept_ra" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv6_conf_all_accept_ra.conf'

    sysctl_net_ipv6_conf_all_accept_ra_value='0'

    #
    # Set runtime for net.ipv6.conf.all.accept_ra
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv6.conf.all.accept_ra="$sysctl_net_ipv6_conf_all_accept_ra_value"
    fi

    #
    # If net.ipv6.conf.all.accept_ra present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv6.conf.all.accept_ra = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.accept_ra")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_accept_ra_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.accept_ra\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.accept_ra\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-254010
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_ra
    - name: XCCDF Value sysctl_net_ipv6_conf_all_accept_ra_value # promote to variable
      set_fact:
        sysctl_net_ipv6_conf_all_accept_ra_value: !!str 0
      tags:
        - always

    - name: Configure Accepting Router Advertisements on All IPv6 Interfaces - Set fact
        for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254010
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_ra

    - name: Configure Accepting Router Advertisements on All IPv6 Interfaces - Find all
        files that contain net.ipv6.conf.all.accept_ra
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv6.conf.all.accept_ra\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254010
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_ra

    - name: Configure Accepting Router Advertisements on All IPv6 Interfaces - Find all
        files that set net.ipv6.conf.all.accept_ra to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv6.conf.all.accept_ra\s*=\s*{{ sysctl_net_ipv6_conf_all_accept_ra_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254010
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_ra

    - name: Configure Accepting Router Advertisements on All IPv6 Interfaces - Comment
        out any occurrences of net.ipv6.conf.all.accept_ra from config files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv6.conf.all.accept_ra
        replace: '#net.ipv6.conf.all.accept_ra'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - DISA-STIG-RHEL-09-254010
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_ra

    - name: Configure Accepting Router Advertisements on All IPv6 Interfaces - Comment
        out any occurrences of net.ipv6.conf.all.accept_ra from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv6.conf.all.accept_ra
        replace: '#net.ipv6.conf.all.accept_ra'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254010
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_ra

    - name: Configure Accepting Router Advertisements on All IPv6 Interfaces - Ensure
        sysctl net.ipv6.conf.all.accept_ra is set
      ansible.posix.sysctl:
        name: net.ipv6.conf.all.accept_ra
        value: '{{ sysctl_net_ipv6_conf_all_accept_ra_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv6_conf_all_accept_ra.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254010
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_ra
#### Rule Disable Accepting ICMP Redirects for All IPv6 Interfaces [ref]
To set the runtime status of the `net.ipv6.conf.all.accept_redirects` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv6.conf.all.accept_redirects=0
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv6.conf.all.accept_redirects = 0
Rationale: | An illicit ICMP redirect message could result in a man-in-the-middle attack.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv6_conf_all_accept_redirects
References: |
cis-csc | 11, 14, 3, 9
cobit5 | BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6
iso27001-2013 | A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2
nist | CM-7(a), CM-7(b), CM-6(a), CM-6(b), CM-6.1(iv)
nist-csf | PR.IP-1, PR.PT-3
os-srg | SRG-OS-000480-GPOS-00227
anssi | R13
cis | 3.3.5
stigref | SV-257972r1155757_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv6.conf.all.accept_redirects%3D0%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv6_conf_all_accept_redirects.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv6.conf.all.accept_redirects from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.accept_redirects.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv6.conf.all.accept_redirects" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv6_conf_all_accept_redirects.conf'

    sysctl_net_ipv6_conf_all_accept_redirects_value='0'

    #
    # Set runtime for net.ipv6.conf.all.accept_redirects
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv6.conf.all.accept_redirects="$sysctl_net_ipv6_conf_all_accept_redirects_value"
    fi

    #
    # If net.ipv6.conf.all.accept_redirects present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv6.conf.all.accept_redirects = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.accept_redirects")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_accept_redirects_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.accept_redirects\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.accept_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-254015
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_redirects
    - name: XCCDF Value sysctl_net_ipv6_conf_all_accept_redirects_value # promote to variable
      set_fact:
        sysctl_net_ipv6_conf_all_accept_redirects_value: !!str 0
      tags:
        - always

    - name: Disable Accepting ICMP Redirects for All IPv6 Interfaces - Set fact for sysctl
        paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254015
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_redirects

    - name: Disable Accepting ICMP Redirects for All IPv6 Interfaces - Find all files
        that contain net.ipv6.conf.all.accept_redirects
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv6.conf.all.accept_redirects\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254015
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_redirects

    - name: Disable Accepting ICMP Redirects for All IPv6 Interfaces - Find all files
        that set net.ipv6.conf.all.accept_redirects to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv6.conf.all.accept_redirects\s*=\s*{{ sysctl_net_ipv6_conf_all_accept_redirects_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254015
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_redirects

    - name: Disable Accepting ICMP Redirects for All IPv6 Interfaces - Comment out any
        occurrences of net.ipv6.conf.all.accept_redirects from config files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv6.conf.all.accept_redirects
        replace: '#net.ipv6.conf.all.accept_redirects'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - DISA-STIG-RHEL-09-254015
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_redirects

    - name: Disable Accepting ICMP Redirects for All IPv6 Interfaces - Comment out any
        occurrences of net.ipv6.conf.all.accept_redirects from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv6.conf.all.accept_redirects
        replace: '#net.ipv6.conf.all.accept_redirects'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254015
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_redirects

    - name: Disable Accepting ICMP Redirects for All IPv6 Interfaces - Ensure sysctl net.ipv6.conf.all.accept_redirects
        is set
      ansible.posix.sysctl:
        name: net.ipv6.conf.all.accept_redirects
        value: '{{ sysctl_net_ipv6_conf_all_accept_redirects_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv6_conf_all_accept_redirects.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254015
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_redirects
#### Rule Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv6 Interfaces [ref]
To set the runtime status of the `net.ipv6.conf.all.accept_source_route` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv6.conf.all.accept_source_route=0
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv6.conf.all.accept_source_route = 0
Rationale: | Source-routed packets allow the source of the packet to suggest routers forward the packet along a different path than configured on the router, which can be used to bypass network security measures. This requirement applies only to the forwarding of source-routerd traffic, such as when IPv6 forwarding is enabled and the system is functioning as a router. Accepting source-routed packets in the IPv6 protocol has few legitimate uses. It should be disabled unless it is absolutely required.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv6_conf_all_accept_source_route
References: |
cis-csc | 1, 12, 13, 14, 15, 16, 18, 4, 6, 8, 9
cobit5 | APO01.06, APO13.01, DSS01.05, DSS03.01, DSS05.02, DSS05.04, DSS05.07, DSS06.02
cui | 3.1.20
isa-62443-2009 | 4.2.3.4, 4.3.3.4, 4.4.3.3
isa-62443-2013 | SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nist | CM-7(a), CM-7(b), CM-6(a)
nist-csf | DE.AE-1, ID.AM-3, PR.AC-5, PR.DS-5, PR.PT-4
os-srg | SRG-OS-000480-GPOS-00227
anssi | R13
cis | 3.3.8
stigref | SV-257973r1155760_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv6.conf.all.accept_source_route%3D0%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv6_conf_all_accept_source_route.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv6.conf.all.accept_source_route from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.all.accept_source_route.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv6.conf.all.accept_source_route" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv6_conf_all_accept_source_route.conf'

    sysctl_net_ipv6_conf_all_accept_source_route_value='0'

    #
    # Set runtime for net.ipv6.conf.all.accept_source_route
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv6.conf.all.accept_source_route="$sysctl_net_ipv6_conf_all_accept_source_route_value"
    fi

    #
    # If net.ipv6.conf.all.accept_source_route present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv6.conf.all.accept_source_route = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.all.accept_source_route")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_all_accept_source_route_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.all.accept_source_route\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.all.accept_source_route\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-254020
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_source_route
    - name: XCCDF Value sysctl_net_ipv6_conf_all_accept_source_route_value # promote to variable
      set_fact:
        sysctl_net_ipv6_conf_all_accept_source_route_value: !!str 0
      tags:
        - always

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv6 Interfaces
        - Set fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254020
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv6 Interfaces
        - Find all files that contain net.ipv6.conf.all.accept_source_route
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv6.conf.all.accept_source_route\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254020
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv6 Interfaces
        - Find all files that set net.ipv6.conf.all.accept_source_route to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv6.conf.all.accept_source_route\s*=\s*{{ sysctl_net_ipv6_conf_all_accept_source_route_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254020
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv6 Interfaces
        - Comment out any occurrences of net.ipv6.conf.all.accept_source_route from config
        files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv6.conf.all.accept_source_route
        replace: '#net.ipv6.conf.all.accept_source_route'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - DISA-STIG-RHEL-09-254020
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv6 Interfaces
        - Comment out any occurrences of net.ipv6.conf.all.accept_source_route from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv6.conf.all.accept_source_route
        replace: '#net.ipv6.conf.all.accept_source_route'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254020
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv6 Interfaces
        - Ensure sysctl net.ipv6.conf.all.accept_source_route is set
      ansible.posix.sysctl:
        name: net.ipv6.conf.all.accept_source_route
        value: '{{ sysctl_net_ipv6_conf_all_accept_source_route_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv6_conf_all_accept_source_route.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254020
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_all_accept_source_route
#### Rule Disable Accepting Router Advertisements on all IPv6 Interfaces by Default [ref]
To set the runtime status of the `net.ipv6.conf.default.accept_ra` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv6.conf.default.accept_ra=0
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv6.conf.default.accept_ra = 0
Rationale: | An illicit router advertisement message could result in a man-in-the-middle attack.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv6_conf_default_accept_ra
References: |
cis-csc | 11, 14, 3, 9
cobit5 | BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6
iso27001-2013 | A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2
nist | CM-7(a), CM-7(b), CM-6(a)
nist-csf | PR.IP-1, PR.PT-3
os-srg | SRG-OS-000480-GPOS-00227
cis | 3.3.11
stigref | SV-257975r1155766_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv6.conf.default.accept_ra%3D0%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv6_conf_default_accept_ra.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv6.conf.default.accept_ra from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.default.accept_ra.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv6.conf.default.accept_ra" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv6_conf_default_accept_ra.conf'

    sysctl_net_ipv6_conf_default_accept_ra_value='0'

    #
    # Set runtime for net.ipv6.conf.default.accept_ra
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv6.conf.default.accept_ra="$sysctl_net_ipv6_conf_default_accept_ra_value"
    fi

    #
    # If net.ipv6.conf.default.accept_ra present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv6.conf.default.accept_ra = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.default.accept_ra")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_default_accept_ra_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.default.accept_ra\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.default.accept_ra\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-254030
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_ra
    - name: XCCDF Value sysctl_net_ipv6_conf_default_accept_ra_value # promote to variable
      set_fact:
        sysctl_net_ipv6_conf_default_accept_ra_value: !!str 0
      tags:
        - always

    - name: Disable Accepting Router Advertisements on all IPv6 Interfaces by Default
        - Set fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254030
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_ra

    - name: Disable Accepting Router Advertisements on all IPv6 Interfaces by Default
        - Find all files that contain net.ipv6.conf.default.accept_ra
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv6.conf.default.accept_ra\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254030
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_ra

    - name: Disable Accepting Router Advertisements on all IPv6 Interfaces by Default
        - Find all files that set net.ipv6.conf.default.accept_ra to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv6.conf.default.accept_ra\s*=\s*{{ sysctl_net_ipv6_conf_default_accept_ra_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254030
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_ra

    - name: Disable Accepting Router Advertisements on all IPv6 Interfaces by Default
        - Comment out any occurrences of net.ipv6.conf.default.accept_ra from config files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv6.conf.default.accept_ra
        replace: '#net.ipv6.conf.default.accept_ra'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - DISA-STIG-RHEL-09-254030
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_ra

    - name: Disable Accepting Router Advertisements on all IPv6 Interfaces by Default
        - Comment out any occurrences of net.ipv6.conf.default.accept_ra from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv6.conf.default.accept_ra
        replace: '#net.ipv6.conf.default.accept_ra'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254030
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_ra

    - name: Disable Accepting Router Advertisements on all IPv6 Interfaces by Default
        - Ensure sysctl net.ipv6.conf.default.accept_ra is set
      ansible.posix.sysctl:
        name: net.ipv6.conf.default.accept_ra
        value: '{{ sysctl_net_ipv6_conf_default_accept_ra_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv6_conf_default_accept_ra.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254030
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_ra
#### Rule Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv6 Interfaces [ref]
To set the runtime status of the `net.ipv6.conf.default.accept_redirects` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv6.conf.default.accept_redirects=0
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv6.conf.default.accept_redirects = 0
Rationale: | An illicit ICMP redirect message could result in a man-in-the-middle attack.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv6_conf_default_accept_redirects
References: |
cis-csc | 11, 14, 3, 9
cobit5 | BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6
iso27001-2013 | A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2
nist | CM-7(a), CM-7(b), CM-6(a)
nist-csf | PR.IP-1, PR.PT-3
os-srg | SRG-OS-000480-GPOS-00227
anssi | R13
cis | 3.3.5
stigref | SV-257976r1155769_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv6.conf.default.accept_redirects%20%3D%200%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv6_conf_default_accept_redirects.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv6.conf.default.accept_redirects from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.default.accept_redirects.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv6.conf.default.accept_redirects" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv6_conf_default_accept_redirects.conf'

    sysctl_net_ipv6_conf_default_accept_redirects_value='0'

    #
    # Set runtime for net.ipv6.conf.default.accept_redirects
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv6.conf.default.accept_redirects="$sysctl_net_ipv6_conf_default_accept_redirects_value"
    fi

    #
    # If net.ipv6.conf.default.accept_redirects present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv6.conf.default.accept_redirects = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.default.accept_redirects")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_default_accept_redirects_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.default.accept_redirects\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.default.accept_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-254035
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_redirects
    - name: XCCDF Value sysctl_net_ipv6_conf_default_accept_redirects_value # promote to variable
      set_fact:
        sysctl_net_ipv6_conf_default_accept_redirects_value: !!str 0
      tags:
        - always

    - name: Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv6 Interfaces
        - Set fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254035
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_redirects

    - name: Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv6 Interfaces
        - Find all files that contain net.ipv6.conf.default.accept_redirects
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv6.conf.default.accept_redirects\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254035
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_redirects

    - name: Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv6 Interfaces
        - Find all files that set net.ipv6.conf.default.accept_redirects to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv6.conf.default.accept_redirects\s*=\s*{{ sysctl_net_ipv6_conf_default_accept_redirects_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254035
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_redirects

    - name: Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv6 Interfaces
        - Comment out any occurrences of net.ipv6.conf.default.accept_redirects from config
        files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv6.conf.default.accept_redirects
        replace: '#net.ipv6.conf.default.accept_redirects'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - DISA-STIG-RHEL-09-254035
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_redirects

    - name: Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv6 Interfaces
        - Comment out any occurrences of net.ipv6.conf.default.accept_redirects from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv6.conf.default.accept_redirects
        replace: '#net.ipv6.conf.default.accept_redirects'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254035
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_redirects

    - name: Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv6 Interfaces
        - Ensure sysctl net.ipv6.conf.default.accept_redirects is set
      ansible.posix.sysctl:
        name: net.ipv6.conf.default.accept_redirects
        value: '{{ sysctl_net_ipv6_conf_default_accept_redirects_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv6_conf_default_accept_redirects.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254035
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_redirects
#### Rule Disable Kernel Parameter for Accepting Source-Routed Packets on IPv6 Interfaces by Default [ref]
To set the runtime status of the `net.ipv6.conf.default.accept_source_route` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv6.conf.default.accept_source_route=0
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv6.conf.default.accept_source_route = 0
Rationale: | Source-routed packets allow the source of the packet to suggest routers forward the packet along a different path than configured on the router, which can be used to bypass network security measures. This requirement applies only to the forwarding of source-routerd traffic, such as when IPv6 forwarding is enabled and the system is functioning as a router. Accepting source-routed packets in the IPv6 protocol has few legitimate uses. It should be disabled unless it is absolutely required.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv6_conf_default_accept_source_route
References: |
cis-csc | 1, 12, 13, 14, 15, 16, 18, 4, 6, 8, 9
cobit5 | APO01.06, APO13.01, DSS01.05, DSS03.01, DSS05.02, DSS05.04, DSS05.07, DSS06.02
cui | 3.1.20
isa-62443-2009 | 4.2.3.4, 4.3.3.4, 4.4.3.3
isa-62443-2013 | SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nist | CM-7(a), CM-7(b), CM-6(a), CM-6(b), CM-6.1(iv)
nist-csf | DE.AE-1, ID.AM-3, PR.AC-5, PR.DS-5, PR.PT-4
pcidss | Req-1.4.3
os-srg | SRG-OS-000480-GPOS-00227
anssi | R13
pcidss4 | 1.4.2, 1.4
cis | 3.3.8
stigref | SV-257977r1155772_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv6.conf.default.accept_source_route%3D0%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv6_conf_default_accept_source_route.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv6.conf.default.accept_source_route from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv6.conf.default.accept_source_route.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv6.conf.default.accept_source_route" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv6_conf_default_accept_source_route.conf'

    sysctl_net_ipv6_conf_default_accept_source_route_value='0'

    #
    # Set runtime for net.ipv6.conf.default.accept_source_route
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv6.conf.default.accept_source_route="$sysctl_net_ipv6_conf_default_accept_source_route_value"
    fi

    #
    # If net.ipv6.conf.default.accept_source_route present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv6.conf.default.accept_source_route = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv6.conf.default.accept_source_route")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv6_conf_default_accept_source_route_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv6.conf.default.accept_source_route\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv6.conf.default.accept_source_route\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-254040
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_source_route
    - name: XCCDF Value sysctl_net_ipv6_conf_default_accept_source_route_value # promote to variable
      set_fact:
        sysctl_net_ipv6_conf_default_accept_source_route_value: !!str 0
      tags:
        - always

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on IPv6 Interfaces
        by Default - Set fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254040
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on IPv6 Interfaces
        by Default - Find all files that contain net.ipv6.conf.default.accept_source_route
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv6.conf.default.accept_source_route\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254040
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on IPv6 Interfaces
        by Default - Find all files that set net.ipv6.conf.default.accept_source_route
        to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv6.conf.default.accept_source_route\s*=\s*{{ sysctl_net_ipv6_conf_default_accept_source_route_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254040
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on IPv6 Interfaces
        by Default - Comment out any occurrences of net.ipv6.conf.default.accept_source_route
        from config files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv6.conf.default.accept_source_route
        replace: '#net.ipv6.conf.default.accept_source_route'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - DISA-STIG-RHEL-09-254040
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on IPv6 Interfaces
        by Default - Comment out any occurrences of net.ipv6.conf.default.accept_source_route
        from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv6.conf.default.accept_source_route
        replace: '#net.ipv6.conf.default.accept_source_route'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254040
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on IPv6 Interfaces
        by Default - Ensure sysctl net.ipv6.conf.default.accept_source_route is set
      ansible.posix.sysctl:
        name: net.ipv6.conf.default.accept_source_route
        value: '{{ sysctl_net_ipv6_conf_default_accept_source_route_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv6_conf_default_accept_source_route.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-254040
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv6_conf_default_accept_source_route
Group Kernel Parameters Which Affect Networking Group contains 2 groups and 16 rules
[ref] The `sysctl` utility is used to set parameters which affect the operation of the Linux kernel. Kernel parameters which affect networking and have security implications are described here.
Group Network Related Kernel Runtime Parameters for Hosts and Routers Group contains 13 rules
[ref] Certain kernel parameters should be set for systems which are acting as either hosts or routers to improve the system's ability defend against certain types of IPv4 protocol attacks.
#### Rule Disable Accepting ICMP Redirects for All IPv4 Interfaces [ref]
To set the runtime status of the `net.ipv4.conf.all.accept_redirects` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv4.conf.all.accept_redirects=0
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv4.conf.all.accept_redirects = 0
Rationale: | ICMP redirect messages are used by routers to inform hosts that a more direct route exists for a particular destination. These messages modify the host's route table and are unauthenticated. An illicit ICMP redirect message could result in a man-in-the-middle attack. This feature of the IPv4 protocol has few legitimate uses. It should be disabled unless absolutely required."
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_all_accept_redirects
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 2, 3, 7, 8, 9
cjis | 5.10.1.1
cobit5 | APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS03.05, DSS05.02, DSS05.05, DSS05.07, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 6.2, SR 7.1, SR 7.2, SR 7.6
iso27001-2013 | A.12.1.2, A.12.1.3, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.9.1.2
nist | CM-7(a), CM-7(b), CM-6(a), SC-7(a)
nist-csf | DE.CM-1, PR.DS-4, PR.IP-1, PR.PT-3
os-srg | SRG-OS-000480-GPOS-00227
anssi | R12
cis | 3.3.5
stigref | SV-257958r1155721_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv4.conf.all.accept_redirects%3D0%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_all_accept_redirects.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv4.conf.all.accept_redirects from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.accept_redirects.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv4.conf.all.accept_redirects" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv4_conf_all_accept_redirects.conf'

    sysctl_net_ipv4_conf_all_accept_redirects_value='0'

    #
    # Set runtime for net.ipv4.conf.all.accept_redirects
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv4.conf.all.accept_redirects="$sysctl_net_ipv4_conf_all_accept_redirects_value"
    fi

    #
    # If net.ipv4.conf.all.accept_redirects present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv4.conf.all.accept_redirects = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.accept_redirects")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_all_accept_redirects_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.accept_redirects\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.accept_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253015
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_accept_redirects
    - name: XCCDF Value sysctl_net_ipv4_conf_all_accept_redirects_value # promote to variable
      set_fact:
        sysctl_net_ipv4_conf_all_accept_redirects_value: !!str 0
      tags:
        - always

    - name: Disable Accepting ICMP Redirects for All IPv4 Interfaces - Set fact for sysctl
        paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253015
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_accept_redirects

    - name: Disable Accepting ICMP Redirects for All IPv4 Interfaces - Find all files
        that contain net.ipv4.conf.all.accept_redirects
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.all.accept_redirects\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253015
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_accept_redirects

    - name: Disable Accepting ICMP Redirects for All IPv4 Interfaces - Find all files
        that set net.ipv4.conf.all.accept_redirects to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.all.accept_redirects\s*=\s*{{ sysctl_net_ipv4_conf_all_accept_redirects_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253015
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_accept_redirects

    - name: Disable Accepting ICMP Redirects for All IPv4 Interfaces - Comment out any
        occurrences of net.ipv4.conf.all.accept_redirects from config files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv4.conf.all.accept_redirects
        replace: '#net.ipv4.conf.all.accept_redirects'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253015
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_accept_redirects

    - name: Disable Accepting ICMP Redirects for All IPv4 Interfaces - Comment out any
        occurrences of net.ipv4.conf.all.accept_redirects from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv4.conf.all.accept_redirects
        replace: '#net.ipv4.conf.all.accept_redirects'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253015
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_accept_redirects

    - name: Disable Accepting ICMP Redirects for All IPv4 Interfaces - Ensure sysctl net.ipv4.conf.all.accept_redirects
        is set
      ansible.posix.sysctl:
        name: net.ipv4.conf.all.accept_redirects
        value: '{{ sysctl_net_ipv4_conf_all_accept_redirects_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv4_conf_all_accept_redirects.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253015
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_accept_redirects
#### Rule Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv4 Interfaces [ref]
To set the runtime status of the `net.ipv4.conf.all.accept_source_route` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv4.conf.all.accept_source_route=0
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv4.conf.all.accept_source_route = 0
Rationale: | Source-routed packets allow the source of the packet to suggest routers forward the packet along a different path than configured on the router, which can be used to bypass network security measures. This requirement applies only to the forwarding of source-routerd traffic, such as when IPv4 forwarding is enabled and the system is functioning as a router. Accepting source-routed packets in the IPv4 protocol has few legitimate uses. It should be disabled unless it is absolutely required.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_all_accept_source_route
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 18, 2, 3, 4, 6, 7, 8, 9
cobit5 | APO01.06, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.2.3.4, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, 4.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, SR 7.6
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.12.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nerc-cip | CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1
nist | CM-7(a), CM-7(b), SC-5, CM-6(a), SC-7(a)
nist-csf | DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.IP-1, PR.PT-3, PR.PT-4
os-srg | SRG-OS-000480-GPOS-00227
anssi | R12
cis | 3.3.8
stigref | SV-257959r1155724_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv4.conf.all.accept_source_route%3D0%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_all_accept_source_route.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv4.conf.all.accept_source_route from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.accept_source_route.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv4.conf.all.accept_source_route" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv4_conf_all_accept_source_route.conf'

    sysctl_net_ipv4_conf_all_accept_source_route_value='0'

    #
    # Set runtime for net.ipv4.conf.all.accept_source_route
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv4.conf.all.accept_source_route="$sysctl_net_ipv4_conf_all_accept_source_route_value"
    fi

    #
    # If net.ipv4.conf.all.accept_source_route present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv4.conf.all.accept_source_route = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.accept_source_route")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_all_accept_source_route_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.accept_source_route\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.accept_source_route\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-253020
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_accept_source_route
    - name: XCCDF Value sysctl_net_ipv4_conf_all_accept_source_route_value # promote to variable
      set_fact:
        sysctl_net_ipv4_conf_all_accept_source_route_value: !!str 0
      tags:
        - always

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv4 Interfaces
        - Set fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253020
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv4 Interfaces
        - Find all files that contain net.ipv4.conf.all.accept_source_route
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.all.accept_source_route\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253020
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv4 Interfaces
        - Find all files that set net.ipv4.conf.all.accept_source_route to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.all.accept_source_route\s*=\s*{{ sysctl_net_ipv4_conf_all_accept_source_route_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253020
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv4 Interfaces
        - Comment out any occurrences of net.ipv4.conf.all.accept_source_route from config
        files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv4.conf.all.accept_source_route
        replace: '#net.ipv4.conf.all.accept_source_route'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - DISA-STIG-RHEL-09-253020
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv4 Interfaces
        - Comment out any occurrences of net.ipv4.conf.all.accept_source_route from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv4.conf.all.accept_source_route
        replace: '#net.ipv4.conf.all.accept_source_route'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253020
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on all IPv4 Interfaces
        - Ensure sysctl net.ipv4.conf.all.accept_source_route is set
      ansible.posix.sysctl:
        name: net.ipv4.conf.all.accept_source_route
        value: '{{ sysctl_net_ipv4_conf_all_accept_source_route_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv4_conf_all_accept_source_route.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253020
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_accept_source_route
#### Rule Enable Kernel Parameter to Log Martian Packets on all IPv4 Interfaces [ref]
To set the runtime status of the `net.ipv4.conf.all.log_martians` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv4.conf.all.log_martians=1
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv4.conf.all.log_martians = 1
Rationale: | The presence of "martian" packets (which have impossible addresses) as well as spoofed packets, source-routed packets, and redirects could be a sign of nefarious network activity. Logging these packets enables this activity to be detected.
Severity: | unknown
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_all_log_martians
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 2, 3, 7, 8, 9
cobit5 | APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS01.04, DSS03.05, DSS05.02, DSS05.03, DSS05.05, DSS05.07, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, SR 7.6
iso27001-2013 | A.11.2.6, A.12.1.2, A.12.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.6.2.1, A.6.2.2, A.9.1.2
nist | CM-7(a), CM-7(b), SC-5(3)(a)
nist-csf | DE.CM-1, PR.AC-3, PR.DS-4, PR.IP-1, PR.PT-3, PR.PT-4
os-srg | SRG-OS-000480-GPOS-00227
cis | 3.3.9
stigref | SV-257960r1155727_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv4.conf.all.log_martians%3D1%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_all_log_martians.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv4.conf.all.log_martians from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.log_martians.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv4.conf.all.log_martians" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv4_conf_all_log_martians.conf'

    sysctl_net_ipv4_conf_all_log_martians_value='1'

    #
    # Set runtime for net.ipv4.conf.all.log_martians
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv4.conf.all.log_martians="$sysctl_net_ipv4_conf_all_log_martians_value"
    fi

    #
    # If net.ipv4.conf.all.log_martians present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv4.conf.all.log_martians = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.log_martians")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_all_log_martians_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.log_martians\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.log_martians\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-253025
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(3)(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_conf_all_log_martians
      - unknown_severity
    - name: XCCDF Value sysctl_net_ipv4_conf_all_log_martians_value # promote to variable
      set_fact:
        sysctl_net_ipv4_conf_all_log_martians_value: !!str 1
      tags:
        - always

    - name: Enable Kernel Parameter to Log Martian Packets on all IPv4 Interfaces - Set
        fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253025
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(3)(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_conf_all_log_martians
      - unknown_severity

    - name: Enable Kernel Parameter to Log Martian Packets on all IPv4 Interfaces - Find
        all files that contain net.ipv4.conf.all.log_martians
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.all.log_martians\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253025
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(3)(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_conf_all_log_martians
      - unknown_severity

    - name: Enable Kernel Parameter to Log Martian Packets on all IPv4 Interfaces - Find
        all files that set net.ipv4.conf.all.log_martians to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.all.log_martians\s*=\s*{{ sysctl_net_ipv4_conf_all_log_martians_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253025
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(3)(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_conf_all_log_martians
      - unknown_severity

    - name: Enable Kernel Parameter to Log Martian Packets on all IPv4 Interfaces - Comment
        out any occurrences of net.ipv4.conf.all.log_martians from config files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv4.conf.all.log_martians
        replace: '#net.ipv4.conf.all.log_martians'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - DISA-STIG-RHEL-09-253025
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(3)(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_conf_all_log_martians
      - unknown_severity

    - name: Enable Kernel Parameter to Log Martian Packets on all IPv4 Interfaces - Comment
        out any occurrences of net.ipv4.conf.all.log_martians from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv4.conf.all.log_martians
        replace: '#net.ipv4.conf.all.log_martians'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253025
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(3)(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_conf_all_log_martians
      - unknown_severity

    - name: Enable Kernel Parameter to Log Martian Packets on all IPv4 Interfaces - Ensure
        sysctl net.ipv4.conf.all.log_martians is set
      ansible.posix.sysctl:
        name: net.ipv4.conf.all.log_martians
        value: '{{ sysctl_net_ipv4_conf_all_log_martians_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv4_conf_all_log_martians.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253025
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(3)(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_conf_all_log_martians
      - unknown_severity
#### Rule Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces [ref]
To set the runtime status of the `net.ipv4.conf.all.rp_filter` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv4.conf.all.rp_filter=1
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv4.conf.all.rp_filter = 1
Rationale: | Enabling reverse path filtering drops packets with source addresses that should not have been able to be received on the interface they were received on. It should not be used on systems which are routers for complicated networks, but is helpful for end hosts and routers serving small networks.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_all_rp_filter
References: |
cis-csc | 1, 12, 13, 14, 15, 16, 18, 2, 4, 6, 7, 8, 9
cobit5 | APO01.06, APO13.01, BAI04.04, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.07, DSS06.02
cui | 3.1.20
isa-62443-2009 | 4.2.3.4, 4.3.3.4, 4.4.3.3
isa-62443-2013 | SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, SR 7.6
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.12.1.3, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nist | CM-7(a), CM-7(b), CM-6(a), SC-7(a)
nist-csf | DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.PT-4
pcidss | Req-1.4.3
os-srg | SRG-OS-000480-GPOS-00227
anssi | R12
pcidss4 | 1.4.3, 1.4
cis | 3.3.7
stigref | SV-257962r1155733_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv4.conf.all.rp_filter%3D1%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_all_rp_filter.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv4.conf.all.rp_filter from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.rp_filter.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv4.conf.all.rp_filter" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv4_conf_all_rp_filter.conf'

    sysctl_net_ipv4_conf_all_rp_filter_value='1'

    #
    # Set runtime for net.ipv4.conf.all.rp_filter
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv4.conf.all.rp_filter="$sysctl_net_ipv4_conf_all_rp_filter_value"
    fi

    #
    # If net.ipv4.conf.all.rp_filter present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv4.conf.all.rp_filter = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.rp_filter")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_all_rp_filter_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.rp_filter\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.rp_filter\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-253035
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_rp_filter
    - name: XCCDF Value sysctl_net_ipv4_conf_all_rp_filter_value # promote to variable
      set_fact:
        sysctl_net_ipv4_conf_all_rp_filter_value: !!str 1
      tags:
        - always

    - name: Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces
        - Set fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253035
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_rp_filter

    - name: Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces
        - Find all files that contain net.ipv4.conf.all.rp_filter
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.all.rp_filter\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253035
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_rp_filter

    - name: Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces
        - Find all files that set net.ipv4.conf.all.rp_filter to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.all.rp_filter\s*=\s*{{ sysctl_net_ipv4_conf_all_rp_filter_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253035
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_rp_filter

    - name: Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces
        - Comment out any occurrences of net.ipv4.conf.all.rp_filter from config files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv4.conf.all.rp_filter
        replace: '#net.ipv4.conf.all.rp_filter'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - DISA-STIG-RHEL-09-253035
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_rp_filter

    - name: Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces
        - Comment out any occurrences of net.ipv4.conf.all.rp_filter from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv4.conf.all.rp_filter
        replace: '#net.ipv4.conf.all.rp_filter'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253035
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_rp_filter

    - name: Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces
        - Ensure sysctl net.ipv4.conf.all.rp_filter is set
      ansible.posix.sysctl:
        name: net.ipv4.conf.all.rp_filter
        value: '{{ sysctl_net_ipv4_conf_all_rp_filter_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv4_conf_all_rp_filter.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253035
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_rp_filter
#### Rule Disable Kernel Parameter for Accepting Secure ICMP Redirects on all IPv4 Interfaces [ref]
To set the runtime status of the `net.ipv4.conf.all.secure_redirects` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv4.conf.all.secure_redirects=0
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv4.conf.all.secure_redirects = 0
Rationale: | Accepting "secure" ICMP redirects (from those gateways listed as default gateways) has few legitimate uses. It should be disabled unless it is absolutely required.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_all_secure_redirects
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 18, 2, 3, 4, 6, 7, 8, 9
cobit5 | APO01.06, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.2.3.4, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, 4.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, SR 7.6
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.12.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nist | CM-7(a), CM-7(b), CM-6(a), SC-7(a)
nist-csf | DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.IP-1, PR.PT-3, PR.PT-4
pcidss | Req-1.4.3
os-srg | SRG-OS-000480-GPOS-00227
anssi | R12
pcidss4 | 1.4.3, 1.4
cis | 3.3.6
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv4.conf.all.secure_redirects%3D0%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_all_secure_redirects.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv4.conf.all.secure_redirects from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.secure_redirects.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv4.conf.all.secure_redirects" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv4_conf_all_secure_redirects.conf'

    sysctl_net_ipv4_conf_all_secure_redirects_value='0'

    #
    # Set runtime for net.ipv4.conf.all.secure_redirects
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv4.conf.all.secure_redirects="$sysctl_net_ipv4_conf_all_secure_redirects_value"
    fi

    #
    # If net.ipv4.conf.all.secure_redirects present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv4.conf.all.secure_redirects = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.secure_redirects")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_all_secure_redirects_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.secure_redirects\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.secure_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_secure_redirects
    - name: XCCDF Value sysctl_net_ipv4_conf_all_secure_redirects_value # promote to variable
      set_fact:
        sysctl_net_ipv4_conf_all_secure_redirects_value: !!str 0
      tags:
        - always

    - name: Disable Kernel Parameter for Accepting Secure ICMP Redirects on all IPv4 Interfaces
        - Set fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_secure_redirects

    - name: Disable Kernel Parameter for Accepting Secure ICMP Redirects on all IPv4 Interfaces
        - Find all files that contain net.ipv4.conf.all.secure_redirects
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.all.secure_redirects\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_secure_redirects

    - name: Disable Kernel Parameter for Accepting Secure ICMP Redirects on all IPv4 Interfaces
        - Find all files that set net.ipv4.conf.all.secure_redirects to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.all.secure_redirects\s*=\s*{{ sysctl_net_ipv4_conf_all_secure_redirects_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_secure_redirects

    - name: Disable Kernel Parameter for Accepting Secure ICMP Redirects on all IPv4 Interfaces
        - Comment out any occurrences of net.ipv4.conf.all.secure_redirects from config
        files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv4.conf.all.secure_redirects
        replace: '#net.ipv4.conf.all.secure_redirects'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_secure_redirects

    - name: Disable Kernel Parameter for Accepting Secure ICMP Redirects on all IPv4 Interfaces
        - Comment out any occurrences of net.ipv4.conf.all.secure_redirects from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv4.conf.all.secure_redirects
        replace: '#net.ipv4.conf.all.secure_redirects'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_secure_redirects

    - name: Disable Kernel Parameter for Accepting Secure ICMP Redirects on all IPv4 Interfaces
        - Ensure sysctl net.ipv4.conf.all.secure_redirects is set
      ansible.posix.sysctl:
        name: net.ipv4.conf.all.secure_redirects
        value: '{{ sysctl_net_ipv4_conf_all_secure_redirects_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv4_conf_all_secure_redirects.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_secure_redirects
#### Rule Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv4 Interfaces [ref]
To set the runtime status of the `net.ipv4.conf.default.accept_redirects` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv4.conf.default.accept_redirects=0
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv4.conf.default.accept_redirects = 0
Rationale: | ICMP redirect messages are used by routers to inform hosts that a more direct route exists for a particular destination. These messages modify the host's route table and are unauthenticated. An illicit ICMP redirect message could result in a man-in-the-middle attack. This feature of the IPv4 protocol has few legitimate uses. It should be disabled unless absolutely required.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_default_accept_redirects
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 18, 2, 3, 4, 6, 7, 8, 9
cjis | 5.10.1.1
cobit5 | APO01.06, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.2.3.4, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, 4.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, SR 7.6
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.12.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nist | CM-7(a), CM-7(b), CM-6(a), SC-7(a)
nist-csf | DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.IP-1, PR.PT-3, PR.PT-4
pcidss | Req-1.4.3
os-srg | SRG-OS-000480-GPOS-00227
anssi | R12
pcidss4 | 1.4.3, 1.4
cis | 3.3.5
stigref | SV-257963r1155736_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv4.conf.default.accept_redirects%3D0%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_default_accept_redirects.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv4.conf.default.accept_redirects from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.default.accept_redirects.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv4.conf.default.accept_redirects" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv4_conf_default_accept_redirects.conf'

    sysctl_net_ipv4_conf_default_accept_redirects_value='0'

    #
    # Set runtime for net.ipv4.conf.default.accept_redirects
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv4.conf.default.accept_redirects="$sysctl_net_ipv4_conf_default_accept_redirects_value"
    fi

    #
    # If net.ipv4.conf.default.accept_redirects present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv4.conf.default.accept_redirects = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.default.accept_redirects")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_default_accept_redirects_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.default.accept_redirects\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.default.accept_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253040
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_accept_redirects
    - name: XCCDF Value sysctl_net_ipv4_conf_default_accept_redirects_value # promote to variable
      set_fact:
        sysctl_net_ipv4_conf_default_accept_redirects_value: !!str 0
      tags:
        - always

    - name: Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv4 Interfaces
        - Set fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253040
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_accept_redirects

    - name: Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv4 Interfaces
        - Find all files that contain net.ipv4.conf.default.accept_redirects
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.default.accept_redirects\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253040
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_accept_redirects

    - name: Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv4 Interfaces
        - Find all files that set net.ipv4.conf.default.accept_redirects to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.default.accept_redirects\s*=\s*{{ sysctl_net_ipv4_conf_default_accept_redirects_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253040
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_accept_redirects

    - name: Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv4 Interfaces
        - Comment out any occurrences of net.ipv4.conf.default.accept_redirects from config
        files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv4.conf.default.accept_redirects
        replace: '#net.ipv4.conf.default.accept_redirects'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253040
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_accept_redirects

    - name: Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv4 Interfaces
        - Comment out any occurrences of net.ipv4.conf.default.accept_redirects from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv4.conf.default.accept_redirects
        replace: '#net.ipv4.conf.default.accept_redirects'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253040
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_accept_redirects

    - name: Disable Kernel Parameter for Accepting ICMP Redirects by Default on IPv4 Interfaces
        - Ensure sysctl net.ipv4.conf.default.accept_redirects is set
      ansible.posix.sysctl:
        name: net.ipv4.conf.default.accept_redirects
        value: '{{ sysctl_net_ipv4_conf_default_accept_redirects_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv4_conf_default_accept_redirects.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253040
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_accept_redirects
#### Rule Disable Kernel Parameter for Accepting Source-Routed Packets on IPv4 Interfaces by Default [ref]
To set the runtime status of the `net.ipv4.conf.default.accept_source_route` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv4.conf.default.accept_source_route=0
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv4.conf.default.accept_source_route = 0
Rationale: | Source-routed packets allow the source of the packet to suggest routers forward the packet along a different path than configured on the router, which can be used to bypass network security measures. Accepting source-routed packets in the IPv4 protocol has few legitimate uses. It should be disabled unless it is absolutely required, such as when IPv4 forwarding is enabled and the system is legitimately functioning as a router.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_default_accept_source_route
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 18, 2, 3, 4, 6, 7, 8, 9
cjis | 5.10.1.1
cobit5 | APO01.06, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.2.3.4, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, 4.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, SR 7.6
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.12.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nerc-cip | CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1
nist | CM-7(a), CM-7(b), SC-5, SC-7(a)
nist-csf | DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.IP-1, PR.PT-3, PR.PT-4
os-srg | SRG-OS-000480-GPOS-00227
anssi | R12
cis | 3.3.8
stigref | SV-257964r1155739_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv4.conf.default.accept_source_route%3D0%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_default_accept_source_route.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv4.conf.default.accept_source_route from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.default.accept_source_route.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv4.conf.default.accept_source_route" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv4_conf_default_accept_source_route.conf'

    sysctl_net_ipv4_conf_default_accept_source_route_value='0'

    #
    # Set runtime for net.ipv4.conf.default.accept_source_route
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv4.conf.default.accept_source_route="$sysctl_net_ipv4_conf_default_accept_source_route_value"
    fi

    #
    # If net.ipv4.conf.default.accept_source_route present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv4.conf.default.accept_source_route = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.default.accept_source_route")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_default_accept_source_route_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.default.accept_source_route\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.default.accept_source_route\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253045
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_accept_source_route
    - name: XCCDF Value sysctl_net_ipv4_conf_default_accept_source_route_value # promote to variable
      set_fact:
        sysctl_net_ipv4_conf_default_accept_source_route_value: !!str 0
      tags:
        - always

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on IPv4 Interfaces
        by Default - Set fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253045
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on IPv4 Interfaces
        by Default - Find all files that contain net.ipv4.conf.default.accept_source_route
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.default.accept_source_route\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253045
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on IPv4 Interfaces
        by Default - Find all files that set net.ipv4.conf.default.accept_source_route
        to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.default.accept_source_route\s*=\s*{{ sysctl_net_ipv4_conf_default_accept_source_route_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253045
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on IPv4 Interfaces
        by Default - Comment out any occurrences of net.ipv4.conf.default.accept_source_route
        from config files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv4.conf.default.accept_source_route
        replace: '#net.ipv4.conf.default.accept_source_route'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253045
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on IPv4 Interfaces
        by Default - Comment out any occurrences of net.ipv4.conf.default.accept_source_route
        from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv4.conf.default.accept_source_route
        replace: '#net.ipv4.conf.default.accept_source_route'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253045
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_accept_source_route

    - name: Disable Kernel Parameter for Accepting Source-Routed Packets on IPv4 Interfaces
        by Default - Ensure sysctl net.ipv4.conf.default.accept_source_route is set
      ansible.posix.sysctl:
        name: net.ipv4.conf.default.accept_source_route
        value: '{{ sysctl_net_ipv4_conf_default_accept_source_route_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv4_conf_default_accept_source_route.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253045
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_accept_source_route
#### Rule Enable Kernel Parameter to Log Martian Packets on all IPv4 Interfaces by Default [ref]
To set the runtime status of the `net.ipv4.conf.default.log_martians` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv4.conf.default.log_martians=1
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv4.conf.default.log_martians = 1
Rationale: | The presence of "martian" packets (which have impossible addresses) as well as spoofed packets, source-routed packets, and redirects could be a sign of nefarious network activity. Logging these packets enables this activity to be detected.
Severity: | unknown
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_default_log_martians
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 2, 3, 7, 8, 9
cobit5 | APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS01.04, DSS03.05, DSS05.02, DSS05.03, DSS05.05, DSS05.07, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, SR 7.6
iso27001-2013 | A.11.2.6, A.12.1.2, A.12.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.6.2.1, A.6.2.2, A.9.1.2
nist | CM-7(a), CM-7(b), SC-5(3)(a)
nist-csf | DE.CM-1, PR.AC-3, PR.DS-4, PR.IP-1, PR.PT-3, PR.PT-4
os-srg | SRG-OS-000480-GPOS-00227
cis | 3.3.9
stigref | SV-257961r1155730_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv4.conf.default.log_martians%3D1%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_default_log_martians.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv4.conf.default.log_martians from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.default.log_martians.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv4.conf.default.log_martians" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv4_conf_default_log_martians.conf'

    sysctl_net_ipv4_conf_default_log_martians_value='1'

    #
    # Set runtime for net.ipv4.conf.default.log_martians
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv4.conf.default.log_martians="$sysctl_net_ipv4_conf_default_log_martians_value"
    fi

    #
    # If net.ipv4.conf.default.log_martians present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv4.conf.default.log_martians = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.default.log_martians")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_default_log_martians_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.default.log_martians\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.default.log_martians\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-253030
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(3)(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_conf_default_log_martians
      - unknown_severity
    - name: XCCDF Value sysctl_net_ipv4_conf_default_log_martians_value # promote to variable
      set_fact:
        sysctl_net_ipv4_conf_default_log_martians_value: !!str 1
      tags:
        - always

    - name: Enable Kernel Parameter to Log Martian Packets on all IPv4 Interfaces by Default
        - Set fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253030
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(3)(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_conf_default_log_martians
      - unknown_severity

    - name: Enable Kernel Parameter to Log Martian Packets on all IPv4 Interfaces by Default
        - Find all files that contain net.ipv4.conf.default.log_martians
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.default.log_martians\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253030
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(3)(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_conf_default_log_martians
      - unknown_severity

    - name: Enable Kernel Parameter to Log Martian Packets on all IPv4 Interfaces by Default
        - Find all files that set net.ipv4.conf.default.log_martians to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.default.log_martians\s*=\s*{{ sysctl_net_ipv4_conf_default_log_martians_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253030
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(3)(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_conf_default_log_martians
      - unknown_severity

    - name: Enable Kernel Parameter to Log Martian Packets on all IPv4 Interfaces by Default
        - Comment out any occurrences of net.ipv4.conf.default.log_martians from config
        files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv4.conf.default.log_martians
        replace: '#net.ipv4.conf.default.log_martians'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - DISA-STIG-RHEL-09-253030
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(3)(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_conf_default_log_martians
      - unknown_severity

    - name: Enable Kernel Parameter to Log Martian Packets on all IPv4 Interfaces by Default
        - Comment out any occurrences of net.ipv4.conf.default.log_martians from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv4.conf.default.log_martians
        replace: '#net.ipv4.conf.default.log_martians'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253030
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(3)(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_conf_default_log_martians
      - unknown_severity

    - name: Enable Kernel Parameter to Log Martian Packets on all IPv4 Interfaces by Default
        - Ensure sysctl net.ipv4.conf.default.log_martians is set
      ansible.posix.sysctl:
        name: net.ipv4.conf.default.log_martians
        value: '{{ sysctl_net_ipv4_conf_default_log_martians_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv4_conf_default_log_martians.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253030
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(3)(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_conf_default_log_martians
      - unknown_severity
#### Rule Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces by Default [ref]
To set the runtime status of the `net.ipv4.conf.default.rp_filter` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv4.conf.default.rp_filter=1
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv4.conf.default.rp_filter = 1
Rationale: | Enabling reverse path filtering drops packets with source addresses that should not have been able to be received on the interface they were received on. It should not be used on systems which are routers for complicated networks, but is helpful for end hosts and routers serving small networks.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_default_rp_filter
References: |
cis-csc | 1, 12, 13, 14, 15, 16, 18, 2, 4, 6, 7, 8, 9
cobit5 | APO01.06, APO13.01, BAI04.04, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.07, DSS06.02
cui | 3.1.20
isa-62443-2009 | 4.2.3.4, 4.3.3.4, 4.4.3.3
isa-62443-2013 | SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, SR 7.6
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.12.1.3, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nist | CM-7(a), CM-7(b), CM-6(a), SC-7(a)
nist-csf | DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.PT-4
os-srg | SRG-OS-000480-GPOS-00227
anssi | R12
cis | 3.3.7
stigref | SV-257965r1155646_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv4.conf.default.rp_filter%3D1%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_default_rp_filter.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv4.conf.default.rp_filter from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.default.rp_filter.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv4.conf.default.rp_filter" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv4_conf_default_rp_filter.conf'

    sysctl_net_ipv4_conf_default_rp_filter_value='1'

    #
    # Set runtime for net.ipv4.conf.default.rp_filter
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv4.conf.default.rp_filter="$sysctl_net_ipv4_conf_default_rp_filter_value"
    fi

    #
    # If net.ipv4.conf.default.rp_filter present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv4.conf.default.rp_filter = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.default.rp_filter")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_default_rp_filter_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.default.rp_filter\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.default.rp_filter\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-253050
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_rp_filter
    - name: XCCDF Value sysctl_net_ipv4_conf_default_rp_filter_value # promote to variable
      set_fact:
        sysctl_net_ipv4_conf_default_rp_filter_value: !!str 1
      tags:
        - always

    - name: Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces
        by Default - Set fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253050
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_rp_filter

    - name: Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces
        by Default - Find all files that contain net.ipv4.conf.default.rp_filter
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.default.rp_filter\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253050
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_rp_filter

    - name: Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces
        by Default - Find all files that set net.ipv4.conf.default.rp_filter to correct
        value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.default.rp_filter\s*=\s*{{ sysctl_net_ipv4_conf_default_rp_filter_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253050
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_rp_filter

    - name: Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces
        by Default - Comment out any occurrences of net.ipv4.conf.default.rp_filter from
        config files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv4.conf.default.rp_filter
        replace: '#net.ipv4.conf.default.rp_filter'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - DISA-STIG-RHEL-09-253050
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_rp_filter

    - name: Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces
        by Default - Comment out any occurrences of net.ipv4.conf.default.rp_filter from
        /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv4.conf.default.rp_filter
        replace: '#net.ipv4.conf.default.rp_filter'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253050
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_rp_filter

    - name: Enable Kernel Parameter to Use Reverse Path Filtering on all IPv4 Interfaces
        by Default - Ensure sysctl net.ipv4.conf.default.rp_filter is set
      ansible.posix.sysctl:
        name: net.ipv4.conf.default.rp_filter
        value: '{{ sysctl_net_ipv4_conf_default_rp_filter_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv4_conf_default_rp_filter.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253050
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_rp_filter
#### Rule Configure Kernel Parameter for Accepting Secure Redirects By Default [ref]
To set the runtime status of the `net.ipv4.conf.default.secure_redirects` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv4.conf.default.secure_redirects=0
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv4.conf.default.secure_redirects = 0
Rationale: | Accepting "secure" ICMP redirects (from those gateways listed as default gateways) has few legitimate uses. It should be disabled unless it is absolutely required.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_default_secure_redirects
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 18, 2, 3, 4, 6, 7, 8, 9
cobit5 | APO01.06, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.2.3.4, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, 4.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, SR 7.6
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.12.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nerc-cip | CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1
nist | CM-7(a), CM-7(b), SC-5, SC-7(a)
nist-csf | DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.IP-1, PR.PT-3, PR.PT-4
os-srg | SRG-OS-000480-GPOS-00227
anssi | R12
cis | 3.3.6
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv4.conf.default.secure_redirects%3D0%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_default_secure_redirects.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv4.conf.default.secure_redirects from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.default.secure_redirects.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv4.conf.default.secure_redirects" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv4_conf_default_secure_redirects.conf'

    sysctl_net_ipv4_conf_default_secure_redirects_value='0'

    #
    # Set runtime for net.ipv4.conf.default.secure_redirects
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv4.conf.default.secure_redirects="$sysctl_net_ipv4_conf_default_secure_redirects_value"
    fi

    #
    # If net.ipv4.conf.default.secure_redirects present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv4.conf.default.secure_redirects = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.default.secure_redirects")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_conf_default_secure_redirects_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.default.secure_redirects\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.default.secure_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_secure_redirects
    - name: XCCDF Value sysctl_net_ipv4_conf_default_secure_redirects_value # promote to variable
      set_fact:
        sysctl_net_ipv4_conf_default_secure_redirects_value: !!str 0
      tags:
        - always

    - name: Configure Kernel Parameter for Accepting Secure Redirects By Default - Set
        fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_secure_redirects

    - name: Configure Kernel Parameter for Accepting Secure Redirects By Default - Find
        all files that contain net.ipv4.conf.default.secure_redirects
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.default.secure_redirects\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_secure_redirects

    - name: Configure Kernel Parameter for Accepting Secure Redirects By Default - Find
        all files that set net.ipv4.conf.default.secure_redirects to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.default.secure_redirects\s*=\s*{{ sysctl_net_ipv4_conf_default_secure_redirects_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_secure_redirects

    - name: Configure Kernel Parameter for Accepting Secure Redirects By Default - Comment
        out any occurrences of net.ipv4.conf.default.secure_redirects from config files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv4.conf.default.secure_redirects
        replace: '#net.ipv4.conf.default.secure_redirects'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_secure_redirects

    - name: Configure Kernel Parameter for Accepting Secure Redirects By Default - Comment
        out any occurrences of net.ipv4.conf.default.secure_redirects from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv4.conf.default.secure_redirects
        replace: '#net.ipv4.conf.default.secure_redirects'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_secure_redirects

    - name: Configure Kernel Parameter for Accepting Secure Redirects By Default - Ensure
        sysctl net.ipv4.conf.default.secure_redirects is set
      ansible.posix.sysctl:
        name: net.ipv4.conf.default.secure_redirects
        value: '{{ sysctl_net_ipv4_conf_default_secure_redirects_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv4_conf_default_secure_redirects.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_secure_redirects
#### Rule Enable Kernel Parameter to Ignore ICMP Broadcast Echo Requests on IPv4 Interfaces [ref]
To set the runtime status of the `net.ipv4.icmp_echo_ignore_broadcasts` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv4.icmp_echo_ignore_broadcasts=1
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv4.icmp_echo_ignore_broadcasts = 1
Rationale: | Responding to broadcast (ICMP) echoes facilitates network mapping and provides a vector for amplification attacks. Ignoring ICMP echo requests (pings) sent to broadcast or multicast addresses makes the system slightly more difficult to enumerate on the network.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_icmp_echo_ignore_broadcasts
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 18, 2, 3, 4, 6, 7, 8, 9
cjis | 5.10.1.1
cobit5 | APO01.06, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.2.3.4, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, 4.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, SR 7.6
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.12.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nerc-cip | CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1
nist | CM-7(a), CM-7(b), SC-5
nist-csf | DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.IP-1, PR.PT-3, PR.PT-4
pcidss | Req-1.4.3
os-srg | SRG-OS-000480-GPOS-00227
pcidss4 | 1.4.2, 1.4
cis | 3.3.4
stigref | SV-257966r1155742_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv4.icmp_echo_ignore_broadcasts%3D1%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv4_icmp_echo_ignore_broadcasts.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv4.icmp_echo_ignore_broadcasts from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.icmp_echo_ignore_broadcasts.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv4.icmp_echo_ignore_broadcasts" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv4_icmp_echo_ignore_broadcasts.conf'

    sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value='1'

    #
    # Set runtime for net.ipv4.icmp_echo_ignore_broadcasts
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv4.icmp_echo_ignore_broadcasts="$sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value"
    fi

    #
    # If net.ipv4.icmp_echo_ignore_broadcasts present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv4.icmp_echo_ignore_broadcasts = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.icmp_echo_ignore_broadcasts")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.icmp_echo_ignore_broadcasts\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.icmp_echo_ignore_broadcasts\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253055
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_icmp_echo_ignore_broadcasts
    - name: XCCDF Value sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value # promote to variable
      set_fact:
        sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value: !!str 1
      tags:
        - always

    - name: Enable Kernel Parameter to Ignore ICMP Broadcast Echo Requests on IPv4 Interfaces
        - Set fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253055
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_icmp_echo_ignore_broadcasts

    - name: Enable Kernel Parameter to Ignore ICMP Broadcast Echo Requests on IPv4 Interfaces
        - Find all files that contain net.ipv4.icmp_echo_ignore_broadcasts
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.icmp_echo_ignore_broadcasts\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253055
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_icmp_echo_ignore_broadcasts

    - name: Enable Kernel Parameter to Ignore ICMP Broadcast Echo Requests on IPv4 Interfaces
        - Find all files that set net.ipv4.icmp_echo_ignore_broadcasts to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.icmp_echo_ignore_broadcasts\s*=\s*{{ sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253055
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_icmp_echo_ignore_broadcasts

    - name: Enable Kernel Parameter to Ignore ICMP Broadcast Echo Requests on IPv4 Interfaces
        - Comment out any occurrences of net.ipv4.icmp_echo_ignore_broadcasts from config
        files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv4.icmp_echo_ignore_broadcasts
        replace: '#net.ipv4.icmp_echo_ignore_broadcasts'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253055
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_icmp_echo_ignore_broadcasts

    - name: Enable Kernel Parameter to Ignore ICMP Broadcast Echo Requests on IPv4 Interfaces
        - Comment out any occurrences of net.ipv4.icmp_echo_ignore_broadcasts from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv4.icmp_echo_ignore_broadcasts
        replace: '#net.ipv4.icmp_echo_ignore_broadcasts'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253055
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_icmp_echo_ignore_broadcasts

    - name: Enable Kernel Parameter to Ignore ICMP Broadcast Echo Requests on IPv4 Interfaces
        - Ensure sysctl net.ipv4.icmp_echo_ignore_broadcasts is set
      ansible.posix.sysctl:
        name: net.ipv4.icmp_echo_ignore_broadcasts
        value: '{{ sysctl_net_ipv4_icmp_echo_ignore_broadcasts_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv4_icmp_echo_ignore_broadcasts.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253055
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_icmp_echo_ignore_broadcasts
#### Rule Enable Kernel Parameter to Ignore Bogus ICMP Error Responses on IPv4 Interfaces [ref]
To set the runtime status of the `net.ipv4.icmp_ignore_bogus_error_responses` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv4.icmp_ignore_bogus_error_responses=1
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv4.icmp_ignore_bogus_error_responses = 1
Rationale: | Ignoring bogus ICMP error responses reduces log size, although some activity would not be logged.
Severity: | unknown
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_icmp_ignore_bogus_error_responses
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 2, 3, 7, 8, 9
cobit5 | APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS03.05, DSS05.02, DSS05.05, DSS05.07, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 6.2, SR 7.1, SR 7.2, SR 7.6
iso27001-2013 | A.12.1.2, A.12.1.3, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.9.1.2
nerc-cip | CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1
nist | CM-7(a), CM-7(b), SC-5
nist-csf | DE.CM-1, PR.DS-4, PR.IP-1, PR.PT-3
pcidss | Req-1.4.3
os-srg | SRG-OS-000480-GPOS-00227
anssi | R12
pcidss4 | 1.4.2, 1.4
cis | 3.3.3
stigref | SV-257967r1155745_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv4.icmp_ignore_bogus_error_responses%3D1%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv4_icmp_ignore_bogus_error_responses.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv4.icmp_ignore_bogus_error_responses from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.icmp_ignore_bogus_error_responses.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv4.icmp_ignore_bogus_error_responses" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv4_icmp_ignore_bogus_error_responses.conf'

    sysctl_net_ipv4_icmp_ignore_bogus_error_responses_value='1'

    #
    # Set runtime for net.ipv4.icmp_ignore_bogus_error_responses
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv4.icmp_ignore_bogus_error_responses="$sysctl_net_ipv4_icmp_ignore_bogus_error_responses_value"
    fi

    #
    # If net.ipv4.icmp_ignore_bogus_error_responses present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv4.icmp_ignore_bogus_error_responses = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.icmp_ignore_bogus_error_responses")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_icmp_ignore_bogus_error_responses_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.icmp_ignore_bogus_error_responses\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.icmp_ignore_bogus_error_responses\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-253060
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_icmp_ignore_bogus_error_responses
      - unknown_severity
    - name: XCCDF Value sysctl_net_ipv4_icmp_ignore_bogus_error_responses_value # promote to variable
      set_fact:
        sysctl_net_ipv4_icmp_ignore_bogus_error_responses_value: !!str 1
      tags:
        - always

    - name: Enable Kernel Parameter to Ignore Bogus ICMP Error Responses on IPv4 Interfaces
        - Set fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253060
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_icmp_ignore_bogus_error_responses
      - unknown_severity

    - name: Enable Kernel Parameter to Ignore Bogus ICMP Error Responses on IPv4 Interfaces
        - Find all files that contain net.ipv4.icmp_ignore_bogus_error_responses
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.icmp_ignore_bogus_error_responses\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253060
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_icmp_ignore_bogus_error_responses
      - unknown_severity

    - name: Enable Kernel Parameter to Ignore Bogus ICMP Error Responses on IPv4 Interfaces
        - Find all files that set net.ipv4.icmp_ignore_bogus_error_responses to correct
        value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.icmp_ignore_bogus_error_responses\s*=\s*{{ sysctl_net_ipv4_icmp_ignore_bogus_error_responses_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253060
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_icmp_ignore_bogus_error_responses
      - unknown_severity

    - name: Enable Kernel Parameter to Ignore Bogus ICMP Error Responses on IPv4 Interfaces
        - Comment out any occurrences of net.ipv4.icmp_ignore_bogus_error_responses from
        config files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv4.icmp_ignore_bogus_error_responses
        replace: '#net.ipv4.icmp_ignore_bogus_error_responses'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - DISA-STIG-RHEL-09-253060
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_icmp_ignore_bogus_error_responses
      - unknown_severity

    - name: Enable Kernel Parameter to Ignore Bogus ICMP Error Responses on IPv4 Interfaces
        - Comment out any occurrences of net.ipv4.icmp_ignore_bogus_error_responses from
        /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv4.icmp_ignore_bogus_error_responses
        replace: '#net.ipv4.icmp_ignore_bogus_error_responses'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253060
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_icmp_ignore_bogus_error_responses
      - unknown_severity

    - name: Enable Kernel Parameter to Ignore Bogus ICMP Error Responses on IPv4 Interfaces
        - Ensure sysctl net.ipv4.icmp_ignore_bogus_error_responses is set
      ansible.posix.sysctl:
        name: net.ipv4.icmp_ignore_bogus_error_responses
        value: '{{ sysctl_net_ipv4_icmp_ignore_bogus_error_responses_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv4_icmp_ignore_bogus_error_responses.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-253060
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - PCI-DSS-Req-1.4.3
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.2
      - disable_strategy
      - low_complexity
      - medium_disruption
      - reboot_required
      - sysctl_net_ipv4_icmp_ignore_bogus_error_responses
      - unknown_severity
#### Rule Enable Kernel Parameter to Use TCP Syncookies on Network Interfaces [ref]
To set the runtime status of the `net.ipv4.tcp_syncookies` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv4.tcp_syncookies=1
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv4.tcp_syncookies = 1
Rationale: | A TCP SYN flood attack can cause a denial of service by filling a system's TCP connection table with connections in the SYN_RCVD state. Syncookies can be used to track a connection when a subsequent ACK is received, verifying the initiator is attempting a valid connection and is not a flood source. This feature is activated when a flood condition is detected, and enables the system to continue servicing valid connection requests.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_tcp_syncookies
References: |
cis-csc | 1, 12, 13, 14, 15, 16, 18, 2, 4, 6, 7, 8, 9
cjis | 5.10.1.1
cobit5 | APO01.06, APO13.01, BAI04.04, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.07, DSS06.02
cui | 3.1.20
isa-62443-2009 | 4.2.3.4, 4.3.3.4, 4.4.3.3
isa-62443-2013 | SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, SR 7.6
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.12.1.3, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nist | CM-7(a), CM-7(b), SC-5(1), SC-5(2), SC-5(3)(a), CM-6(a)
nist-csf | DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.PT-4
pcidss | Req-1.4.1
os-srg | SRG-OS-000480-GPOS-00227, SRG-OS-000420-GPOS-00186, SRG-OS-000142-GPOS-00071
anssi | R12
pcidss4 | 1.4.3, 1.4
cis | 3.3.10
stigref | SV-257957r1155618_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv4.tcp_syncookies%3D1%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv4_tcp_syncookies.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv4.tcp_syncookies from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.tcp_syncookies.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv4.tcp_syncookies" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv4_tcp_syncookies.conf'

    sysctl_net_ipv4_tcp_syncookies_value='1'

    #
    # Set runtime for net.ipv4.tcp_syncookies
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv4.tcp_syncookies="$sysctl_net_ipv4_tcp_syncookies_value"
    fi

    #
    # If net.ipv4.tcp_syncookies present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "net.ipv4.tcp_syncookies = value" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.tcp_syncookies")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$sysctl_net_ipv4_tcp_syncookies_value"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.tcp_syncookies\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.tcp_syncookies\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253010
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(1)
      - NIST-800-53-SC-5(2)
      - NIST-800-53-SC-5(3)(a)
      - PCI-DSS-Req-1.4.1
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_tcp_syncookies
    - name: XCCDF Value sysctl_net_ipv4_tcp_syncookies_value # promote to variable
      set_fact:
        sysctl_net_ipv4_tcp_syncookies_value: !!str 1
      tags:
        - always

    - name: Enable Kernel Parameter to Use TCP Syncookies on Network Interfaces - Set
        fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253010
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(1)
      - NIST-800-53-SC-5(2)
      - NIST-800-53-SC-5(3)(a)
      - PCI-DSS-Req-1.4.1
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_tcp_syncookies

    - name: Enable Kernel Parameter to Use TCP Syncookies on Network Interfaces - Find
        all files that contain net.ipv4.tcp_syncookies
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.tcp_syncookies\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253010
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(1)
      - NIST-800-53-SC-5(2)
      - NIST-800-53-SC-5(3)(a)
      - PCI-DSS-Req-1.4.1
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_tcp_syncookies

    - name: Enable Kernel Parameter to Use TCP Syncookies on Network Interfaces - Find
        all files that set net.ipv4.tcp_syncookies to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.tcp_syncookies\s*=\s*{{ sysctl_net_ipv4_tcp_syncookies_value
          }}$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253010
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(1)
      - NIST-800-53-SC-5(2)
      - NIST-800-53-SC-5(3)(a)
      - PCI-DSS-Req-1.4.1
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_tcp_syncookies

    - name: Enable Kernel Parameter to Use TCP Syncookies on Network Interfaces - Comment
        out any occurrences of net.ipv4.tcp_syncookies from config files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv4.tcp_syncookies
        replace: '#net.ipv4.tcp_syncookies'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253010
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(1)
      - NIST-800-53-SC-5(2)
      - NIST-800-53-SC-5(3)(a)
      - PCI-DSS-Req-1.4.1
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_tcp_syncookies

    - name: Enable Kernel Parameter to Use TCP Syncookies on Network Interfaces - Comment
        out any occurrences of net.ipv4.tcp_syncookies from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv4.tcp_syncookies
        replace: '#net.ipv4.tcp_syncookies'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253010
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(1)
      - NIST-800-53-SC-5(2)
      - NIST-800-53-SC-5(3)(a)
      - PCI-DSS-Req-1.4.1
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_tcp_syncookies

    - name: Enable Kernel Parameter to Use TCP Syncookies on Network Interfaces - Ensure
        sysctl net.ipv4.tcp_syncookies is set
      ansible.posix.sysctl:
        name: net.ipv4.tcp_syncookies
        value: '{{ sysctl_net_ipv4_tcp_syncookies_value }}'
        sysctl_file: /etc/sysctl.d/net_ipv4_tcp_syncookies.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253010
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5(1)
      - NIST-800-53-SC-5(2)
      - NIST-800-53-SC-5(3)(a)
      - PCI-DSS-Req-1.4.1
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_tcp_syncookies
Group Network Parameters for Hosts Only Group contains 3 rules
[ref] If the system is not going to be used as a router, then setting certain kernel parameters ensure that the host will not perform routing of network traffic.
#### Rule Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces [ref]
To set the runtime status of the `net.ipv4.conf.all.send_redirects` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv4.conf.all.send_redirects=0
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv4.conf.all.send_redirects = 0
Rationale: | ICMP redirect messages are used by routers to inform hosts that a more direct route exists for a particular destination. These messages contain information from the system's route table possibly revealing portions of the network topology. The ability to send ICMP redirects is only appropriate for systems acting as routers.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_all_send_redirects
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 18, 2, 3, 4, 6, 7, 8, 9
cjis | 5.10.1.1
cobit5 | APO01.06, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.2.3.4, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, 4.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, SR 7.6
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.12.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nerc-cip | CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1
nist | CM-7(a), CM-7(b), SC-5, CM-6(a), SC-7(a)
nist-csf | DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.IP-1, PR.PT-3, PR.PT-4
os-srg | SRG-OS-000480-GPOS-00227
anssi | R12
pcidss4 | 1.4.5, 1.4
cis | 3.3.2
stigref | SV-257968r1155748_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv4.conf.all.send_redirects%3D0%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_all_send_redirects.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv4.conf.all.send_redirects from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.all.send_redirects.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv4.conf.all.send_redirects" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv4_conf_all_send_redirects.conf'

    #
    # Set runtime for net.ipv4.conf.all.send_redirects
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv4.conf.all.send_redirects="0"
    fi

    #
    # If net.ipv4.conf.all.send_redirects present in /etc/sysctl.conf, change value to "0"
    #	else, add "net.ipv4.conf.all.send_redirects = 0" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.all.send_redirects")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "0"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.all.send_redirects\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.all.send_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253065
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.5
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_send_redirects

    - name: Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces
        - Set fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253065
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.5
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_send_redirects

    - name: Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces
        - Find all files that contain net.ipv4.conf.all.send_redirects
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.all.send_redirects\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253065
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.5
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_send_redirects

    - name: Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces
        - Find all files that set net.ipv4.conf.all.send_redirects to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.all.send_redirects\s*=\s*0$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253065
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.5
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_send_redirects

    - name: Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces
        - Comment out any occurrences of net.ipv4.conf.all.send_redirects from config
        files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv4.conf.all.send_redirects
        replace: '#net.ipv4.conf.all.send_redirects'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253065
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.5
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_send_redirects

    - name: Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces
        - Comment out any occurrences of net.ipv4.conf.all.send_redirects from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv4.conf.all.send_redirects
        replace: '#net.ipv4.conf.all.send_redirects'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253065
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.5
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_send_redirects

    - name: Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces
        - Ensure sysctl net.ipv4.conf.all.send_redirects is set to 0
      ansible.posix.sysctl:
        name: net.ipv4.conf.all.send_redirects
        value: '0'
        sysctl_file: /etc/sysctl.d/net_ipv4_conf_all_send_redirects.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253065
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.5
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_all_send_redirects
#### Rule Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces by Default [ref]
To set the runtime status of the `net.ipv4.conf.default.send_redirects` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv4.conf.default.send_redirects=0
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv4.conf.default.send_redirects = 0
Rationale: | ICMP redirect messages are used by routers to inform hosts that a more direct route exists for a particular destination. These messages contain information from the system's route table possibly revealing portions of the network topology. The ability to send ICMP redirects is only appropriate for systems acting as routers.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_conf_default_send_redirects
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 18, 2, 3, 4, 6, 7, 8, 9
cjis | 5.10.1.1
cobit5 | APO01.06, APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS01.05, DSS03.01, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.2.3.4, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, 4.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, SR 7.6
iso27001-2013 | A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.12.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nerc-cip | CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1
nist | CM-7(a), CM-7(b), SC-5, CM-6(a), SC-7(a)
nist-csf | DE.AE-1, DE.CM-1, ID.AM-3, PR.AC-5, PR.DS-4, PR.DS-5, PR.IP-1, PR.PT-3, PR.PT-4
os-srg | SRG-OS-000480-GPOS-00227
anssi | R12
pcidss4 | 1.4.5, 1.4
cis | 3.3.2
stigref | SV-257969r1155623_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.ipv4.conf.default.send_redirects%3D0%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_ipv4_conf_default_send_redirects.conf
            overwrite: true
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv4.conf.default.send_redirects from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.conf.default.send_redirects.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv4.conf.default.send_redirects" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv4_conf_default_send_redirects.conf'

    #
    # Set runtime for net.ipv4.conf.default.send_redirects
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv4.conf.default.send_redirects="0"
    fi

    #
    # If net.ipv4.conf.default.send_redirects present in /etc/sysctl.conf, change value to "0"
    #	else, add "net.ipv4.conf.default.send_redirects = 0" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.conf.default.send_redirects")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "0"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.conf.default.send_redirects\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.conf.default.send_redirects\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253070
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.5
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_send_redirects

    - name: Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces
        by Default - Set fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253070
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.5
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_send_redirects

    - name: Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces
        by Default - Find all files that contain net.ipv4.conf.default.send_redirects
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.default.send_redirects\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253070
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.5
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_send_redirects

    - name: Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces
        by Default - Find all files that set net.ipv4.conf.default.send_redirects to correct
        value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.conf.default.send_redirects\s*=\s*0$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253070
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.5
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_send_redirects

    - name: Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces
        by Default - Comment out any occurrences of net.ipv4.conf.default.send_redirects
        from config files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv4.conf.default.send_redirects
        replace: '#net.ipv4.conf.default.send_redirects'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253070
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.5
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_send_redirects

    - name: Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces
        by Default - Comment out any occurrences of net.ipv4.conf.default.send_redirects
        from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv4.conf.default.send_redirects
        replace: '#net.ipv4.conf.default.send_redirects'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253070
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.5
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_send_redirects

    - name: Disable Kernel Parameter for Sending ICMP Redirects on all IPv4 Interfaces
        by Default - Ensure sysctl net.ipv4.conf.default.send_redirects is set to 0
      ansible.posix.sysctl:
        name: net.ipv4.conf.default.send_redirects
        value: '0'
        sysctl_file: /etc/sysctl.d/net_ipv4_conf_default_send_redirects.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.10.1.1
      - DISA-STIG-RHEL-09-253070
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.5
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_conf_default_send_redirects
#### Rule Disable Kernel Parameter for IP Forwarding on IPv4 Interfaces [ref]
To set the runtime status of the `net.ipv4.ip_forward` kernel parameter, run the following command:
    $ sudo sysctl -w net.ipv4.ip_forward=0
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    net.ipv4.ip_forward = 0
Warning: Certain technologies such as virtual machines, containers, etc. rely on IPv4 forwarding to enable and use networking. Disabling IPv4 forwarding would cause those technologies to stop working. Therefore, this rule should not be used in profiles or benchmarks that target usage of IPv4 forwarding.
Rationale: | Routing protocol daemons are typically used on routers to exchange network topology information with other routers. If this capability is used when not required, system network information may be unnecessarily transmitted across the network.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_ipv4_ip_forward
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 2, 3, 7, 8, 9
cobit5 | APO13.01, BAI04.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS03.05, DSS05.02, DSS05.05, DSS05.07, DSS06.06
cui | 3.1.20
isa-62443-2009 | 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.2, SR 7.1, SR 7.2, SR 7.6
iso27001-2013 | A.12.1.2, A.12.1.3, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.17.2.1, A.9.1.2
nerc-cip | CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1
nist | CM-7(a), CM-7(b), SC-5, CM-6(a), SC-7(a)
nist-csf | DE.CM-1, PR.DS-4, PR.IP-1, PR.PT-3, PR.PT-4
pcidss | Req-1.3.1, Req-1.3.2
os-srg | SRG-OS-000480-GPOS-00227
anssi | R12
pcidss4 | 1.4.3, 1.4
cis | 3.3.1
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of net.ipv4.ip_forward from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*net.ipv4.ip_forward.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "net.ipv4.ip_forward" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/net_ipv4_ip_forward.conf'

    #
    # Set runtime for net.ipv4.ip_forward
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w net.ipv4.ip_forward="0"
    fi

    #
    # If net.ipv4.ip_forward present in /etc/sysctl.conf, change value to "0"
    #	else, add "net.ipv4.ip_forward = 0" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^net.ipv4.ip_forward")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "0"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^net.ipv4.ip_forward\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^net.ipv4.ip_forward\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.3.1
      - PCI-DSS-Req-1.3.2
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_ip_forward

    - name: Disable Kernel Parameter for IP Forwarding on IPv4 Interfaces - Set fact for
        sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.3.1
      - PCI-DSS-Req-1.3.2
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_ip_forward

    - name: Disable Kernel Parameter for IP Forwarding on IPv4 Interfaces - Find all files
        that contain net.ipv4.ip_forward
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.ip_forward\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.3.1
      - PCI-DSS-Req-1.3.2
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_ip_forward

    - name: Disable Kernel Parameter for IP Forwarding on IPv4 Interfaces - Find all files
        that set net.ipv4.ip_forward to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*net.ipv4.ip_forward\s*=\s*0$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.3.1
      - PCI-DSS-Req-1.3.2
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_ip_forward

    - name: Disable Kernel Parameter for IP Forwarding on IPv4 Interfaces - Comment out
        any occurrences of net.ipv4.ip_forward from config files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*net.ipv4.ip_forward
        replace: '#net.ipv4.ip_forward'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.3.1
      - PCI-DSS-Req-1.3.2
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_ip_forward

    - name: Disable Kernel Parameter for IP Forwarding on IPv4 Interfaces - Comment out
        any occurrences of net.ipv4.ip_forward from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*net.ipv4.ip_forward
        replace: '#net.ipv4.ip_forward'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.3.1
      - PCI-DSS-Req-1.3.2
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_ip_forward

    - name: Disable Kernel Parameter for IP Forwarding on IPv4 Interfaces - Ensure sysctl
        net.ipv4.ip_forward is set to 0
      ansible.posix.sysctl:
        name: net.ipv4.ip_forward
        value: '0'
        sysctl_file: /etc/sysctl.d/net_ipv4_ip_forward.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.1.20
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-5
      - NIST-800-53-SC-7(a)
      - PCI-DSS-Req-1.3.1
      - PCI-DSS-Req-1.3.2
      - PCI-DSSv4-1.4
      - PCI-DSSv4-1.4.3
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_ipv4_ip_forward
Group nftables Group contains 1 rule
[ref] `If firewalld or iptables are being used in your environment, please follow the guidance in their respective section and pass-over the guidance in this section.` nftables is a subsystem of the Linux kernel providing filtering and classification of network packets/datagrams/frames and is the successor to iptables. The biggest change with the successor nftables is its simplicity. With iptables, we have to configure every single rule and use the syntax which can be compared with normal commands. With nftables, the simpler syntax, much like BPF (Berkely Packet Filter) means shorter lines and less repetition. Support for nftables should also be compiled into the kernel, together with the related nftables modules. It is available in Linux kernels >= 3.13. Please ensure that your kernel supports nftables before choosing this option.
#### Rule Verify nftables Service is Disabled [ref]
nftables is a subsystem of the Linux kernel providing filtering and classification of network packets/datagrams/frames and is the successor to iptables. The `nftables` service can be disabled with the following command:
    systemctl disable nftables
Rationale: | Running both `firewalld` and `nftables` may lead to conflict. `nftables` is actually one of the backends for `firewalld` management tools.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_nftables_disabled
References: |
pcidss4 | 1.2.1, 1.2
cis | 4.1.2
Remediation Kubernetes snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        systemd:
          units:
          - name: nftables.service
            enabled: false
            mask: true
          - name: nftables.socket
            enabled: false
            mask: true
Remediation OSBuild Blueprint snippet ⇲
    [customizations.services]
    masked = ["nftables"]
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if ( ( rpm --quiet -q firewalld && rpm --quiet -q nftables && rpm --quiet -q kernel-core ) ); then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then
      "$SYSTEMCTL_EXEC" stop 'nftables.service'
    fi
    "$SYSTEMCTL_EXEC" disable 'nftables.service'
    "$SYSTEMCTL_EXEC" mask 'nftables.service'
    # Disable socket activation if we have a unit file for it
    if "$SYSTEMCTL_EXEC" -q list-unit-files nftables.socket; then
        if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then
          "$SYSTEMCTL_EXEC" stop 'nftables.socket'
        fi
        "$SYSTEMCTL_EXEC" mask 'nftables.socket'
    fi
    # The service may not be running because it has been started and failed,
    # so let's reset the state so OVAL checks pass.
    # Service should be 'inactive', not 'failed' after reboot though.
    "$SYSTEMCTL_EXEC" reset-failed 'nftables.service' || true

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable
    service disable nftables
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - PCI-DSSv4-1.2
      - PCI-DSSv4-1.2.1
      - disable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_nftables_disabled

    - name: Verify nftables Service is Disabled - Disable service nftables
      block:

      - name: Verify nftables Service is Disabled - Collect systemd Services Present in
          the System
        ansible.builtin.command: systemctl -q list-unit-files --type service
        register: service_exists
        changed_when: false
        failed_when: service_exists.rc not in [0, 1]
        check_mode: false

      - name: Verify nftables Service is Disabled - Ensure nftables.service is Masked
        ansible.builtin.systemd:
          name: nftables.service
          state: stopped
          enabled: false
          masked: true
        when: service_exists.stdout_lines is search("nftables.service", multiline=True)

      - name: Unit Socket Exists - nftables.socket
        ansible.builtin.command: systemctl -q list-unit-files nftables.socket
        register: socket_file_exists
        changed_when: false
        failed_when: socket_file_exists.rc not in [0, 1]
        check_mode: false

      - name: Verify nftables Service is Disabled - Disable Socket nftables
        ansible.builtin.systemd:
          name: nftables.socket
          enabled: false
          state: stopped
          masked: true
        when: socket_file_exists.stdout_lines is search("nftables.socket", multiline=True)
      tags:
      - PCI-DSSv4-1.2
      - PCI-DSSv4-1.2.1
      - disable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_nftables_disabled
      - special_service_block
      when: ( "firewalld" in ansible_facts.packages and "nftables" in ansible_facts.packages
        and "kernel-core" in ansible_facts.packages )
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    include disable_nftables

    class disable_nftables {
      service {'nftables':
        enable => false,
        ensure => 'stopped',
      }
    }
Group File Permissions and Masks Group contains 3 groups and 5 rules
[ref] Traditional Unix security relies heavily on file and directory permissions to prevent unauthorized users from reading or modifying files to which they should not have access. Several of the commands in this section search filesystems for files or directories with certain characteristics, and are intended to be run on every local partition on a given system. When the variable PART appears in one of the commands below, it means that the command is intended to be run repeatedly, with the name of each local partition substituted for PART in turn. The following command prints a list of all xfs partitions on the local system, which is the default filesystem for Red Hat Enterprise Linux 9 installations:
    $ mount -t xfs | awk '{print $3}'
For any systems that use a different local filesystem type, modify this command as appropriate.
Group Restrict Partition Mount Options Group contains 4 rules
[ref] System partitions can be mounted with certain options that limit what files on those partitions can do. These options are set in the `/etc/fstab` configuration file, and can be used to make certain types of malicious behavior more difficult.
#### Rule Add nosuid Option to /boot/efi [ref]
The `nosuid` mount option can be used to prevent execution of setuid programs in `/boot/efi`. The SUID and SGID permissions should not be required on the boot partition. Add the `nosuid` option to the fourth column of `/etc/fstab` for the line which controls mounting of `/boot/efi`.
Rationale: | The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from boot partitions.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_mount_option_boot_efi_nosuid
References: |
nist | CM-6(b), CM-6.1(iv)
os-srg | SRG-OS-000480-GPOS-00227
stigref | SV-257862r1155661_rule
Remediation Shell script ⇲
Reboot: | false
    # Remediation is applicable only in certain platforms
    if ( ! ( { rpm --quiet -q kernel-core ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} && ([ -f /run/ostree-booted ] || [ -L /ostree ]) ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ) && { ( findmnt --kernel "/boot/efi" > /dev/null || findmnt --fstab "/boot/efi" > /dev/null ); }; then

    function perform_remediation {

        # Check if the partition is mounted with an excluded filesystem type
        if findmnt --kernel --raw --evaluate --output=FSTYPE "/boot/efi" | grep -q "^vfat$"; then
            # The mount point /boot/efi is using vfat filesystem
            # which is excluded from this requirement. No remediation needed.
            return 0
        fi

            # the mount point /boot/efi has to be defined in /etc/fstab
            # before this remediation can be executed. In case it is not defined, the
            # remediation aborts and no changes regarding the mount point are done.
            mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/boot/efi")"

        grep "$mount_point_match_regexp" -q /etc/fstab \
            || { echo "The mount point '/boot/efi' is not even in /etc/fstab, so we can't set up mount options" >&2;
                    echo "Not remediating, because there is no record of /boot/efi in /etc/fstab" >&2; return 1; }

        mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /boot/efi)"

        # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
        if ! grep -q "$mount_point_match_regexp" /etc/fstab; then
            # runtime opts without some automatic kernel/userspace-added defaults
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                        | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//")
            [ "$previous_mount_opts" ] && previous_mount_opts+=","
            # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in
            # fstab as "block".  The next variable is to satisfy shellcheck SC2050.
            fs_type=""
            if [  "$fs_type" == "iso9660" ] ; then
                previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts")
            fi
            echo " /boot/efi  defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab
        # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
        elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nosuid"; then
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
            sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab
        fi

        if mkdir -p "/boot/efi"; then
            if mountpoint -q "/boot/efi"; then
                mount -o remount --target "/boot/efi"
            fi
        fi
    }

    perform_remediation

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | high
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-231105
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_efi_nosuid
      - no_reboot_needed

    - name: 'Add nosuid Option to /boot/efi: Check filesystem type of /boot/efi'
      ansible.builtin.command: findmnt --kernel --raw --evaluate --output=FSTYPE '/boot/efi'
      register: fs_type_check
      failed_when: fs_type_check.rc > 1
      changed_when: false
      check_mode: false
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
      tags:
      - DISA-STIG-RHEL-09-231105
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_efi_nosuid
      - no_reboot_needed

    - name: 'Add nosuid Option to /boot/efi: Set fact for excluded filesystem'
      set_fact:
        is_excluded_fstype: '{{ fs_type_check.rc == 0 and ''vfat'' in fs_type_check.stdout_lines
          }}'
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
      tags:
      - DISA-STIG-RHEL-09-231105
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_efi_nosuid
      - no_reboot_needed

    - name: 'Add nosuid Option to /boot/efi: Check information associated to mountpoint'
      ansible.builtin.command: findmnt --fstab '/boot/efi'
      register: device_name
      failed_when: device_name.rc > 1
      changed_when: false
      check_mode: false
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
      - not is_excluded_fstype | default(false)
      tags:
      - DISA-STIG-RHEL-09-231105
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_efi_nosuid
      - no_reboot_needed

    - name: 'Add nosuid Option to /boot/efi: Create mount_info dictionary variable'
      set_fact:
        mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
      with_together:
      - '{{ device_name.stdout_lines[0].split() | map(''lower'') | list }}'
      - '{{ device_name.stdout_lines[1].split() | list }}'
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
      - device_name.stdout is defined and device_name.stdout_lines is defined
      - (device_name.stdout | length > 0)
      - not is_excluded_fstype | default(false)
      tags:
      - DISA-STIG-RHEL-09-231105
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_efi_nosuid
      - no_reboot_needed

    - name: 'Add nosuid Option to /boot/efi: If /boot/efi not mounted, craft mount_info
        manually'
      set_fact:
        mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
      with_together:
      - - target
        - source
        - fstype
        - options
      - - /boot/efi
        - ''
        - ''
        - defaults
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
      - ("--fstab" | length == 0)
      - device_name.stdout is defined and device_name.stdout_lines is defined
      - (device_name.stdout | length == 0)
      - not is_excluded_fstype | default(false)
      tags:
      - DISA-STIG-RHEL-09-231105
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_efi_nosuid
      - no_reboot_needed

    - name: 'Add nosuid Option to /boot/efi: Make sure nosuid option is part of the to
        /boot/efi options'
      set_fact:
        mount_info: '{{ mount_info | combine( {''options'':''''~(mount_info.options |
          default(''''))~('','' if (mount_info.options | default('''')) else '''')~''nosuid''
          }) }}'
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
      - mount_info is defined and "nosuid" not in (mount_info.options | default(''))
      - not is_excluded_fstype | default(false)
      tags:
      - DISA-STIG-RHEL-09-231105
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_efi_nosuid
      - no_reboot_needed

    - name: 'Add nosuid Option to /boot/efi: Ensure /boot/efi is mounted with nosuid option'
      ansible.posix.mount:
        path: /boot/efi
        src: '{{ mount_info.source | default('''') }}'
        opts: '{{ mount_info.options | default('''') }}'
        state: mounted
        fstype: '{{ mount_info.fstype | default('''') }}'
      register: mount_result
      failed_when:
      - mount_result is failed
      - '''target is busy'' not in (mount_result.msg | default(''''))'
      - '''already mounted'' not in (mount_result.msg | default(''''))'
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - '"/boot/efi" in ansible_mounts | map(attribute="mount") | list'
      - mount_info is defined
      - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
        | length == 0)
      - not is_excluded_fstype | default(false)
      tags:
      - DISA-STIG-RHEL-09-231105
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_efi_nosuid
      - no_reboot_needed
#### Rule Add nodev Option to /boot [ref]
The `nodev` mount option can be used to prevent device files from being created in `/boot`. Legitimate character and block devices should exist only in the `/dev` directory on the root partition or within chroot jails built for system services. Add the `nodev` option to the fourth column of `/etc/fstab` for the line which controls mounting of `/boot`.
Rationale: | The only legitimate location for device files is the `/dev` directory located on the root partition. The only exception to this is chroot jails.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_mount_option_boot_nodev
References: |
nerc-cip | CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2
nist | CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7
nist-csf | PR.IP-1, PR.PT-2, PR.PT-3
os-srg | SRG-OS-000368-GPOS-00154
stigref | SV-257860r1044940_rule
Remediation Shell script ⇲
Reboot: | false
    # Remediation is applicable only in certain platforms
    if ( ! ( { rpm --quiet -q kernel-core ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} && ([ -f /run/ostree-booted ] || [ -L /ostree ]) ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then

    function perform_remediation {

            # the mount point /boot has to be defined in /etc/fstab
            # before this remediation can be executed. In case it is not defined, the
            # remediation aborts and no changes regarding the mount point are done.
            mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/boot")"

        grep "$mount_point_match_regexp" -q /etc/fstab \
            || { echo "The mount point '/boot' is not even in /etc/fstab, so we can't set up mount options" >&2;
                    echo "Not remediating, because there is no record of /boot in /etc/fstab" >&2; return 1; }

        mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /boot)"

        # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
        if ! grep -q "$mount_point_match_regexp" /etc/fstab; then
            # runtime opts without some automatic kernel/userspace-added defaults
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                        | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//")
            [ "$previous_mount_opts" ] && previous_mount_opts+=","
            # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in
            # fstab as "block".  The next variable is to satisfy shellcheck SC2050.
            fs_type=""
            if [  "$fs_type" == "iso9660" ] ; then
                previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts")
            fi
            echo " /boot  defaults,${previous_mount_opts}nodev 0 0" >> /etc/fstab
        # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
        elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nodev"; then
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
            sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab
        fi

        if mkdir -p "/boot"; then
            if mountpoint -q "/boot"; then
                mount -o remount --target "/boot"
            fi
        fi
    }

    perform_remediation

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Anaconda snippet ⇲
Complexity: | low
Disruption: | high
Reboot: | false
Strategy: | enable
    part /boot --mountoptions="nodev"
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | high
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-231095
      - NIST-800-53-AC-6
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MP-7
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_nodev
      - no_reboot_needed

    - name: 'Add nodev Option to /boot: Check information associated to mountpoint'
      ansible.builtin.command: findmnt --fstab '/boot'
      register: device_name
      failed_when: device_name.rc > 1
      changed_when: false
      check_mode: false
      when: ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      tags:
      - DISA-STIG-RHEL-09-231095
      - NIST-800-53-AC-6
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MP-7
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_nodev
      - no_reboot_needed

    - name: 'Add nodev Option to /boot: Create mount_info dictionary variable'
      set_fact:
        mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
      with_together:
      - '{{ device_name.stdout_lines[0].split() | map(''lower'') | list }}'
      - '{{ device_name.stdout_lines[1].split() | list }}'
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - device_name.stdout is defined and device_name.stdout_lines is defined
      - (device_name.stdout | length > 0)
      tags:
      - DISA-STIG-RHEL-09-231095
      - NIST-800-53-AC-6
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MP-7
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_nodev
      - no_reboot_needed

    - name: 'Add nodev Option to /boot: If /boot not mounted, craft mount_info manually'
      set_fact:
        mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
      with_together:
      - - target
        - source
        - fstype
        - options
      - - /boot
        - ''
        - ''
        - defaults
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - ("--fstab" | length == 0)
      - device_name.stdout is defined and device_name.stdout_lines is defined
      - (device_name.stdout | length == 0)
      tags:
      - DISA-STIG-RHEL-09-231095
      - NIST-800-53-AC-6
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MP-7
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_nodev
      - no_reboot_needed

    - name: 'Add nodev Option to /boot: Make sure nodev option is part of the to /boot
        options'
      set_fact:
        mount_info: '{{ mount_info | combine( {''options'':''''~(mount_info.options |
          default(''''))~('','' if (mount_info.options | default('''')) else '''')~''nodev''
          }) }}'
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - mount_info is defined and "nodev" not in (mount_info.options | default(''))
      tags:
      - DISA-STIG-RHEL-09-231095
      - NIST-800-53-AC-6
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MP-7
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_nodev
      - no_reboot_needed

    - name: 'Add nodev Option to /boot: Ensure /boot is mounted with nodev option'
      ansible.posix.mount:
        path: /boot
        src: '{{ mount_info.source | default('''') }}'
        opts: '{{ mount_info.options | default('''') }}'
        state: mounted
        fstype: '{{ mount_info.fstype | default('''') }}'
      register: mount_result
      failed_when:
      - mount_result is failed
      - '''target is busy'' not in (mount_result.msg | default(''''))'
      - '''already mounted'' not in (mount_result.msg | default(''''))'
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - mount_info is defined
      - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
        | length == 0)
      tags:
      - DISA-STIG-RHEL-09-231095
      - NIST-800-53-AC-6
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MP-7
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_nodev
      - no_reboot_needed
#### Rule Add noexec Option to /boot [ref]
The `noexec` mount option can be used to prevent binaries from being executed out of `/boot`. Add the `noexec` option to the fourth column of `/etc/fstab` for the line which controls mounting of `/boot`.
Rationale: | The `/boot` partition contains the kernel and the bootloader. No binaries should be executed from this partition after the booting process finishes.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_mount_option_boot_noexec
References: |
anssi | R28
Remediation Shell script ⇲
Reboot: | false
    # Remediation is applicable only in certain platforms
    if ( ! ( { rpm --quiet -q kernel-core ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} && ([ -f /run/ostree-booted ] || [ -L /ostree ]) ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then

    function perform_remediation {

            # the mount point /boot has to be defined in /etc/fstab
            # before this remediation can be executed. In case it is not defined, the
            # remediation aborts and no changes regarding the mount point are done.
            mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/boot")"

        grep "$mount_point_match_regexp" -q /etc/fstab \
            || { echo "The mount point '/boot' is not even in /etc/fstab, so we can't set up mount options" >&2;
                    echo "Not remediating, because there is no record of /boot in /etc/fstab" >&2; return 1; }

        mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /boot)"

        # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
        if ! grep -q "$mount_point_match_regexp" /etc/fstab; then
            # runtime opts without some automatic kernel/userspace-added defaults
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                        | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//")
            [ "$previous_mount_opts" ] && previous_mount_opts+=","
            # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in
            # fstab as "block".  The next variable is to satisfy shellcheck SC2050.
            fs_type=""
            if [  "$fs_type" == "iso9660" ] ; then
                previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts")
            fi
            echo " /boot  defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab
        # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
        elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "noexec"; then
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
            sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab
        fi

        if mkdir -p "/boot"; then
            if mountpoint -q "/boot"; then
                mount -o remount --target "/boot"
            fi
        fi
    }

    perform_remediation

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Anaconda snippet ⇲
Complexity: | low
Disruption: | high
Reboot: | false
Strategy: | enable
    part /boot --mountoptions="noexec"
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | high
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_noexec
      - no_reboot_needed

    - name: 'Add noexec Option to /boot: Check information associated to mountpoint'
      ansible.builtin.command: findmnt --fstab '/boot'
      register: device_name
      failed_when: device_name.rc > 1
      changed_when: false
      check_mode: false
      when: ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      tags:
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_noexec
      - no_reboot_needed

    - name: 'Add noexec Option to /boot: Create mount_info dictionary variable'
      set_fact:
        mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
      with_together:
      - '{{ device_name.stdout_lines[0].split() | map(''lower'') | list }}'
      - '{{ device_name.stdout_lines[1].split() | list }}'
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - device_name.stdout is defined and device_name.stdout_lines is defined
      - (device_name.stdout | length > 0)
      tags:
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_noexec
      - no_reboot_needed

    - name: 'Add noexec Option to /boot: If /boot not mounted, craft mount_info manually'
      set_fact:
        mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
      with_together:
      - - target
        - source
        - fstype
        - options
      - - /boot
        - ''
        - ''
        - defaults
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - ("--fstab" | length == 0)
      - device_name.stdout is defined and device_name.stdout_lines is defined
      - (device_name.stdout | length == 0)
      tags:
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_noexec
      - no_reboot_needed

    - name: 'Add noexec Option to /boot: Make sure noexec option is part of the to /boot
        options'
      set_fact:
        mount_info: '{{ mount_info | combine( {''options'':''''~(mount_info.options |
          default(''''))~('','' if (mount_info.options | default('''')) else '''')~''noexec''
          }) }}'
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - mount_info is defined and "noexec" not in (mount_info.options | default(''))
      tags:
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_noexec
      - no_reboot_needed

    - name: 'Add noexec Option to /boot: Ensure /boot is mounted with noexec option'
      ansible.posix.mount:
        path: /boot
        src: '{{ mount_info.source | default('''') }}'
        opts: '{{ mount_info.options | default('''') }}'
        state: mounted
        fstype: '{{ mount_info.fstype | default('''') }}'
      register: mount_result
      failed_when:
      - mount_result is failed
      - '''target is busy'' not in (mount_result.msg | default(''''))'
      - '''already mounted'' not in (mount_result.msg | default(''''))'
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - mount_info is defined
      - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
        | length == 0)
      tags:
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_noexec
      - no_reboot_needed
#### Rule Add nosuid Option to /boot [ref]
The `nosuid` mount option can be used to prevent execution of setuid programs in `/boot`. The SUID and SGID permissions should not be required on the boot partition. Add the `nosuid` option to the fourth column of `/etc/fstab` for the line which controls mounting of `/boot`.
Rationale: | The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from boot partitions.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_mount_option_boot_nosuid
References: |
nerc-cip | CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2
nist | CM-7(a), CM-7(b), CM-6(a), AC-6, AC-6(1), MP-7
nist-csf | PR.IP-1, PR.PT-2, PR.PT-3
os-srg | SRG-OS-000368-GPOS-00154, SRG-OS-000480-GPOS-00227
anssi | R28
stigref | SV-257861r1044941_rule
Remediation Shell script ⇲
Reboot: | false
    # Remediation is applicable only in certain platforms
    if ( ! ( { rpm --quiet -q kernel-core ;} && { rpm --quiet -q rpm-ostree ;} && { rpm --quiet -q bootc ;} && { ! rpm --quiet -q openshift-kubelet ;} && ([ -f /run/ostree-booted ] || [ -L /ostree ]) ) && ! ( [ -f /.dockerenv ] || [ -f /run/.containerenv ] ) ); then

    function perform_remediation {

            # the mount point /boot has to be defined in /etc/fstab
            # before this remediation can be executed. In case it is not defined, the
            # remediation aborts and no changes regarding the mount point are done.
            mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" "/boot")"

        grep "$mount_point_match_regexp" -q /etc/fstab \
            || { echo "The mount point '/boot' is not even in /etc/fstab, so we can't set up mount options" >&2;
                    echo "Not remediating, because there is no record of /boot in /etc/fstab" >&2; return 1; }

        mount_point_match_regexp="$(printf "^[[:space:]]*[^#].*[[:space:]]%s[[:space:]]" /boot)"

        # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
        if ! grep -q "$mount_point_match_regexp" /etc/fstab; then
            # runtime opts without some automatic kernel/userspace-added defaults
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                        | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//")
            [ "$previous_mount_opts" ] && previous_mount_opts+=","
            # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in
            # fstab as "block".  The next variable is to satisfy shellcheck SC2050.
            fs_type=""
            if [  "$fs_type" == "iso9660" ] ; then
                previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts")
            fi
            echo " /boot  defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab
        # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
        elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "nosuid"; then
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
            sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab
        fi

        if mkdir -p "/boot"; then
            if mountpoint -q "/boot"; then
                mount -o remount --target "/boot"
            fi
        fi
    }

    perform_remediation

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Anaconda snippet ⇲
Complexity: | low
Disruption: | high
Reboot: | false
Strategy: | enable
    part /boot --mountoptions="nosuid"
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | high
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-231100
      - NIST-800-53-AC-6
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MP-7
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_nosuid
      - no_reboot_needed

    - name: 'Add nosuid Option to /boot: Check information associated to mountpoint'
      ansible.builtin.command: findmnt --fstab '/boot'
      register: device_name
      failed_when: device_name.rc > 1
      changed_when: false
      check_mode: false
      when: ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      tags:
      - DISA-STIG-RHEL-09-231100
      - NIST-800-53-AC-6
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MP-7
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_nosuid
      - no_reboot_needed

    - name: 'Add nosuid Option to /boot: Create mount_info dictionary variable'
      set_fact:
        mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
      with_together:
      - '{{ device_name.stdout_lines[0].split() | map(''lower'') | list }}'
      - '{{ device_name.stdout_lines[1].split() | list }}'
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - device_name.stdout is defined and device_name.stdout_lines is defined
      - (device_name.stdout | length > 0)
      tags:
      - DISA-STIG-RHEL-09-231100
      - NIST-800-53-AC-6
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MP-7
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_nosuid
      - no_reboot_needed

    - name: 'Add nosuid Option to /boot: If /boot not mounted, craft mount_info manually'
      set_fact:
        mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
      with_together:
      - - target
        - source
        - fstype
        - options
      - - /boot
        - ''
        - ''
        - defaults
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - ("--fstab" | length == 0)
      - device_name.stdout is defined and device_name.stdout_lines is defined
      - (device_name.stdout | length == 0)
      tags:
      - DISA-STIG-RHEL-09-231100
      - NIST-800-53-AC-6
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MP-7
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_nosuid
      - no_reboot_needed

    - name: 'Add nosuid Option to /boot: Make sure nosuid option is part of the to /boot
        options'
      set_fact:
        mount_info: '{{ mount_info | combine( {''options'':''''~(mount_info.options |
          default(''''))~('','' if (mount_info.options | default('''')) else '''')~''nosuid''
          }) }}'
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - mount_info is defined and "nosuid" not in (mount_info.options | default(''))
      tags:
      - DISA-STIG-RHEL-09-231100
      - NIST-800-53-AC-6
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MP-7
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_nosuid
      - no_reboot_needed

    - name: 'Add nosuid Option to /boot: Ensure /boot is mounted with nosuid option'
      ansible.posix.mount:
        path: /boot
        src: '{{ mount_info.source | default('''') }}'
        opts: '{{ mount_info.options | default('''') }}'
        state: mounted
        fstype: '{{ mount_info.fstype | default('''') }}'
      register: mount_result
      failed_when:
      - mount_result is failed
      - '''target is busy'' not in (mount_result.msg | default(''''))'
      - '''already mounted'' not in (mount_result.msg | default(''''))'
      when:
      - ( not ( "kernel-core" in ansible_facts.packages and "rpm-ostree" in ansible_facts.packages
        and "bootc" in ansible_facts.packages and not "openshift-kubelet" in ansible_facts.packages
        and "ostree" in ansible_proc_cmdline ) and not ( ansible_virtualization_type in
        ["docker", "lxc", "openvz", "podman", "container"] ) )
      - mount_info is defined
      - (device_name.stdout is defined and (device_name.stdout | length > 0)) or ("--fstab"
        | length == 0)
      tags:
      - DISA-STIG-RHEL-09-231100
      - NIST-800-53-AC-6
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MP-7
      - configure_strategy
      - high_disruption
      - low_complexity
      - medium_severity
      - mount_option_boot_nosuid
      - no_reboot_needed
Group Restrict Programs from Dangerous Execution Patterns Group contains 1 group and 1 rule
[ref] The recommendations in this section are designed to ensure that the system's features to protect against potentially dangerous program execution are activated. These protections are applied at the system initialization or kernel level, and defend against certain types of badly-configured or compromised programs.
Group Disable Core Dumps Group contains 1 rule
[ref] A core dump file is the memory image of an executable program when it was terminated by the operating system due to errant behavior. In most cases, only software developers legitimately need to access these files. The core dump files may also contain sensitive information, or unnecessarily occupy large amounts of disk space. Once a hard limit is set in `/etc/security/limits.conf`, or to a file within the `/etc/security/limits.d/` directory, a user cannot increase that limit within his or her own session. If access to core dumps is required, consider restricting them to only certain users or groups. See the `limits.conf` man page for more information. The core dumps of setuid programs are further protected. The `sysctl` variable `fs.suid_dumpable` controls whether the kernel allows core dumps from these programs at all. The default value of 0 is recommended.
#### Rule Disable Core Dumps for SUID programs [ref]
To set the runtime status of the `fs.suid_dumpable` kernel parameter, run the following command:
    $ sudo sysctl -w fs.suid_dumpable=0
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    fs.suid_dumpable = 0
Rationale: | The core dump of a setuid program is more likely to contain sensitive data, as the program itself runs with greater privileges than the user who initiated execution of the program. Disabling the ability for any setuid program to write a core file decreases the risk of unauthorized access of such data.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_fs_suid_dumpable
References: |
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3), 164.308(a)(4), 164.310(b), 164.310(c), 164.312(a), 164.312(e)
nist | SI-11(a), SI-11(b)
anssi | R14
pcidss4 | 3.3.1.1, 3.3.1, 3.3
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    # Comment out any occurrences of fs.suid_dumpable from /etc/sysctl.d/*.conf files

    for f in /etc/sysctl.d/*.conf /run/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf; do

      # skip systemd-sysctl symlink (/etc/sysctl.d/99-sysctl.conf -> /etc/sysctl.conf)
      if [[ "$(readlink -f "$f")" == "/etc/sysctl.conf" ]]; then continue; fi

      matching_list=$(grep -P '^(?!#).*[\s]*fs.suid_dumpable.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          escaped_entry=$(sed -e 's|/|\\/|g' <<< "$entry")
          # comment out "fs.suid_dumpable" matches to preserve user data
          sed -i --follow-symlinks "s/^${escaped_entry}$/# &/g" $f
        done <<< "$matching_list"
      fi
    done

    #
    # Set sysctl config file which to save the desired value
    #

    SYSCONFIG_FILE='/etc/sysctl.d/fs_suid_dumpable.conf'

    #
    # Set runtime for fs.suid_dumpable
    #
    if ! { rpm --quiet -q kernel rpm-ostree bootc && ! rpm --quiet -q openshift-kubelet && { [ -f "/run/.containerenv" ] || [ -f "/.containerenv" ]; }; } ; then
        /sbin/sysctl -q -n -w fs.suid_dumpable="0"
    fi

    #
    # If fs.suid_dumpable present in /etc/sysctl.conf, change value to "0"
    #	else, add "fs.suid_dumpable = 0" to /etc/sysctl.conf
    #

    sed -i "/^$SYSCONFIG_VAR/d" /etc/sysctl.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^fs.suid_dumpable")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "0"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^fs.suid_dumpable\\>" "${SYSCONFIG_FILE}"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^fs.suid_dumpable\\>.*/$escaped_formatted_output/gi" "${SYSCONFIG_FILE}"
    else
        if [[ -s "${SYSCONFIG_FILE}" ]] && [[ -n "$(tail -c 1 -- "${SYSCONFIG_FILE}" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "${SYSCONFIG_FILE}"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "${SYSCONFIG_FILE}" >> "${SYSCONFIG_FILE}"
        printf '%s\n' "$formatted_output" >> "${SYSCONFIG_FILE}"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-53-SI-11(a)
      - NIST-800-53-SI-11(b)
      - PCI-DSSv4-3.3
      - PCI-DSSv4-3.3.1
      - PCI-DSSv4-3.3.1.1
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_fs_suid_dumpable

    - name: Disable Core Dumps for SUID programs - Set fact for sysctl paths
      ansible.builtin.set_fact:
        sysctl_paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-53-SI-11(a)
      - NIST-800-53-SI-11(b)
      - PCI-DSSv4-3.3
      - PCI-DSSv4-3.3.1
      - PCI-DSSv4-3.3.1.1
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_fs_suid_dumpable

    - name: Disable Core Dumps for SUID programs - Find all files that contain fs.suid_dumpable
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*fs.suid_dumpable\s*=\s*.*$'
      register: find_all_values
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-53-SI-11(a)
      - NIST-800-53-SI-11(b)
      - PCI-DSSv4-3.3
      - PCI-DSSv4-3.3.1
      - PCI-DSSv4-3.3.1.1
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_fs_suid_dumpable

    - name: Disable Core Dumps for SUID programs - Find all files that set fs.suid_dumpable
        to correct value
      ansible.builtin.shell:
        cmd: find -L {{ sysctl_paths | join(" ") }} -type f -name '*.conf' | xargs grep
          -HP '^\s*fs.suid_dumpable\s*=\s*0$'
      register: find_correct_value
      check_mode: false
      changed_when: false
      failed_when: false
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-53-SI-11(a)
      - NIST-800-53-SI-11(b)
      - PCI-DSSv4-3.3
      - PCI-DSSv4-3.3.1
      - PCI-DSSv4-3.3.1.1
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_fs_suid_dumpable

    - name: Disable Core Dumps for SUID programs - Comment out any occurrences of fs.suid_dumpable
        from config files
      ansible.builtin.replace:
        path: '{{ item | split(":") | first }}'
        regexp: ^[\s]*fs.suid_dumpable
        replace: '#fs.suid_dumpable'
      loop: '{{ find_all_values.stdout_lines }}'
      when:
      - '"kernel-core" in ansible_facts.packages'
      - find_correct_value.stdout_lines | length == 0 or find_all_values.stdout_lines
        | length > find_correct_value.stdout_lines | length
      tags:
      - NIST-800-53-SI-11(a)
      - NIST-800-53-SI-11(b)
      - PCI-DSSv4-3.3
      - PCI-DSSv4-3.3.1
      - PCI-DSSv4-3.3.1.1
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_fs_suid_dumpable

    - name: Disable Core Dumps for SUID programs - Comment out any occurrences of fs.suid_dumpable
        from /etc/sysctl.conf
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: ^[\s]*fs.suid_dumpable
        replace: '#fs.suid_dumpable'
      with_fileglob:
      - /etc/sysctl.conf
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-53-SI-11(a)
      - NIST-800-53-SI-11(b)
      - PCI-DSSv4-3.3
      - PCI-DSSv4-3.3.1
      - PCI-DSSv4-3.3.1.1
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_fs_suid_dumpable

    - name: Disable Core Dumps for SUID programs - Ensure sysctl fs.suid_dumpable is set
        to 0
      ansible.posix.sysctl:
        name: fs.suid_dumpable
        value: '0'
        sysctl_file: /etc/sysctl.d/fs_suid_dumpable.conf
        state: present
        reload: true
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-53-SI-11(a)
      - NIST-800-53-SI-11(b)
      - PCI-DSSv4-3.3
      - PCI-DSSv4-3.3.1
      - PCI-DSSv4-3.3.1.1
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_fs_suid_dumpable
Group Services Group contains 3 groups and 7 rules
[ref] The best protection against vulnerable software is running less software. This section describes how to review the software which Red Hat Enterprise Linux 9 installs on a system and disable software which is not needed. It then enumerates the software packages installed on a default Red Hat Enterprise Linux 9 system and provides guidance about which ones can be safely disabled. Red Hat Enterprise Linux 9 provides a convenient minimal install option that essentially installs the bare necessities for a functional system. When building Red Hat Enterprise Linux 9 systems, it is highly recommended to select the minimal packages and then build up the system from there.
Group SSH Server Group contains 1 group and 4 rules
[ref] The SSH protocol is recommended for remote login and remote file transfer. SSH provides confidentiality and integrity for data exchanged between two systems, as well as server authentication, through the use of public key cryptography. The implementation included with the system is called OpenSSH, and more detailed documentation is available from its website, https://www.openssh.com. Its server program is called `sshd` and provided by the RPM package `openssh-server`.
Group Configure OpenSSH Server if Necessary Group contains 4 rules
[ref] If the system needs to act as an SSH server, then certain changes should be made to the OpenSSH daemon configuration file `/etc/ssh/sshd_config`. The following recommendations can be applied to this file. See the `sshd_config(5)` man page for more detailed information.
#### Rule Set SSH Client Alive Count Max [ref]
The SSH server sends at most `ClientAliveCountMax` messages during a SSH session and waits for a response from the SSH client. The option `ClientAliveInterval` configures timeout after each `ClientAliveCountMax` message. If the SSH server does not receive a response from the client, then the connection is considered unresponsive and terminated. For SSH earlier than v8.2, a `ClientAliveCountMax` value of `0` causes a timeout precisely when the `ClientAliveInterval` is set. Starting with v8.2, a value of `0` disables the timeout functionality completely. If the option is set to a number greater than `0`, then the session will be disconnected after `ClientAliveInterval * ClientAliveCountMax` seconds without receiving a keep alive message.
Rationale: | This ensures a user login will be terminated as soon as the `ClientAliveInterval` is reached.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_set_keepalive
References: |
cis-csc | 1, 12, 13, 14, 15, 16, 18, 3, 5, 7, 8
cjis | 5.5.6
cobit5 | APO13.01, BAI03.01, BAI03.02, BAI03.03, DSS01.03, DSS03.05, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
cui | 3.1.11
hipaa | 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii)
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 6.2
iso27001-2013 | A.12.4.1, A.12.4.3, A.14.1.1, A.14.2.1, A.14.2.5, A.18.1.4, A.6.1.2, A.6.1.5, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5
nerc-cip | CIP-004-6 R2.2.3, CIP-007-3 R5.1, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3
nist | AC-2(5), AC-12, AC-17(a), SC-10, CM-6(a)
nist-csf | DE.CM-1, DE.CM-3, PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.IP-2
pcidss | Req-8.1.8
os-srg | SRG-OS-000163-GPOS-00072, SRG-OS-000279-GPOS-00109
pcidss4 | 8.2.8, 8.2
cis | 5.1.9
stigref | SV-257995r1045053_rule
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    var_sshd_set_keepalive='1'

    mkdir -p /etc/ssh/sshd_config.d
    touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
    chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf

    LC_ALL=C sed -i "/^\s*ClientAliveCountMax\s\+/Id" "/etc/ssh/sshd_config"
    LC_ALL=C sed -i "/^\s*ClientAliveCountMax\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf
    if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then

        LC_ALL=C sed -i "/^\s*ClientAliveCountMax\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    else
        touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"

    cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak"
    # Insert at the beginning of the file
    printf '%s\n' "ClientAliveCountMax $var_sshd_set_keepalive" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.5.6
      - DISA-STIG-RHEL-09-255095
      - NIST-800-171-3.1.11
      - NIST-800-53-AC-12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-2(5)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-10
      - PCI-DSS-Req-8.1.8
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_set_keepalive
    - name: XCCDF Value var_sshd_set_keepalive # promote to variable
      set_fact:
        var_sshd_set_keepalive: !!str 1
      tags:
        - always

    - name: Set SSH Client Alive Count Max - Check if the parameter ClientAliveCountMax
        is configured
      ansible.builtin.find:
        paths:
        - /etc/ssh/sshd_config
        - /etc/ssh/sshd_config.d
        contains: (?i)^\s*{{ "ClientAliveCountMax"| regex_escape }}\s+
      register: _sshd_config_has_parameter
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.5.6
      - DISA-STIG-RHEL-09-255095
      - NIST-800-171-3.1.11
      - NIST-800-53-AC-12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-2(5)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-10
      - PCI-DSS-Req-8.1.8
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_set_keepalive

    - name: Set SSH Client Alive Count Max - Check if the parameter ClientAliveCountMax
        is configured correctly
      ansible.builtin.find:
        paths:
        - /etc/ssh/sshd_config
        - /etc/ssh/sshd_config.d
        contains: (?i)^\s*{{ "ClientAliveCountMax"| regex_escape }}\s+{{ var_sshd_set_keepalive
          }}$
      register: _sshd_config_correctly
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.5.6
      - DISA-STIG-RHEL-09-255095
      - NIST-800-171-3.1.11
      - NIST-800-53-AC-12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-2(5)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-10
      - PCI-DSS-Req-8.1.8
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_set_keepalive

    - name: Set SSH Client Alive Count Max
      block:

      - name: Deduplicate values from /etc/ssh/sshd_config
        ansible.builtin.lineinfile:
          path: /etc/ssh/sshd_config
          create: false
          regexp: (?i)(?i)^\s*{{ "ClientAliveCountMax"| regex_escape }}\s+
          state: absent

      - name: Check if /etc/ssh/sshd_config.d exists
        ansible.builtin.stat:
          path: /etc/ssh/sshd_config.d
        register: _etc_ssh_sshd_config_d_exists

      - name: Check if the parameter ClientAliveCountMax is present in /etc/ssh/sshd_config.d
        ansible.builtin.find:
          paths: /etc/ssh/sshd_config.d
          recurse: 'yes'
          follow: 'no'
          contains: (?i)^\s*{{ "ClientAliveCountMax"| regex_escape }}\s+
        register: _etc_ssh_sshd_config_d_has_parameter
        when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir

      - name: Remove parameter from files in /etc/ssh/sshd_config.d
        ansible.builtin.lineinfile:
          path: '{{ item.path }}'
          create: false
          regexp: (?i)(?i)^\s*{{ "ClientAliveCountMax"| regex_escape }}\s+
          state: absent
        with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}'
        when: _etc_ssh_sshd_config_d_has_parameter.matched > 0

      - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
        ansible.builtin.lineinfile:
          path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
          create: true
          regexp: (?i)(?i)^\s*{{ "ClientAliveCountMax"| regex_escape }}\s+
          line: ClientAliveCountMax {{ var_sshd_set_keepalive }}
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when:
      - '"kernel-core" in ansible_facts.packages'
      - _sshd_config_correctly.matched == 0 or _sshd_config_has_parameter.matched != 1
      tags:
      - CJIS-5.5.6
      - DISA-STIG-RHEL-09-255095
      - NIST-800-171-3.1.11
      - NIST-800-53-AC-12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-2(5)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-10
      - PCI-DSS-Req-8.1.8
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_set_keepalive

    - name: Set SSH Client Alive Count Max - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
      ansible.builtin.file:
        path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
        mode: '0600'
        state: touch
        modification_time: preserve
        access_time: preserve
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.5.6
      - DISA-STIG-RHEL-09-255095
      - NIST-800-171-3.1.11
      - NIST-800-53-AC-12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-2(5)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-10
      - PCI-DSS-Req-8.1.8
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_set_keepalive
#### Rule Set SSH Client Alive Interval [ref]
SSH allows administrators to set a network responsiveness timeout interval. After this interval has passed, the unresponsive client will be automatically logged out. To set this timeout interval, edit the following line in `/etc/ssh/sshd_config` as follows:
    ClientAliveInterval 900
The timeout interval is given in seconds. For example, have a timeout of 10 minutes, set interval to 600. If a shorter timeout has already been set for the login shell, that value will preempt any SSH setting made in `/etc/ssh/sshd_config`. Keep in mind that some processes may stop SSH from correctly detecting that the user is idle. Warning: SSH disconnecting unresponsive clients will not have desired effect without also configuring ClientAliveCountMax in the SSH service configuration. Warning: Following conditions may prevent the SSH session to time out: Remote processes on the remote machine generates output. As the output has to be transferred over the network to the client, the timeout is reset every time such transfer happens. Any `scp` or `sftp` activity by the same user to the host resets the timeout.
Rationale: | Terminating an idle ssh session within a short time period reduces the window of opportunity for unauthorized personnel to take control of a management session enabled on the console or console port that has been let unattended.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_set_idle_timeout
References: |
cis-csc | 1, 12, 13, 14, 15, 16, 18, 3, 5, 7, 8
cjis | 5.5.6
cobit5 | APO13.01, BAI03.01, BAI03.02, BAI03.03, DSS01.03, DSS03.05, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10
cui | 3.1.11
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.3
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 6.2
iso27001-2013 | A.12.4.1, A.12.4.3, A.14.1.1, A.14.2.1, A.14.2.5, A.18.1.4, A.6.1.2, A.6.1.5, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5
nerc-cip | CIP-004-6 R2.2.3, CIP-007-3 R5.1, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3
nist | CM-6(a), AC-17(a), AC-2(5), AC-12, AC-17(a), SC-10, CM-6(a)
nist-csf | DE.CM-1, DE.CM-3, PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.IP-2
pcidss | Req-8.1.8
os-srg | SRG-OS-000126-GPOS-00066, SRG-OS-000163-GPOS-00072, SRG-OS-000279-GPOS-00109, SRG-OS-000395-GPOS-00175
pcidss4 | 8.2.8, 8.2
cis | 5.1.9
stigref | SV-257996r1134915_rule
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    sshd_idle_timeout_value='900'

    mkdir -p /etc/ssh/sshd_config.d
    touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
    chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf

    LC_ALL=C sed -i "/^\s*ClientAliveInterval\s\+/Id" "/etc/ssh/sshd_config"
    LC_ALL=C sed -i "/^\s*ClientAliveInterval\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf
    if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then

        LC_ALL=C sed -i "/^\s*ClientAliveInterval\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    else
        touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"

    cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak"
    # Insert at the beginning of the file
    printf '%s\n' "ClientAliveInterval $sshd_idle_timeout_value" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.5.6
      - DISA-STIG-RHEL-09-255100
      - NIST-800-171-3.1.11
      - NIST-800-53-AC-12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-2(5)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-10
      - PCI-DSS-Req-8.1.8
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_set_idle_timeout
    - name: XCCDF Value sshd_idle_timeout_value # promote to variable
      set_fact:
        sshd_idle_timeout_value: !!str 900
      tags:
        - always

    - name: Set SSH Client Alive Interval - Check if the parameter ClientAliveInterval
        is configured
      ansible.builtin.find:
        paths:
        - /etc/ssh/sshd_config
        - /etc/ssh/sshd_config.d
        contains: (?i)^\s*{{ "ClientAliveInterval"| regex_escape }}\s+
      register: _sshd_config_has_parameter
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.5.6
      - DISA-STIG-RHEL-09-255100
      - NIST-800-171-3.1.11
      - NIST-800-53-AC-12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-2(5)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-10
      - PCI-DSS-Req-8.1.8
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_set_idle_timeout

    - name: Set SSH Client Alive Interval - Check if the parameter ClientAliveInterval
        is configured correctly
      ansible.builtin.find:
        paths:
        - /etc/ssh/sshd_config
        - /etc/ssh/sshd_config.d
        contains: (?i)^\s*{{ "ClientAliveInterval"| regex_escape }}\s+{{ sshd_idle_timeout_value
          }}$
      register: _sshd_config_correctly
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.5.6
      - DISA-STIG-RHEL-09-255100
      - NIST-800-171-3.1.11
      - NIST-800-53-AC-12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-2(5)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-10
      - PCI-DSS-Req-8.1.8
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_set_idle_timeout

    - name: Set SSH Client Alive Interval
      block:

      - name: Deduplicate values from /etc/ssh/sshd_config
        ansible.builtin.lineinfile:
          path: /etc/ssh/sshd_config
          create: false
          regexp: (?i)(?i)^\s*{{ "ClientAliveInterval"| regex_escape }}\s+
          state: absent

      - name: Check if /etc/ssh/sshd_config.d exists
        ansible.builtin.stat:
          path: /etc/ssh/sshd_config.d
        register: _etc_ssh_sshd_config_d_exists

      - name: Check if the parameter ClientAliveInterval is present in /etc/ssh/sshd_config.d
        ansible.builtin.find:
          paths: /etc/ssh/sshd_config.d
          recurse: 'yes'
          follow: 'no'
          contains: (?i)^\s*{{ "ClientAliveInterval"| regex_escape }}\s+
        register: _etc_ssh_sshd_config_d_has_parameter
        when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir

      - name: Remove parameter from files in /etc/ssh/sshd_config.d
        ansible.builtin.lineinfile:
          path: '{{ item.path }}'
          create: false
          regexp: (?i)(?i)^\s*{{ "ClientAliveInterval"| regex_escape }}\s+
          state: absent
        with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}'
        when: _etc_ssh_sshd_config_d_has_parameter.matched > 0

      - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
        ansible.builtin.lineinfile:
          path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
          create: true
          regexp: (?i)(?i)^\s*{{ "ClientAliveInterval"| regex_escape }}\s+
          line: ClientAliveInterval {{ sshd_idle_timeout_value }}
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when:
      - '"kernel-core" in ansible_facts.packages'
      - _sshd_config_correctly.matched == 0 or _sshd_config_has_parameter.matched != 1
      tags:
      - CJIS-5.5.6
      - DISA-STIG-RHEL-09-255100
      - NIST-800-171-3.1.11
      - NIST-800-53-AC-12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-2(5)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-10
      - PCI-DSS-Req-8.1.8
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_set_idle_timeout

    - name: Set SSH Client Alive Interval - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
      ansible.builtin.file:
        path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
        mode: '0600'
        state: touch
        modification_time: preserve
        access_time: preserve
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.5.6
      - DISA-STIG-RHEL-09-255100
      - NIST-800-171-3.1.11
      - NIST-800-53-AC-12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-2(5)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-10
      - PCI-DSS-Req-8.1.8
      - PCI-DSSv4-8.2
      - PCI-DSSv4-8.2.8
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_set_idle_timeout
#### Rule Enable SSH Warning Banner [ref]
To enable the warning banner and ensure it is consistent across the system, add or correct the following line in `/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf`:
    Banner /etc/issue.net
Another section contains information on how to create an appropriate system-wide warning banner.
Rationale: | The warning message reinforces policy awareness during the logon process and facilitates possible legal action against attackers. Alternatively, systems whose ownership should not be obvious should ensure usage of a banner that does not provide easy attribution.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_enable_warning_banner_net
References: |
cjis | 5.5.6
cobit5 | DSS05.04, DSS05.10, DSS06.10
cui | 3.1.9
hipaa | 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii)
isa-62443-2009 | 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9
iso27001-2013 | A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3
nist | AC-8(a), AC-8(c), AC-17(a), CM-6(a)
nist-csf | PR.AC-7
os-srg | SRG-OS-000023-GPOS-00006, SRG-OS-000228-GPOS-00088
cis | 5.1.8
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q kernel-core; then

    mkdir -p /etc/ssh/sshd_config.d
    touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
    chmod 0600 /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf

    LC_ALL=C sed -i "/^\s*Banner\s\+/Id" "/etc/ssh/sshd_config"
    LC_ALL=C sed -i "/^\s*Banner\s\+/Id" "/etc/ssh/sshd_config.d"/*.conf
    if [ -e "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" ] ; then

        LC_ALL=C sed -i "/^\s*Banner\s\+/Id" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    else
        touch "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"

    cp "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak"
    # Insert at the beginning of the file
    printf '%s\n' "Banner /etc/issue.net" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    cat "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.5.6
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(c)
      - NIST-800-53-CM-6(a)
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_enable_warning_banner_net

    - name: Enable SSH Warning Banner - Check if the parameter Banner is configured
      ansible.builtin.find:
        paths:
        - /etc/ssh/sshd_config
        - /etc/ssh/sshd_config.d
        contains: (?i)^\s*{{ "Banner"| regex_escape }}\s+
      register: _sshd_config_has_parameter
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.5.6
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(c)
      - NIST-800-53-CM-6(a)
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_enable_warning_banner_net

    - name: Enable SSH Warning Banner - Check if the parameter Banner is configured correctly
      ansible.builtin.find:
        paths:
        - /etc/ssh/sshd_config
        - /etc/ssh/sshd_config.d
        contains: (?i)^\s*{{ "Banner"| regex_escape }}\s+/etc/issue.net$
      register: _sshd_config_correctly
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.5.6
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(c)
      - NIST-800-53-CM-6(a)
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_enable_warning_banner_net

    - name: Enable SSH Warning Banner
      block:

      - name: Deduplicate values from /etc/ssh/sshd_config
        ansible.builtin.lineinfile:
          path: /etc/ssh/sshd_config
          create: false
          regexp: (?i)(?i)^\s*{{ "Banner"| regex_escape }}\s+
          state: absent

      - name: Check if /etc/ssh/sshd_config.d exists
        ansible.builtin.stat:
          path: /etc/ssh/sshd_config.d
        register: _etc_ssh_sshd_config_d_exists

      - name: Check if the parameter Banner is present in /etc/ssh/sshd_config.d
        ansible.builtin.find:
          paths: /etc/ssh/sshd_config.d
          recurse: 'yes'
          follow: 'no'
          contains: (?i)^\s*{{ "Banner"| regex_escape }}\s+
        register: _etc_ssh_sshd_config_d_has_parameter
        when: _etc_ssh_sshd_config_d_exists.stat.isdir is defined and _etc_ssh_sshd_config_d_exists.stat.isdir

      - name: Remove parameter from files in /etc/ssh/sshd_config.d
        ansible.builtin.lineinfile:
          path: '{{ item.path }}'
          create: false
          regexp: (?i)(?i)^\s*{{ "Banner"| regex_escape }}\s+
          state: absent
        with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}'
        when: _etc_ssh_sshd_config_d_has_parameter.matched > 0

      - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
        ansible.builtin.lineinfile:
          path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
          create: true
          regexp: (?i)(?i)^\s*{{ "Banner"| regex_escape }}\s+
          line: Banner /etc/issue.net
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when:
      - '"kernel-core" in ansible_facts.packages'
      - _sshd_config_correctly.matched == 0 or _sshd_config_has_parameter.matched != 1
      tags:
      - CJIS-5.5.6
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(c)
      - NIST-800-53-CM-6(a)
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_enable_warning_banner_net

    - name: Enable SSH Warning Banner - set file mode for /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
      ansible.builtin.file:
        path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
        mode: '0600'
        state: touch
        modification_time: preserve
        access_time: preserve
      when: '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.5.6
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(c)
      - NIST-800-53-CM-6(a)
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_enable_warning_banner_net
#### Rule Limit Users' SSH Access [ref]
By default, the SSH configuration allows any user with an account to access the system. There are several options available to limit which users and group can access the system via SSH. It is recommended that at least one of the following options be leveraged: - AllowUsers variable gives the system administrator the option of allowing specific users to ssh into the system. The list consists of space separated user names. Numeric user IDs are not recognized with this variable. If a system administrator wants to restrict user access further by specifically allowing a user's access only from a particular host, the entry can be specified in the form of user@host. - AllowGroups variable gives the system administrator the option of allowing specific groups of users to ssh into the system. The list consists of space separated group names. Numeric group IDs are not recognized with this variable. - DenyUsers variable gives the system administrator the option of denying specific users to ssh into the system. The list consists of space separated user names. Numeric user IDs are not recognized with this variable. If a system administrator wants to restrict user access further by specifically denying a user's access from a particular host, the entry can be specified in the form of user@host. - DenyGroups variable gives the system administrator the option of denying specific groups of users to ssh into the system. The list consists of space separated group names. Numeric group IDs are not recognized with this variable. Warning: Automated remediation is not available for this configuration check because each system has unique user names and group names.
Rationale: | Specifying which accounts are allowed SSH access into the system reduces the possibility of unauthorized access to the system.
Severity: | unknown
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_limit_user_access
References: |
cis-csc | 11, 12, 14, 15, 16, 18, 3, 5
cobit5 | DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.03, DSS06.06
cui | 3.1.12
isa-62443-2009 | 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4
isa-62443-2013 | SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7
iso27001-2013 | A.6.1.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5
nerc-cip | CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.2.3, CIP-004-6 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.2, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3
nist | AC-3, CM-6(a)
nist-csf | PR.AC-4, PR.AC-6, PR.PT-3
pcidss | Req-2.2.4
pcidss4 | 2.2.6, 2.2
cis | 5.1.7
Group USBGuard daemon Group contains 3 rules
[ref] The USBGuard daemon enforces the USB device authorization policy for all USB devices.
#### Rule Install usbguard Package [ref]
The `usbguard` package can be installed with the following command:
    $ sudo dnf install usbguard
Rationale: | `usbguard` is a software framework that helps to protect against rogue USB devices by implementing basic whitelisting/blacklisting capabilities based on USB device attributes.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_usbguard_installed
References: |
nist | CM-8(3), IA-3
ospp | FMT_SMF_EXT.1
os-srg | SRG-OS-000378-GPOS-00163
app-srg-ctr | SRG-APP-000141-CTR-000315
ism | 1418
stigref | SV-258035r1045125_rule
Remediation script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    dnf install usbguard
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
      extensions:
        - usbguard
Remediation OSBuild Blueprint snippet ⇲
    [[packages]]
    name = "usbguard"
    version = "*"
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    # Remediation is applicable only in certain platforms
    if ( ! ( grep -sqE "^.*\.s390x$" /proc/sys/kernel/osrelease || grep -sqE "^s390x$" /proc/sys/kernel/arch; ) && rpm --quiet -q kernel-core ); then

    if ! rpm -q --quiet "usbguard" ; then
        dnf install -y "usbguard"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    package install usbguard
Remediation Anaconda snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    package --add=usbguard
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-291015
      - NIST-800-53-CM-8(3)
      - NIST-800-53-IA-3
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_usbguard_installed

    - name: Ensure usbguard is installed
      ansible.builtin.package:
        name: usbguard
        state: present
      when: ( ansible_architecture != "s390x" and "kernel-core" in ansible_facts.packages
        )
      tags:
      - DISA-STIG-RHEL-09-291015
      - NIST-800-53-CM-8(3)
      - NIST-800-53-IA-3
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_usbguard_installed
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    include install_usbguard

    class install_usbguard {
      package { 'usbguard':
        ensure => 'installed',
      }
    }
#### Rule Enable the USBGuard Service [ref]
The USBGuard service should be enabled. The `usbguard` service can be enabled with the following command:
    $ sudo systemctl enable usbguard.service
Rationale: | The `usbguard` service must be running in order to enforce the USB device authorization policy for all USB devices.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_usbguard_enabled
References: |
nist | CM-8(3)(a), IA-3
ospp | FMT_SMF_EXT.1
os-srg | SRG-OS-000378-GPOS-00163
app-srg-ctr | SRG-APP-000141-CTR-000315
ism | 1418
stigref | SV-258036r1014861_rule
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    metadata:
      annotations:
        complianceascode.io/depends-on: xccdf_org.ssgproject.content_rule_package_usbguard_installed
    spec:
      config:
        ignition:
          version: 3.1.0
        systemd:
          units:
          - name: usbguard.service
            enabled: true
Remediation OSBuild Blueprint snippet ⇲
    [customizations.services]
    enabled = ["usbguard"]
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    # Remediation is applicable only in certain platforms
    if ( ! ( grep -sqE "^.*\.s390x$" /proc/sys/kernel/osrelease || grep -sqE "^s390x$" /proc/sys/kernel/arch; ) && rpm --quiet -q kernel-core ); then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" unmask 'usbguard.service'
    if [[ $("$SYSTEMCTL_EXEC" is-system-running) != "offline" ]]; then
      "$SYSTEMCTL_EXEC" start 'usbguard.service'
    fi
    "$SYSTEMCTL_EXEC" enable 'usbguard.service'

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable
    service enable usbguard
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-291020
      - NIST-800-53-CM-8(3)(a)
      - NIST-800-53-IA-3
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_usbguard_enabled

    - name: Enable the USBGuard Service - Enable service usbguard
      block:

      - name: Gather the package facts
        ansible.builtin.package_facts:
          manager: auto

      - name: Enable the USBGuard Service - Enable Service usbguard
        ansible.builtin.systemd:
          name: usbguard
          enabled: true
          state: started
          masked: false
        when:
        - '"usbguard" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-291020
      - NIST-800-53-CM-8(3)(a)
      - NIST-800-53-IA-3
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_usbguard_enabled
      - special_service_block
      when: ( ansible_architecture != "s390x" and "kernel-core" in ansible_facts.packages
        )
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable
    include enable_usbguard

    class enable_usbguard {
      service {'usbguard':
        enable => true,
        ensure => 'running',
      }
    }
#### Rule Generate USBGuard Policy [ref]
By default USBGuard when enabled prevents access to all USB devices and this lead to inaccessible system if they use USB mouse/keyboard. To prevent this scenario, the initial policy configuration must be generated based on current connected USB devices.
Rationale: | The `usbguard` must be configured to allow connected USB devices to work properly, avoiding the system to become inaccessible.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_usbguard_generate_policy
References: |
nist | CM-8(3)(a), IA-3
os-srg | SRG-OS-000378-GPOS-00163
stigref | SV-258038r1045128_rule
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    # Remediation is applicable only in certain platforms
    if ( ! ( grep -sqE "^.*\.s390x$" /proc/sys/kernel/osrelease || grep -sqE "^s390x$" /proc/sys/kernel/arch; ) && rpm --quiet -q kernel-core ); then

    if rpm --quiet -q usbguard
    then
        USBGUARD_CONF=/etc/usbguard/rules.conf
        if [ ! -f "$USBGUARD_CONF" ] || [ ! -s "$USBGUARD_CONF" ]; then
            usbguard generate-policy > $USBGUARD_CONF
            if [ ! -s "$USBGUARD_CONF" ]; then
                # make sure OVAL check doesn't fail on systems where
                # generate-policy doesn't find any USB devices (for
                # example a system might not have a USB bus)
                echo "# No USB devices found" > $USBGUARD_CONF
            fi
            # make sure it has correct permissions
            chmod 600 $USBGUARD_CONF

            SYSTEMCTL_EXEC='/usr/bin/systemctl'
            "$SYSTEMCTL_EXEC" unmask 'usbguard.service'
            "$SYSTEMCTL_EXEC" restart 'usbguard.service'
            "$SYSTEMCTL_EXEC" enable 'usbguard.service'
        fi
    else
        echo "USBGuard is not installed. No remediation was applied!"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-291030
      - NIST-800-53-CM-8(3)(a)
      - NIST-800-53-IA-3
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - usbguard_generate_policy

    - name: Generate USBGuard Policy
      block:

      - name: Gather the package facts
        ansible.builtin.package_facts:
          manager: auto

      - name: Check that the /etc/usbguard/rules.conf exists
        ansible.builtin.stat:
          path: /etc/usbguard/rules.conf
        register: policy_file

      - name: Create USBGuard Policy configuration
        ansible.builtin.command: usbguard generate-policy
        register: policy
        when: not policy_file.stat.exists or policy_file.stat.size == 0

      - name: Copy the Generated Policy configuration to a persistent file
        ansible.builtin.copy:
          content: '{{ policy.stdout }}'
          dest: /etc/usbguard/rules.conf
          mode: 384
        when: not policy_file.stat.exists or policy_file.stat.size == 0

      - name: Add comment into /etc/usbguard/rules.conf when system has no USB devices
        ansible.builtin.lineinfile:
          path: /etc/usbguard/rules.conf
          line: '# No USB devices found'
          state: present
        when: not policy_file.stat.exists or policy_file.stat.size == 0

      - name: Enable service usbguard
        ansible.builtin.systemd:
          name: usbguard
          enabled: 'yes'
          state: started
          masked: 'no'
      when:
      - ( ansible_architecture != "s390x" and "kernel-core" in ansible_facts.packages
        )
      - '"usbguard" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-291030
      - NIST-800-53-CM-8(3)(a)
      - NIST-800-53-IA-3
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - usbguard_generate_policy
Group System Accounting with auditd Group contains 4 groups and 26 rules
[ref] The audit service provides substantial capabilities for recording system activities. By default, the service audits about SELinux AVC denials and certain types of security-relevant events such as system logins, account modifications, and authentication events performed by programs such as sudo. Under its default configuration, `auditd` has modest disk space requirements, and should not noticeably impact system performance. NOTE: The Linux Audit daemon `auditd` can be configured to use the `augenrules` program to read audit rules files (`*.rules`) located in `/etc/audit/rules.d` location and compile them to create the resulting form of the `/etc/audit/audit.rules` configuration file during the daemon startup (default configuration). Alternatively, the `auditd` daemon can use the `auditctl` utility to read audit rules from the `/etc/audit/audit.rules` configuration file during daemon startup, and load them into the kernel. The expected behavior is configured via the appropriate `ExecStartPost` directive setting in the `/usr/lib/systemd/system/auditd.service` configuration file. To instruct the `auditd` daemon to use the `augenrules` program to read audit rules (default configuration), use the following setting:
    ExecStartPost=-/sbin/augenrules --load
in the `/usr/lib/systemd/system/auditd.service` configuration file. In order to instruct the `auditd` daemon to use the `auditctl` utility to read audit rules, use the following setting:
    ExecStartPost=-/sbin/auditctl -R /etc/audit/audit.rules
in the `/usr/lib/systemd/system/auditd.service` configuration file. Refer to `[Service]` section of the `/usr/lib/systemd/system/auditd.service` configuration file for further details. Government networks often have substantial auditing requirements and `auditd` can be configured to meet these requirements. Examining some example audit records demonstrates how the Linux audit system satisfies common requirements. The following example from Red Hat Enterprise Linux 7 Documentation available at `https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html-single/selinux_users_and_administrators_guide/index#sect-Security-Enhanced_Linux-Fixing_Problems-Raw_Audit_Messages` shows the substantial amount of information captured in a two typical "raw" audit messages, followed by a breakdown of the most important fields. In this example the message is SELinux-related and reports an AVC denial (and the associated system call) that occurred when the Apache HTTP Server attempted to access the `/var/www/html/file1` file (labeled with the `samba_share_t` type):
    type=AVC msg=audit(1226874073.147:96): avc:  denied  { getattr } for pid=2465 comm="httpd"
    path="/var/www/html/file1" dev=dm-0 ino=284133 scontext=unconfined_u:system_r:httpd_t:s0
    tcontext=unconfined_u:object_r:samba_share_t:s0 tclass=file

    type=SYSCALL msg=audit(1226874073.147:96): arch=40000003 syscall=196 success=no exit=-13
    a0=b98df198 a1=bfec85dc a2=54dff4 a3=2008171 items=0 ppid=2463 pid=2465 auid=502 uid=48
    gid=48 euid=48 suid=48 fsuid=48 egid=48 sgid=48 fsgid=48 tty=(none) ses=6 comm="httpd"
    exe="/usr/sbin/httpd" subj=unconfined_u:system_r:httpd_t:s0 key=(null)
`msg=audit(1226874073.147:96)` The number in parentheses is the unformatted time stamp (Epoch time) for the event, which can be converted to standard time by using the `date` command. `{ getattr }` The item in braces indicates the permission that was denied. `getattr` indicates the source process was trying to read the target file's status information. This occurs before reading files. This action is denied due to the file being accessed having the wrong label. Commonly seen permissions include `getattr`, `read`, and `write`. `comm="httpd"` The executable that launched the process. The full path of the executable is found in the `exe=` section of the system call (`SYSCALL`) message, which in this case, is `exe="/usr/sbin/httpd"`. `path="/var/www/html/file1"` The path to the object (target) the process attempted to access. `scontext="unconfined_u:system_r:httpd_t:s0"` The SELinux context of the process that attempted the denied action. In this case, it is the SELinux context of the Apache HTTP Server, which is running in the `httpd_t` domain. `tcontext="unconfined_u:object_r:samba_share_t:s0"` The SELinux context of the object (target) the process attempted to access. In this case, it is the SELinux context of `file1`. Note: the `samba_share_t` type is not accessible to processes running in the `httpd_t` domain. From the system call (`SYSCALL`) message, two items are of interest: `success=no`: indicates whether the denial (AVC) was enforced or not. `success=no` indicates the system call was not successful (SELinux denied access). `success=yes` indicates the system call was successful - this can be seen for permissive domains or unconfined domains, such as `initrc_t` and `kernel_t`. `exe="/usr/sbin/httpd"`: the full path to the executable that launched the process, which in this case, is `exe="/usr/sbin/httpd"`.
Group Configure auditd Rules for Comprehensive Auditing Group contains 2 groups and 25 rules
[ref] The `auditd` program can perform comprehensive monitoring of system activity. This section describes recommended configuration settings for comprehensive auditing, but a full description of the auditing system's capabilities is beyond the scope of this guide. The mailing list linux-audit@redhat.com exists to facilitate community discussion of the auditing system. The audit subsystem supports extensive collection of events, including: Tracing of arbitrary system calls (identified by name or number) on entry or exit. Filtering by PID, UID, call success, system call argument (with some limitations), etc. Monitoring of specific files for modifications to the file's contents or metadata. Auditing rules at startup are controlled by the file `/etc/audit/audit.rules`. Add rules to it to meet the auditing requirements for your organization. Each line in `/etc/audit/audit.rules` represents a series of arguments that can be passed to `auditctl` and can be individually tested during runtime. See documentation in `/usr/share/doc/audit-VERSION` and in the related man pages for more details. If copying any example audit rulesets from `/usr/share/doc/audit-VERSION`, be sure to comment out the lines containing `arch=` which are not appropriate for your system's architecture. Then review and understand the following rules, ensuring rules are activated as needed for the appropriate architecture. After reviewing all the rules, reading the following sections, and editing as needed, the new rules can be activated as follows:
    $ sudo service auditd restart
Group Record Events that Modify the System's Discretionary Access Controls Group contains 13 rules
[ref] At a minimum, the audit system should collect file permission changes for all users and root. Note that the "-F arch=b32" lines should be present even on a 64 bit system. These commands identify system calls for auditing. Even if the system is 64 bit it can still execute 32 bit system calls. Additionally, these rules can be configured in a number of ways while still achieving the desired effect. An example of this is that the "-S" calls could be split up and placed on separate lines, however, this is less efficient. Add the following to `/etc/audit/audit.rules`:
    -a always,exit -F arch=b32 -S chmod,fchmod,fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod

        -a always,exit -F arch=b32 -S chown,fchown,fchownat,lchown -F auid>=1000 -F auid!=unset -F key=perm_mod
        -a always,exit -F arch=b32 -S setxattr,lsetxattr,fsetxattr,removexattr,lremovexattr,fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
If your system is 64 bit then these lines should be duplicated and the arch=b32 replaced with arch=b64 as follows:
    -a always,exit -F arch=b64 -S chmod,fchmod,fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod

        -a always,exit -F arch=b64 -S chown,fchown,fchownat,lchown -F auid>=1000 -F auid!=unset -F key=perm_mod
        -a always,exit -F arch=b64 -S setxattr,lsetxattr,fsetxattr,removexattr,lremovexattr,fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
#### Rule Record Events that Modify the System's Discretionary Access Controls - chmod [ref]
At a minimum, the audit system should collect file permission changes for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F arch=b32 -S chmod -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S chmod -F auid>=1000 -F auid!=unset -F key=perm_mod
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:
    -a always,exit -F arch=b32 -S chmod -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S chmod -F auid>=1000 -F auid!=unset -F key=perm_mod
Warning: Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale: | The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_chmod
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2
nist | AU-2(d), AU-12(c), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.5.5
os-srg | SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000466-GPOS-00210, SRG-OS-000458-GPOS-00203
app-srg-ctr | SRG-APP-000091-CTR-000160, SRG-APP-000492-CTR-001220, SRG-APP-000493-CTR-001225, SRG-APP-000494-CTR-001230, SRG-APP-000500-CTR-001260, SRG-APP-000507-CTR-001295, SRG-APP-000495-CTR-001235, SRG-APP-000499-CTR-001255
anssi | R73
ism | 0582
pcidss4 | 10.3.4, 10.3
bsi | SYS.1.1.A10
cis | 6.3.3.9
stigref | SV-258177r1155597_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core && { ( ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ) ); }; then

    # First perform the remediation of the syscall rule
    # Retrieve hardware architecture of the underlying system
    [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    	SYSCALL="chmod"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="chmod fchmod fchmodat"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654015
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_chmod
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Set architecture for audit chmod tasks
      ansible.builtin.set_fact:
        audit_arch: b64
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - not ( ansible_architecture == "aarch64" )
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654015
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_chmod
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for chmod for 32bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - chmod
          syscall_grouping:
          - chmod
          - fchmod
          - fchmodat

      - name: Check existence of chmod in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - chmod
          syscall_grouping:
          - chmod
          - fchmod
          - fchmodat

      - name: Check existence of chmod in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - not ( ansible_architecture == "aarch64" )
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654015
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_chmod
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for chmod for 64bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - chmod
          syscall_grouping:
          - chmod
          - fchmod
          - fchmodat

      - name: Check existence of chmod in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - chmod
          syscall_grouping:
          - chmod
          - fchmod
          - fchmodat

      - name: Check existence of chmod in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - not ( ansible_architecture == "aarch64" )
      - audit_arch == "b64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654015
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_chmod
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy
#### Rule Record Events that Modify the System's Discretionary Access Controls - chown [ref]
At a minimum, the audit system should collect file permission changes for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F arch=b32 -S chown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S chown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:
    -a always,exit -F arch=b32 -S chown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S chown -F auid>=1000 -F auid!=unset -F key=perm_mod
Warning: Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale: | The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_chown
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2
nist | AU-2(d), AU-12(c), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.5.5
os-srg | SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000466-GPOS-00210, SRG-OS-000458-GPOS-00203, SRG-OS-000474-GPOS-00219
app-srg-ctr | SRG-APP-000091-CTR-000160, SRG-APP-000492-CTR-001220, SRG-APP-000493-CTR-001225, SRG-APP-000494-CTR-001230, SRG-APP-000500-CTR-001260, SRG-APP-000507-CTR-001295, SRG-APP-000495-CTR-001235, SRG-APP-000499-CTR-001255
anssi | R73
ism | 0582
pcidss4 | 10.3.4, 10.3
bsi | SYS.1.1.A10
cis | 6.3.3.9
stigref | SV-258178r1155599_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core && { ( ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ) ); }; then

    # First perform the remediation of the syscall rule
    # Retrieve hardware architecture of the underlying system
    [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    	SYSCALL="chown"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="chown fchown fchownat lchown"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654020
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_chown
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Set architecture for audit chown tasks
      ansible.builtin.set_fact:
        audit_arch: b64
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - not ( ansible_architecture == "aarch64" )
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654020
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_chown
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for chown for 32bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - chown
          syscall_grouping:
          - chown
          - fchown
          - fchownat
          - lchown

      - name: Check existence of chown in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - chown
          syscall_grouping:
          - chown
          - fchown
          - fchownat
          - lchown

      - name: Check existence of chown in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - not ( ansible_architecture == "aarch64" )
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654020
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_chown
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for chown for 64bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - chown
          syscall_grouping:
          - chown
          - fchown
          - fchownat
          - lchown

      - name: Check existence of chown in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - chown
          syscall_grouping:
          - chown
          - fchown
          - fchownat
          - lchown

      - name: Check existence of chown in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - not ( ansible_architecture == "aarch64" )
      - audit_arch == "b64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654020
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_chown
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy
#### Rule Record Events that Modify the System's Discretionary Access Controls - fchmod [ref]
At a minimum, the audit system should collect file permission changes for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F arch=b32 -S fchmod -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S fchmod -F auid>=1000 -F auid!=unset -F key=perm_mod
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:
    -a always,exit -F arch=b32 -S fchmod -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S fchmod -F auid>=1000 -F auid!=unset -F key=perm_mod
Warning: Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale: | The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_fchmod
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2
nist | AU-2(d), AU-12(c), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.5.5
os-srg | SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000466-GPOS-00210, SRG-OS-000458-GPOS-00203
app-srg-ctr | SRG-APP-000091-CTR-000160, SRG-APP-000492-CTR-001220, SRG-APP-000493-CTR-001225, SRG-APP-000494-CTR-001230, SRG-APP-000500-CTR-001260, SRG-APP-000507-CTR-001295, SRG-APP-000495-CTR-001235, SRG-APP-000499-CTR-001255
anssi | R73
pcidss4 | 10.3.4, 10.3
bsi | SYS.1.1.A10
cis | 6.3.3.9
stigref | SV-258177r1155597_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # First perform the remediation of the syscall rule
    # Retrieve hardware architecture of the underlying system
    [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    	SYSCALL="fchmod"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="chmod fchmod fchmodat"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654015
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fchmod
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Set architecture for audit fchmod tasks
      ansible.builtin.set_fact:
        audit_arch: b64
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654015
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fchmod
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for fchmod for 32bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fchmod
          syscall_grouping:
          - chmod
          - fchmod
          - fchmodat

      - name: Check existence of fchmod in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fchmod
          syscall_grouping:
          - chmod
          - fchmod
          - fchmodat

      - name: Check existence of fchmod in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654015
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fchmod
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for fchmod for 64bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fchmod
          syscall_grouping:
          - chmod
          - fchmod
          - fchmodat

      - name: Check existence of fchmod in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fchmod
          syscall_grouping:
          - chmod
          - fchmod
          - fchmodat

      - name: Check existence of fchmod in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - audit_arch == "b64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654015
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fchmod
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy
#### Rule Record Events that Modify the System's Discretionary Access Controls - fchmodat [ref]
At a minimum, the audit system should collect file permission changes for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F arch=b32 -S fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:
    -a always,exit -F arch=b32 -S fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod
Warning: Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale: | The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_fchmodat
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2
nist | AU-2(d), AU-12(c), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.5.5
os-srg | SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000466-GPOS-00210, SRG-OS-000458-GPOS-00203
app-srg-ctr | SRG-APP-000091-CTR-000160, SRG-APP-000492-CTR-001220, SRG-APP-000493-CTR-001225, SRG-APP-000494-CTR-001230, SRG-APP-000500-CTR-001260, SRG-APP-000507-CTR-001295, SRG-APP-000495-CTR-001235, SRG-APP-000499-CTR-001255
anssi | R73
pcidss4 | 10.3.4, 10.3
bsi | SYS.1.1.A10
cis | 6.3.3.9
stigref | SV-258177r1155597_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # First perform the remediation of the syscall rule
    # Retrieve hardware architecture of the underlying system
    [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    	SYSCALL="fchmodat"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="chmod fchmod fchmodat"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654015
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fchmodat
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Set architecture for audit fchmodat tasks
      ansible.builtin.set_fact:
        audit_arch: b64
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654015
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fchmodat
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for fchmodat for 32bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fchmodat
          syscall_grouping:
          - chmod
          - fchmod
          - fchmodat

      - name: Check existence of fchmodat in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fchmodat
          syscall_grouping:
          - chmod
          - fchmod
          - fchmodat

      - name: Check existence of fchmodat in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654015
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fchmodat
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for fchmodat for 64bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fchmodat
          syscall_grouping:
          - chmod
          - fchmod
          - fchmodat

      - name: Check existence of fchmodat in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fchmodat
          syscall_grouping:
          - chmod
          - fchmod
          - fchmodat

      - name: Check existence of fchmodat in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - audit_arch == "b64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654015
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fchmodat
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy
#### Rule Record Events that Modify the System's Discretionary Access Controls - fchown [ref]
At a minimum, the audit system should collect file permission changes for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F arch=b32 -S fchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S fchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:
    -a always,exit -F arch=b32 -S fchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S fchown -F auid>=1000 -F auid!=unset -F key=perm_mod
Warning: Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale: | The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_fchown
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2
nist | AU-2(d), AU-12(c), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.5.5
os-srg | SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000466-GPOS-00210, SRG-OS-000458-GPOS-00203, SRG-OS-000474-GPOS-00219
app-srg-ctr | SRG-APP-000091-CTR-000160, SRG-APP-000492-CTR-001220, SRG-APP-000493-CTR-001225, SRG-APP-000494-CTR-001230, SRG-APP-000500-CTR-001260, SRG-APP-000507-CTR-001295, SRG-APP-000495-CTR-001235, SRG-APP-000499-CTR-001255
anssi | R73
pcidss4 | 10.3.4, 10.3
bsi | SYS.1.1.A10
cis | 6.3.3.9
stigref | SV-258178r1155599_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # First perform the remediation of the syscall rule
    # Retrieve hardware architecture of the underlying system
    [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    	SYSCALL="fchown"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="chown fchown fchownat lchown"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654020
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fchown
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Set architecture for audit fchown tasks
      ansible.builtin.set_fact:
        audit_arch: b64
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654020
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fchown
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for fchown for 32bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fchown
          syscall_grouping:
          - chown
          - fchown
          - fchownat
          - lchown

      - name: Check existence of fchown in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fchown
          syscall_grouping:
          - chown
          - fchown
          - fchownat
          - lchown

      - name: Check existence of fchown in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654020
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fchown
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for fchown for 64bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fchown
          syscall_grouping:
          - chown
          - fchown
          - fchownat
          - lchown

      - name: Check existence of fchown in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fchown
          syscall_grouping:
          - chown
          - fchown
          - fchownat
          - lchown

      - name: Check existence of fchown in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - audit_arch == "b64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654020
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fchown
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy
#### Rule Record Events that Modify the System's Discretionary Access Controls - fchownat [ref]
At a minimum, the audit system should collect file permission changes for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F arch=b32 -S fchownat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S fchownat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:
    -a always,exit -F arch=b32 -S fchownat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S fchownat -F auid>=1000 -F auid!=unset -F key=perm_mod
Warning: Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale: | The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_fchownat
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2
nist | AU-2(d), AU-12(c), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.5.5
os-srg | SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000466-GPOS-00210, SRG-OS-000458-GPOS-00203, SRG-OS-000474-GPOS-00219
app-srg-ctr | SRG-APP-000091-CTR-000160, SRG-APP-000492-CTR-001220, SRG-APP-000493-CTR-001225, SRG-APP-000494-CTR-001230, SRG-APP-000500-CTR-001260, SRG-APP-000507-CTR-001295, SRG-APP-000495-CTR-001235, SRG-APP-000499-CTR-001255
anssi | R73
pcidss4 | 10.3.4, 10.3
bsi | SYS.1.1.A10
cis | 6.3.3.9
stigref | SV-258178r1155599_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # First perform the remediation of the syscall rule
    # Retrieve hardware architecture of the underlying system
    [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    	SYSCALL="fchownat"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="chown fchown fchownat lchown"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654020
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fchownat
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Set architecture for audit fchownat tasks
      ansible.builtin.set_fact:
        audit_arch: b64
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654020
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fchownat
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for fchownat for 32bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fchownat
          syscall_grouping:
          - chown
          - fchown
          - fchownat
          - lchown

      - name: Check existence of fchownat in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fchownat
          syscall_grouping:
          - chown
          - fchown
          - fchownat
          - lchown

      - name: Check existence of fchownat in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654020
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fchownat
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for fchownat for 64bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fchownat
          syscall_grouping:
          - chown
          - fchown
          - fchownat
          - lchown

      - name: Check existence of fchownat in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fchownat
          syscall_grouping:
          - chown
          - fchown
          - fchownat
          - lchown

      - name: Check existence of fchownat in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - audit_arch == "b64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654020
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fchownat
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy
#### Rule Record Events that Modify the System's Discretionary Access Controls - fremovexattr [ref]
At a minimum, the audit system should collect file permission changes for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F arch=b32 -S fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b32 -S fremovexattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b64 -S fremovexattr -F auid=0 -F key=perm_mod
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:
    -a always,exit -F arch=b32 -S fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b32 -S fremovexattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b64 -S fremovexattr -F auid=0 -F key=perm_mod
Warning: Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale: | The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_fremovexattr
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2
nist | AU-2(d), AU-12(c), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.5.5
os-srg | SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000458-GPOS-00203, SRG-OS-000462-GPOS-00206, SRG-OS-000463-GPOS-00207, SRG-OS-000471-GPOS-00215, SRG-OS-000474-GPOS-00219, SRG-OS-000466-GPOS-00210, SRG-OS-000468-GPOS-00212, SRG-OS-000064-GPOS-00033
app-srg-ctr | SRG-APP-000091-CTR-000160, SRG-APP-000492-CTR-001220, SRG-APP-000493-CTR-001225, SRG-APP-000494-CTR-001230, SRG-APP-000500-CTR-001260, SRG-APP-000507-CTR-001295, SRG-APP-000495-CTR-001235, SRG-APP-000496-CTR-001240, SRG-APP-000497-CTR-001245, SRG-APP-000498-CTR-001250, SRG-APP-000499-CTR-001255
anssi | R73
pcidss4 | 10.3.4, 10.3
bsi | SYS.1.1.A10
cis | 6.3.3.9
stigref | SV-258179r1155601_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # First perform the remediation of the syscall rule
    # Retrieve hardware architecture of the underlying system
    [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    	SYSCALL="fremovexattr"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid=0"
    	SYSCALL="fremovexattr"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fremovexattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Set architecture for audit fremovexattr tasks
      ansible.builtin.set_fact:
        audit_arch: b64
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fremovexattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for fremovexattr for 32bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fremovexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of fremovexattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fremovexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of fremovexattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fremovexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of fremovexattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fremovexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of fremovexattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fremovexattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for fremovexattr for 64bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fremovexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of fremovexattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fremovexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of fremovexattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fremovexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of fremovexattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fremovexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of fremovexattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - audit_arch == "b64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fremovexattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy
#### Rule Record Events that Modify the System's Discretionary Access Controls - fsetxattr [ref]
At a minimum, the audit system should collect file permission changes for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F arch=b32 -S fsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b32 -S fsetxattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S fsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b64 -S fsetxattr -F auid=0 -F key=perm_mod
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:
    -a always,exit -F arch=b32 -S fsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b32 -S fsetxattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S fsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b64 -S fsetxattr -F auid=0 -F key=perm_mod
Warning: Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale: | The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_fsetxattr
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2
nist | AU-2(d), AU-12(c), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.5.5
os-srg | SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000458-GPOS-00203, SRG-OS-000462-GPOS-00206, SRG-OS-000463-GPOS-00207, SRG-OS-000466-GPOS-00210, SRG-OS-000468-GPOS-00212, SRG-OS-000471-GPOS-00215, SRG-OS-000474-GPOS-00219, SRG-OS-000064-GPOS-00033
app-srg-ctr | SRG-APP-000091-CTR-000160, SRG-APP-000492-CTR-001220, SRG-APP-000493-CTR-001225, SRG-APP-000494-CTR-001230, SRG-APP-000500-CTR-001260, SRG-APP-000507-CTR-001295, SRG-APP-000495-CTR-001235, SRG-APP-000496-CTR-001240, SRG-APP-000497-CTR-001245, SRG-APP-000498-CTR-001250, SRG-APP-000501-CTR-001265, SRG-APP-000502-CTR-001270
anssi | R73
pcidss4 | 10.3.4, 10.3
bsi | SYS.1.1.A10
cis | 6.3.3.9
stigref | SV-258179r1155601_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # First perform the remediation of the syscall rule
    # Retrieve hardware architecture of the underlying system
    [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    	SYSCALL="fsetxattr"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid=0"
    	SYSCALL="fsetxattr"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fsetxattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Set architecture for audit fsetxattr tasks
      ansible.builtin.set_fact:
        audit_arch: b64
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fsetxattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for fsetxattr for 32bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fsetxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of fsetxattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fsetxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of fsetxattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fsetxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of fsetxattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fsetxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of fsetxattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fsetxattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for fsetxattr for 64bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fsetxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of fsetxattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fsetxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of fsetxattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fsetxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of fsetxattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - fsetxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of fsetxattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - audit_arch == "b64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_fsetxattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy
#### Rule Record Events that Modify the System's Discretionary Access Controls - lchown [ref]
At a minimum, the audit system should collect file permission changes for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F arch=b32 -S lchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S lchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:
    -a always,exit -F arch=b32 -S lchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S lchown -F auid>=1000 -F auid!=unset -F key=perm_mod
Warning: Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale: | The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_lchown
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2
nist | AU-2(d), AU-12(c), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.5.5
os-srg | SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000466-GPOS-00210, SRG-OS-000458-GPOS-00203, SRG-OS-000474-GPOS-00219
app-srg-ctr | SRG-APP-000091-CTR-000160, SRG-APP-000492-CTR-001220, SRG-APP-000493-CTR-001225, SRG-APP-000494-CTR-001230, SRG-APP-000500-CTR-001260, SRG-APP-000507-CTR-001295, SRG-APP-000495-CTR-001235, SRG-APP-000499-CTR-001255
anssi | R73
pcidss4 | 10.3.4, 10.3
bsi | SYS.1.1.A10
cis | 6.3.3.9
stigref | SV-258178r1155599_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core && { ( ! ( ( grep -sqE "^.*\.aarch64$" /proc/sys/kernel/osrelease || grep -sqE "^aarch64$" /proc/sys/kernel/arch; ) ) ); }; then

    # First perform the remediation of the syscall rule
    # Retrieve hardware architecture of the underlying system
    [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    	SYSCALL="lchown"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="chown fchown fchownat lchown"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654020
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_lchown
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Set architecture for audit lchown tasks
      ansible.builtin.set_fact:
        audit_arch: b64
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - not ( ansible_architecture == "aarch64" )
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654020
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_lchown
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for lchown for 32bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lchown
          syscall_grouping:
          - chown
          - fchown
          - fchownat
          - lchown

      - name: Check existence of lchown in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lchown
          syscall_grouping:
          - chown
          - fchown
          - fchownat
          - lchown

      - name: Check existence of lchown in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - not ( ansible_architecture == "aarch64" )
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654020
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_lchown
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for lchown for 64bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lchown
          syscall_grouping:
          - chown
          - fchown
          - fchownat
          - lchown

      - name: Check existence of lchown in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lchown
          syscall_grouping:
          - chown
          - fchown
          - fchownat
          - lchown

      - name: Check existence of lchown in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - not ( ansible_architecture == "aarch64" )
      - audit_arch == "b64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654020
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_lchown
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy
#### Rule Record Events that Modify the System's Discretionary Access Controls - lremovexattr [ref]
At a minimum, the audit system should collect file permission changes for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F arch=b32 -S lremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b32 -S lremovexattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S lremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b64 -S lremovexattr -F auid=0 -F key=perm_mod
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:
    -a always,exit -F arch=b32 -S lremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b32 -S lremovexattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S lremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b64 -S lremovexattr -F auid=0 -F key=perm_mod
Warning: Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale: | The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_lremovexattr
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2
nist | AU-2(d), AU-12(c), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.5.5
os-srg | SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000458-GPOS-00203, SRG-OS-000462-GPOS-00206, SRG-OS-000463-GPOS-00207, SRG-OS-000468-GPOS-00212, SRG-OS-000471-GPOS-00215, SRG-OS-000474-GPOS-00219, SRG-OS-000466-GPOS-00210, SRG-OS-000064-GPOS-00033
app-srg-ctr | SRG-APP-000091-CTR-000160, SRG-APP-000492-CTR-001220, SRG-APP-000493-CTR-001225, SRG-APP-000494-CTR-001230, SRG-APP-000500-CTR-001260, SRG-APP-000507-CTR-001295, SRG-APP-000495-CTR-001235, SRG-APP-000496-CTR-001240, SRG-APP-000497-CTR-001245, SRG-APP-000498-CTR-001250, SRG-APP-000499-CTR-001255, SRG-APP-000501-CTR-001265, SRG-APP-000502-CTR-001270
anssi | R73
pcidss4 | 10.3.4, 10.3
bsi | SYS.1.1.A10
cis | 6.3.3.9
stigref | SV-258179r1155601_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # First perform the remediation of the syscall rule
    # Retrieve hardware architecture of the underlying system
    [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    	SYSCALL="lremovexattr"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid=0"
    	SYSCALL="lremovexattr"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_lremovexattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Set architecture for audit lremovexattr tasks
      ansible.builtin.set_fact:
        audit_arch: b64
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_lremovexattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for lremovexattr for 32bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lremovexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of lremovexattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lremovexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of lremovexattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lremovexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of lremovexattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lremovexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of lremovexattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_lremovexattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for lremovexattr for 64bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lremovexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of lremovexattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lremovexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of lremovexattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lremovexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of lremovexattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lremovexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of lremovexattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - audit_arch == "b64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_lremovexattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy
#### Rule Record Events that Modify the System's Discretionary Access Controls - lsetxattr [ref]
At a minimum, the audit system should collect file permission changes for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F arch=b32 -S lsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b32 -S lsetxattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S lsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b64 -S lsetxattr -F auid=0 -F key=perm_mod
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:
    -a always,exit -F arch=b32 -S lsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b32 -S lsetxattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S lsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b64 -S lsetxattr -F auid=0 -F key=perm_mod
Warning: Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale: | The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_lsetxattr
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2
nist | AU-2(d), AU-12(c), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.5.5
os-srg | SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000458-GPOS-00203, SRG-OS-000462-GPOS-00206, SRG-OS-000463-GPOS-00207, SRG-OS-000466-GPOS-00210, SRG-OS-000468-GPOS-00212, SRG-OS-000471-GPOS-00215, SRG-OS-000474-GPOS-00219, SRG-OS-000064-GPOS-00033
app-srg-ctr | SRG-APP-000091-CTR-000160, SRG-APP-000492-CTR-001220, SRG-APP-000493-CTR-001225, SRG-APP-000494-CTR-001230, SRG-APP-000500-CTR-001260, SRG-APP-000507-CTR-001295, SRG-APP-000495-CTR-001235, SRG-APP-000496-CTR-001240, SRG-APP-000497-CTR-001245, SRG-APP-000498-CTR-001250, SRG-APP-000501-CTR-001265, SRG-APP-000502-CTR-001270
anssi | R73
pcidss4 | 10.3.4, 10.3
bsi | SYS.1.1.A10
cis | 6.3.3.9
stigref | SV-258179r1155601_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # First perform the remediation of the syscall rule
    # Retrieve hardware architecture of the underlying system
    [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    	SYSCALL="lsetxattr"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid=0"
    	SYSCALL="lsetxattr"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_lsetxattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Set architecture for audit lsetxattr tasks
      ansible.builtin.set_fact:
        audit_arch: b64
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_lsetxattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for lsetxattr for 32bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lsetxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of lsetxattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lsetxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of lsetxattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lsetxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of lsetxattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lsetxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of lsetxattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_lsetxattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for lsetxattr for 64bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lsetxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of lsetxattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lsetxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of lsetxattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lsetxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of lsetxattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - lsetxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of lsetxattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - audit_arch == "b64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_lsetxattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy
#### Rule Record Events that Modify the System's Discretionary Access Controls - removexattr [ref]
At a minimum, the audit system should collect file permission changes for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F arch=b32 -S removexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b32 -S removexattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S removexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b64 -S removexattr -F auid=0 -F key=perm_mod
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:
    -a always,exit -F arch=b32 -S removexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b32 -S removexattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S removexattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b64 -S removexattr -F auid=0 -F key=perm_mod
Warning: Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale: | The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_removexattr
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2
nist | AU-2(d), AU-12(c), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.5.5
os-srg | SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000458-GPOS-00203, SRG-OS-000462-GPOS-00206, SRG-OS-000463-GPOS-00207, SRG-OS-000468-GPOS-00212, SRG-OS-000471-GPOS-00215, SRG-OS-000474-GPOS-00219, SRG-OS-000466-GPOS-00210, SRG-OS-000064-GPOS-00033
app-srg-ctr | SRG-APP-000091-CTR-000160, SRG-APP-000492-CTR-001220, SRG-APP-000493-CTR-001225, SRG-APP-000494-CTR-001230, SRG-APP-000500-CTR-001260, SRG-APP-000507-CTR-001295, SRG-APP-000495-CTR-001235, SRG-APP-000496-CTR-001240, SRG-APP-000497-CTR-001245, SRG-APP-000498-CTR-001250, SRG-APP-000499-CTR-001255, SRG-APP-000501-CTR-001265, SRG-APP-000502-CTR-001270
anssi | R73
pcidss4 | 10.3.4, 10.3
bsi | SYS.1.1.A10
cis | 6.3.3.9
stigref | SV-258179r1155601_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # First perform the remediation of the syscall rule
    # Retrieve hardware architecture of the underlying system
    [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    	SYSCALL="removexattr"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid=0"
    	SYSCALL="removexattr"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_removexattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Set architecture for audit removexattr tasks
      ansible.builtin.set_fact:
        audit_arch: b64
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_removexattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for removexattr for 32bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - removexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of removexattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - removexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of removexattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - removexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of removexattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - removexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of removexattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_removexattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for removexattr for 64bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - removexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of removexattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - removexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of removexattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - removexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of removexattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - removexattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of removexattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - audit_arch == "b64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_removexattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy
#### Rule Record Events that Modify the System's Discretionary Access Controls - setxattr [ref]
At a minimum, the audit system should collect file permission changes for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F arch=b32 -S setxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b32 -S setxattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S setxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b64 -S setxattr -F auid=0 -F key=perm_mod
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:
    -a always,exit -F arch=b32 -S setxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b32 -S setxattr -F auid=0 -F key=perm_mod
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S setxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
    -a always,exit -F arch=b64 -S setxattr -F auid=0 -F key=perm_mod
Warning: Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale: | The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_setxattr
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2
nist | AU-2(d), AU-12(c), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.5.5
os-srg | SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000466-GPOS-00210, SRG-OS-000471-GPOS-00215, SRG-OS-000064-GPOS-00033, SRG-OS-000458-GPOS-00203
app-srg-ctr | SRG-APP-000091-CTR-000160, SRG-APP-000492-CTR-001220, SRG-APP-000493-CTR-001225, SRG-APP-000494-CTR-001230, SRG-APP-000500-CTR-001260, SRG-APP-000507-CTR-001295, SRG-APP-000495-CTR-001235
anssi | R73
pcidss4 | 10.3.4, 10.3
bsi | SYS.1.1.A10
cis | 6.3.3.9
stigref | SV-258179r1155601_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # First perform the remediation of the syscall rule
    # Retrieve hardware architecture of the underlying system
    [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    	SYSCALL="setxattr"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS=""
    	AUID_FILTERS="-F auid=0"
    	SYSCALL="setxattr"
    	KEY="perm_mod"
    	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

    	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0600 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    	unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod 0600 ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_setxattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Set architecture for audit setxattr tasks
      ansible.builtin.set_fact:
        audit_arch: b64
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_setxattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for setxattr for 32bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - setxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of setxattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - setxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of setxattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - setxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of setxattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - setxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of setxattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_setxattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Perform remediation of Audit rules for setxattr for 64bit platform
      block:

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - setxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of setxattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - setxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of setxattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k |-F
            key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
            -F auid!=unset -F key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - setxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of setxattr in /etc/audit/rules.d/
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        ansible.builtin.set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        ansible.builtin.set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        ansible.builtin.set_fact: found_paths="{{ find_command.results | map(attribute='files')
          | flatten | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        ansible.builtin.set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        ansible.builtin.set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
        when: found_paths | length == 0

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
            | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0

      - name: Declare list of syscalls
        ansible.builtin.set_fact:
          syscalls:
          - setxattr
          syscall_grouping:
          - fremovexattr
          - lremovexattr
          - removexattr
          - fsetxattr
          - lsetxattr
          - setxattr

      - name: Check existence of setxattr in /etc/audit/audit.rules
        ansible.builtin.find:
          paths: /etc/audit
          contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+(( -S
            |,)\w+)* -F auid=0 (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        ansible.builtin.set_fact: audit_file="/etc/audit/audit.rules"

      - name: Declare found syscalls
        ansible.builtin.set_fact: syscalls_found="{{ find_command.results | selectattr('matched')
          | map(attribute='item') | list }}"

      - name: Declare missing syscalls
        ansible.builtin.set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found)
          }}"

      - name: Replace the audit rule in {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found |
            join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid=0 (?:-k |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
          mode: g-rwx,o-rwx
        when: syscalls_found | length > 0 and missing_syscalls | length > 0

      - name: Add the audit rule to {{ audit_file }}
        ansible.builtin.lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
            key=perm_mod
          create: true
          mode: g-rwx,o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - audit_arch == "b64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654025
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_setxattr
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy
Group Record Attempts to Alter Logon and Logout Events Group contains 2 rules
[ref] The audit system already collects login information for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d` in order to watch for attempted manual edits of files involved in storing logon events:
    -w /var/log/tallylog -p wa -k logins
    -w /var/log/faillock -p wa -k logins
    -w /var/log/lastlog -p wa -k logins
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules` file in order to watch for unattempted manual edits of files involved in storing logon events:
    -w /var/log/tallylog -p wa -k logins
    -w /var/log/faillock -p wa -k logins
    -w /var/log/lastlog -p wa -k logins
#### Rule Record Attempts to Alter Logon and Logout Events - faillock [ref]
The audit system already collects login information for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -w /var/log/faillock -p wa -k logins
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules`:
    -w /var/log/faillock -p wa -k logins
Rationale: | Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_login_events_faillock
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2
nist | AU-2(d), AU-12(c), AC-6(9), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.2.3
os-srg | SRG-OS-000392-GPOS-00172, SRG-OS-000470-GPOS-00214, SRG-OS-000473-GPOS-00218
app-srg-ctr | SRG-APP-000503-CTR-001275, SRG-APP-000506-CTR-001290
anssi | R73
ism | 0582
pcidss4 | 10.2.1.3, 10.2.1, 10.2
bsi | SYS.1.1.A10
cis | 6.3.3.12
stigref | SV-258224r1014988_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

    var_accounts_passwords_pam_faillock_dir='/var/log/faillock'

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+${var_accounts_passwords_pam_faillock_dir}" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir} $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir}$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w ${var_accounts_passwords_pam_faillock_dir} -p wa -k logins" >> "$audit_rules_file"

        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/logins.rules' to list of files for inspection.

    readarray -t matches < <(grep -HP "[\s]*-w[\s]+${var_accounts_passwords_pam_faillock_dir}" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/logins.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/logins.rules"
        # If the logins.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0600 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+${var_accounts_passwords_pam_faillock_dir}" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir} $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir}$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w ${var_accounts_passwords_pam_faillock_dir} -p wa -k logins" >> "$audit_rules_file"

        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-654250
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_faillock
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_passwords_pam_faillock_dir # promote to variable
      set_fact:
        var_accounts_passwords_pam_faillock_dir: !!str /var/log/faillock
      tags:
        - always

    - name: Record Attempts to Alter Logon and Logout Events - faillock - Check if watch
        rule for {{ var_accounts_passwords_pam_faillock_dir }} already exists in /etc/audit/rules.d/
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+{{ var_accounts_passwords_pam_faillock_dir }}\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-654250
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_faillock
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Logon and Logout Events - faillock - Search /etc/audit/rules.d
        for other rules with specified key logins
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)logins$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - DISA-STIG-RHEL-09-654250
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_faillock
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Logon and Logout Events - faillock - Use /etc/audit/rules.d/logins.rules
        as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - /etc/audit/rules.d/logins.rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - DISA-STIG-RHEL-09-654250
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_faillock
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Logon and Logout Events - faillock - Use matched
        file as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - DISA-STIG-RHEL-09-654250
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_faillock
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Logon and Logout Events - faillock - Add watch rule
        for {{ var_accounts_passwords_pam_faillock_dir }} in /etc/audit/rules.d/
      ansible.builtin.lineinfile:
        path: '{{ all_files[0] }}'
        line: -w {{ var_accounts_passwords_pam_faillock_dir }} -p wa -k logins
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - DISA-STIG-RHEL-09-654250
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_faillock
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Logon and Logout Events - faillock - Check if watch
        rule for {{ var_accounts_passwords_pam_faillock_dir }} already exists in /etc/audit/audit.rules
      ansible.builtin.find:
        paths: /etc/audit/
        contains: ^\s*-w\s+{{ var_accounts_passwords_pam_faillock_dir }}\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-654250
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_faillock
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Logon and Logout Events - faillock - Add watch rule
        for {{ var_accounts_passwords_pam_faillock_dir }} in /etc/audit/audit.rules
      ansible.builtin.lineinfile:
        line: -w {{ var_accounts_passwords_pam_faillock_dir }} -p wa -k logins
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - DISA-STIG-RHEL-09-654250
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_faillock
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Record Attempts to Alter Logon and Logout Events - lastlog [ref]
The audit system already collects login information for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -w /var/log/lastlog -p wa -k logins
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules`:
    -w /var/log/lastlog -p wa -k logins
Rationale: | Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_login_events_lastlog
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2
nist | AU-2(d), AU-12(c), AC-6(9), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.2.3
os-srg | SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000473-GPOS-00218, SRG-OS-000470-GPOS-00214
app-srg-ctr | SRG-APP-000495-CTR-001235, SRG-APP-000503-CTR-001275, SRG-APP-000506-CTR-001290
anssi | R73
ism | 0582
pcidss4 | 10.2.1.3, 10.2.1, 10.2
bsi | SYS.1.1.A10
cis | 6.3.3.12
stigref | SV-258225r1014990_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/var/log/lastlog" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/lastlog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/var/log/lastlog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/lastlog -p wa -k logins" >> "$audit_rules_file"

        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/logins.rules' to list of files for inspection.

    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/lastlog" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/logins.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/logins.rules"
        # If the logins.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0600 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/var/log/lastlog" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/lastlog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/var/log/lastlog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/lastlog -p wa -k logins" >> "$audit_rules_file"

        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-09-654255
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_lastlog
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Logon and Logout Events - lastlog - Check if watch
        rule for /var/log/lastlog already exists in /etc/audit/rules.d/
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/var/log/lastlog\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-654255
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_lastlog
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Logon and Logout Events - lastlog - Search /etc/audit/rules.d
        for other rules with specified key logins
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)logins$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - DISA-STIG-RHEL-09-654255
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_lastlog
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Logon and Logout Events - lastlog - Use /etc/audit/rules.d/logins.rules
        as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - /etc/audit/rules.d/logins.rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - DISA-STIG-RHEL-09-654255
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_lastlog
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Logon and Logout Events - lastlog - Use matched file
        as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - DISA-STIG-RHEL-09-654255
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_lastlog
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Logon and Logout Events - lastlog - Add watch rule
        for /var/log/lastlog in /etc/audit/rules.d/
      ansible.builtin.lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /var/log/lastlog -p wa -k logins
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - DISA-STIG-RHEL-09-654255
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_lastlog
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Logon and Logout Events - lastlog - Check if watch
        rule for /var/log/lastlog already exists in /etc/audit/audit.rules
      ansible.builtin.find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/var/log/lastlog\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-09-654255
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_lastlog
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Logon and Logout Events - lastlog - Add watch rule
        for /var/log/lastlog in /etc/audit/audit.rules
      ansible.builtin.lineinfile:
        line: -w /var/log/lastlog -p wa -k logins
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - DISA-STIG-RHEL-09-654255
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_lastlog
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Record Attempts to Alter Process and Session Initiation Information btmp [ref]
The audit system already collects process information for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -w /var/log/btmp -p wa -k session
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules`:
    -w /var/log/btmp -p wa -k session
Rationale: | Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_session_events_btmp
References: |
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
nist | AU-12(c), AU-12.1(iv)
os-srg | SRG-OS-000472-GPOS-00217
anssi | R73
ism | 0582, 0846
pcidss4 | 10.2.1.3, 10.2.1, 10.2
cis | 6.3.3.11
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/var/log/btmp" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/btmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/var/log/btmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/btmp -p wa -k session" >> "$audit_rules_file"

        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/session.rules' to list of files for inspection.

    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/btmp" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/session.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/session.rules"
        # If the session.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0600 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/var/log/btmp" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/btmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/var/log/btmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/btmp -p wa -k session" >> "$audit_rules_file"

        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_btmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information btmp -
        Check if watch rule for /var/log/btmp already exists in /etc/audit/rules.d/
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/var/log/btmp\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_btmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information btmp -
        Search /etc/audit/rules.d for other rules with specified key session
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)session$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_btmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information btmp -
        Use /etc/audit/rules.d/session.rules as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - /etc/audit/rules.d/session.rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_btmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information btmp -
        Use matched file as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_btmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information btmp -
        Add watch rule for /var/log/btmp in /etc/audit/rules.d/
      ansible.builtin.lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /var/log/btmp -p wa -k session
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_btmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information btmp -
        Check if watch rule for /var/log/btmp already exists in /etc/audit/audit.rules
      ansible.builtin.find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/var/log/btmp\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_btmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information btmp -
        Add watch rule for /var/log/btmp in /etc/audit/audit.rules
      ansible.builtin.lineinfile:
        line: -w /var/log/btmp -p wa -k session
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_btmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Record Attempts to Alter Process and Session Initiation Information utmp [ref]
The audit system already collects process information for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -w /var/run/utmp -p wa -k session
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules`:
    -w /var/run/utmp -p wa -k session
Rationale: | Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_session_events_utmp
References: |
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
nist | AU-12(c), AU-12.1(iv)
os-srg | SRG-OS-000472-GPOS-00217
anssi | R73
ism | 0582, 0846
pcidss4 | 10.2.1.3, 10.2.1, 10.2
cis | 6.3.3.11
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/var/run/utmp" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/run/utmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/var/run/utmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/run/utmp -p wa -k session" >> "$audit_rules_file"

        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/session.rules' to list of files for inspection.

    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/run/utmp" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/session.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/session.rules"
        # If the session.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0600 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/var/run/utmp" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/run/utmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/var/run/utmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/run/utmp -p wa -k session" >> "$audit_rules_file"

        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_utmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information utmp -
        Check if watch rule for /var/run/utmp already exists in /etc/audit/rules.d/
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/var/run/utmp\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_utmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information utmp -
        Search /etc/audit/rules.d for other rules with specified key session
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)session$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_utmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information utmp -
        Use /etc/audit/rules.d/session.rules as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - /etc/audit/rules.d/session.rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_utmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information utmp -
        Use matched file as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_utmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information utmp -
        Add watch rule for /var/run/utmp in /etc/audit/rules.d/
      ansible.builtin.lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /var/run/utmp -p wa -k session
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_utmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information utmp -
        Check if watch rule for /var/run/utmp already exists in /etc/audit/audit.rules
      ansible.builtin.find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/var/run/utmp\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_utmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information utmp -
        Add watch rule for /var/run/utmp in /etc/audit/audit.rules
      ansible.builtin.lineinfile:
        line: -w /var/run/utmp -p wa -k session
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_utmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Record Attempts to Alter Process and Session Initiation Information wtmp [ref]
The audit system already collects process information for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -w /var/log/wtmp -p wa -k session
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules`:
    -w /var/log/wtmp -p wa -k session
Rationale: | Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_session_events_wtmp
References: |
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
nist | AU-12(c), AU-12.1(iv)
os-srg | SRG-OS-000472-GPOS-00217
anssi | R73
ism | 0582, 0846
pcidss4 | 10.2.1.3, 10.2.1, 10.2
cis | 6.3.3.11
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/var/log/wtmp" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/wtmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/var/log/wtmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/wtmp -p wa -k session" >> "$audit_rules_file"

        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/session.rules' to list of files for inspection.

    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/wtmp" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/session.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/session.rules"
        # If the session.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0600 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/var/log/wtmp" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/wtmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/var/log/wtmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/wtmp -p wa -k session" >> "$audit_rules_file"

        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_wtmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information wtmp -
        Check if watch rule for /var/log/wtmp already exists in /etc/audit/rules.d/
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/var/log/wtmp\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_wtmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information wtmp -
        Search /etc/audit/rules.d for other rules with specified key session
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)session$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_wtmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information wtmp -
        Use /etc/audit/rules.d/session.rules as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - /etc/audit/rules.d/session.rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_wtmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information wtmp -
        Use matched file as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_wtmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information wtmp -
        Add watch rule for /var/log/wtmp in /etc/audit/rules.d/
      ansible.builtin.lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /var/log/wtmp -p wa -k session
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_wtmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information wtmp -
        Check if watch rule for /var/log/wtmp already exists in /etc/audit/audit.rules
      ansible.builtin.find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/var/log/wtmp\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_wtmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to Alter Process and Session Initiation Information wtmp -
        Add watch rule for /var/log/wtmp in /etc/audit/audit.rules
      ansible.builtin.lineinfile:
        line: -w /var/log/wtmp -p wa -k session
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-12.1(iv)
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events_wtmp
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Ensure auditd Collects System Administrator Actions [ref]
If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -w /etc/sudoers -p wa -k actions
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules`:
    -w /etc/sudoers -p wa -k actions
If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -w /etc/sudoers.d/ -p wa -k actions
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules`:
    -w /etc/sudoers.d/ -p wa -k actions
Rationale: | The actions taken by system administrators should be audited to keep a record of what was executed on the system, as well as, for accountability purposes.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_sysadmin_actions
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS06.03, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.2.2, 4.3.3.3.9, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.8, 4.3.3.6.6, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.1, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.6.2.1, A.6.2.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5
nist | AC-2(7)(b), AU-2(d), AU-12(c), AC-6(9), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-1, PR.AC-3, PR.AC-4, PR.AC-6, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.2.2, Req-10.2.5.b
os-srg | SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000304-GPOS-00121, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221
app-srg-ctr | SRG-APP-000026-CTR-000070, SRG-APP-000027-CTR-000075, SRG-APP-000028-CTR-000080, SRG-APP-000291-CTR-000675, SRG-APP-000292-CTR-000680, SRG-APP-000293-CTR-000685, SRG-APP-000294-CTR-000690, SRG-APP-000319-CTR-000745, SRG-APP-000320-CTR-000750, SRG-APP-000509-CTR-001305
anssi | R73
ism | 0582
pcidss4 | 10.2.1.5, 10.2.1, 10.2
bsi | SYS.1.1.A10
cis | 6.3.3.1
Remediation Kubernetes snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ -w%20/etc/sudoers.d/%20-p%20wa%20-k%20actions%0A-w%20/etc/sudoers%20-p%20wa%20-k%20actions%0A }}
            mode: 0600
            path: /etc/audit/rules.d/75-audit-sysadmin-actions.rules
            overwrite: true
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/etc/sudoers" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/etc/sudoers$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /etc/sudoers -p wa -k actions" >> "$audit_rules_file"

        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/actions.rules' to list of files for inspection.

    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/sudoers" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/actions.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/actions.rules"
        # If the actions.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0600 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/etc/sudoers" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/etc/sudoers$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /etc/sudoers -p wa -k actions" >> "$audit_rules_file"

        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/etc/sudoers.d/" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers.d/ $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/etc/sudoers.d/$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /etc/sudoers.d/ -p wa -k actions" >> "$audit_rules_file"

        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/actions.rules' to list of files for inspection.

    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/sudoers.d/" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/actions.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/actions.rules"
        # If the actions.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0600 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/etc/sudoers.d/" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers.d/ $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/etc/sudoers.d/$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /etc/sudoers.d/ -p wa -k actions" >> "$audit_rules_file"

        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure auditd Collects System Administrator Actions - Check if watch rule
        for /etc/sudoers already exists in /etc/audit/audit.rules
      ansible.builtin.find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/etc/sudoers\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure auditd Collects System Administrator Actions - Add watch rule for /etc/sudoers
        in /etc/audit/audit.rules
      ansible.builtin.lineinfile:
        line: -w /etc/sudoers -p wa -k actions
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure auditd Collects System Administrator Actions - Check if watch rule
        for /etc/sudoers already exists in /etc/audit/rules.d/
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/etc/sudoers\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure auditd Collects System Administrator Actions - Search /etc/audit/rules.d
        for other rules with specified key actions
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)actions$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure auditd Collects System Administrator Actions - Use /etc/audit/rules.d/actions.rules
        as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - /etc/audit/rules.d/actions.rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure auditd Collects System Administrator Actions - Use matched file as
        the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure auditd Collects System Administrator Actions - Add watch rule for /etc/sudoers
        in /etc/audit/rules.d/
      ansible.builtin.lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/sudoers -p wa -k actions
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure auditd Collects System Administrator Actions - Check if watch rule
        for /etc/sudoers.d/ already exists in /etc/audit/audit.rules
      ansible.builtin.find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/etc/sudoers.d/\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure auditd Collects System Administrator Actions - Add watch rule for /etc/sudoers.d/
        in /etc/audit/audit.rules
      ansible.builtin.lineinfile:
        line: -w /etc/sudoers.d/ -p wa -k actions
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure auditd Collects System Administrator Actions - Check if watch rule
        for /etc/sudoers.d/ already exists in /etc/audit/rules.d/
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/etc/sudoers.d/\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure auditd Collects System Administrator Actions - Search /etc/audit/rules.d
        for other rules with specified key actions
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)actions$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure auditd Collects System Administrator Actions - Use /etc/audit/rules.d/actions.rules
        as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - /etc/audit/rules.d/actions.rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure auditd Collects System Administrator Actions - Use matched file as
        the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure auditd Collects System Administrator Actions - Add watch rule for /etc/sudoers.d/
        in /etc/audit/rules.d/
      ansible.builtin.lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/sudoers.d/ -p wa -k actions
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Record Events that Modify User/Group Information - /etc/group [ref]
If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -w /etc/group -p wa -k audit_rules_usergroup_modification
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules`:
    -w /etc/group -p wa -k audit_rules_usergroup_modification
Rationale: | In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_usergroup_modification_group
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS06.03, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.2.2, 4.3.3.3.9, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.8, 4.3.3.6.6, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.1, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.6.2.1, A.6.2.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5
nerc-cip | CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3
nist | AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-1, PR.AC-3, PR.AC-4, PR.AC-6, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.2.5
os-srg | SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221
app-srg-ctr | SRG-APP-000495-CTR-001235, SRG-APP-000499-CTR-001255, SRG-APP-000503-CTR-001275
anssi | R73
ism | 0582
pcidss4 | 10.2.1.5, 10.2.1, 10.2
bsi | SYS.1.1.A10, SYS.1.3.A6
cis | 6.3.3.8
stigref | SV-258219r1015130_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/etc/group" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/group $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/etc/group$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /etc/group -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"

        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' to list of files for inspection.

    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/group" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/audit_rules_usergroup_modification.rules"
        # If the audit_rules_usergroup_modification.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0600 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/etc/group" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/group $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/etc/group$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /etc/group -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"

        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654225
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_group
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/group - Check if watch
        rule for /etc/group already exists in /etc/audit/rules.d/
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/etc/group\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654225
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_group
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/group - Search /etc/audit/rules.d
        for other rules with specified key audit_rules_usergroup_modification
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)audit_rules_usergroup_modification$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654225
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_group
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/group - Use /etc/audit/rules.d/audit_rules_usergroup_modification.rules
        as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - /etc/audit/rules.d/audit_rules_usergroup_modification.rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654225
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_group
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/group - Use matched
        file as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654225
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_group
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/group - Add watch
        rule for /etc/group in /etc/audit/rules.d/
      ansible.builtin.lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/group -p wa -k audit_rules_usergroup_modification
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654225
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_group
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/group - Check if watch
        rule for /etc/group already exists in /etc/audit/audit.rules
      ansible.builtin.find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/etc/group\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654225
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_group
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/group - Add watch
        rule for /etc/group in /etc/audit/audit.rules
      ansible.builtin.lineinfile:
        line: -w /etc/group -p wa -k audit_rules_usergroup_modification
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654225
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_group
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Record Events that Modify User/Group Information - /etc/gshadow [ref]
If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -w /etc/gshadow -p wa -k audit_rules_usergroup_modification
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules`:
    -w /etc/gshadow -p wa -k audit_rules_usergroup_modification
Rationale: | In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_usergroup_modification_gshadow
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS06.03, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.2.2, 4.3.3.3.9, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.8, 4.3.3.6.6, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.1, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.6.2.1, A.6.2.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5
nerc-cip | CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3
nist | AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-1, PR.AC-3, PR.AC-4, PR.AC-6, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.2.5
os-srg | SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221
app-srg-ctr | SRG-APP-000495-CTR-001235, SRG-APP-000499-CTR-001255, SRG-APP-000503-CTR-001275
anssi | R73
ism | 0582
pcidss4 | 10.2.1.5, 10.2.1, 10.2
bsi | SYS.1.1.A10, SYS.1.3.A6
cis | 6.3.3.8
stigref | SV-258220r1015131_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/etc/gshadow" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/gshadow $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/etc/gshadow$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /etc/gshadow -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"

        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' to list of files for inspection.

    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/gshadow" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/audit_rules_usergroup_modification.rules"
        # If the audit_rules_usergroup_modification.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0600 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/etc/gshadow" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/gshadow $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/etc/gshadow$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /etc/gshadow -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"

        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654230
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_gshadow
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/gshadow - Check if
        watch rule for /etc/gshadow already exists in /etc/audit/rules.d/
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/etc/gshadow\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654230
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_gshadow
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/gshadow - Search /etc/audit/rules.d
        for other rules with specified key audit_rules_usergroup_modification
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)audit_rules_usergroup_modification$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654230
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_gshadow
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/gshadow - Use /etc/audit/rules.d/audit_rules_usergroup_modification.rules
        as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - /etc/audit/rules.d/audit_rules_usergroup_modification.rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654230
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_gshadow
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/gshadow - Use matched
        file as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654230
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_gshadow
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/gshadow - Add watch
        rule for /etc/gshadow in /etc/audit/rules.d/
      ansible.builtin.lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/gshadow -p wa -k audit_rules_usergroup_modification
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654230
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_gshadow
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/gshadow - Check if
        watch rule for /etc/gshadow already exists in /etc/audit/audit.rules
      ansible.builtin.find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/etc/gshadow\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654230
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_gshadow
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/gshadow - Add watch
        rule for /etc/gshadow in /etc/audit/audit.rules
      ansible.builtin.lineinfile:
        line: -w /etc/gshadow -p wa -k audit_rules_usergroup_modification
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654230
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_gshadow
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Record Events that Modify User/Group Information - /etc/security/opasswd [ref]
If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules`:
    -w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification
Rationale: | In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_usergroup_modification_opasswd
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS06.03, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.2.2, 4.3.3.3.9, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.8, 4.3.3.6.6, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.1, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.6.2.1, A.6.2.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5
nerc-cip | CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3
nist | AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-1, PR.AC-3, PR.AC-4, PR.AC-6, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.2.5
os-srg | SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221
app-srg-ctr | SRG-APP-000495-CTR-001235, SRG-APP-000496-CTR-001240, SRG-APP-000497-CTR-001245, SRG-APP-000498-CTR-001250, SRG-APP-000503-CTR-001275
anssi | R73
ism | 0582
pcidss4 | 10.2.1.5, 10.2.1, 10.2
bsi | SYS.1.1.A10, SYS.1.3.A6
cis | 6.3.3.8
stigref | SV-258221r1015132_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/etc/security/opasswd" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/security/opasswd $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/etc/security/opasswd$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"

        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' to list of files for inspection.

    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/security/opasswd" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/audit_rules_usergroup_modification.rules"
        # If the audit_rules_usergroup_modification.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0600 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/etc/security/opasswd" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/security/opasswd $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/etc/security/opasswd$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"

        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654235
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_opasswd
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/security/opasswd -
        Check if watch rule for /etc/security/opasswd already exists in /etc/audit/rules.d/
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/etc/security/opasswd\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654235
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_opasswd
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/security/opasswd -
        Search /etc/audit/rules.d for other rules with specified key audit_rules_usergroup_modification
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)audit_rules_usergroup_modification$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654235
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_opasswd
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/security/opasswd -
        Use /etc/audit/rules.d/audit_rules_usergroup_modification.rules as the recipient
        for the rule
      ansible.builtin.set_fact:
        all_files:
        - /etc/audit/rules.d/audit_rules_usergroup_modification.rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654235
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_opasswd
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/security/opasswd -
        Use matched file as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654235
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_opasswd
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/security/opasswd -
        Add watch rule for /etc/security/opasswd in /etc/audit/rules.d/
      ansible.builtin.lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654235
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_opasswd
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/security/opasswd -
        Check if watch rule for /etc/security/opasswd already exists in /etc/audit/audit.rules
      ansible.builtin.find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/etc/security/opasswd\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654235
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_opasswd
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/security/opasswd -
        Add watch rule for /etc/security/opasswd in /etc/audit/audit.rules
      ansible.builtin.lineinfile:
        line: -w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654235
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_opasswd
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Record Events that Modify User/Group Information - /etc/passwd [ref]
If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -w /etc/passwd -p wa -k audit_rules_usergroup_modification
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules`:
    -w /etc/passwd -p wa -k audit_rules_usergroup_modification
Rationale: | In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_usergroup_modification_passwd
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS06.03, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.2.2, 4.3.3.3.9, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.8, 4.3.3.6.6, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.1, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.6.2.1, A.6.2.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5
nerc-cip | CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3
nist | AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-1, PR.AC-3, PR.AC-4, PR.AC-6, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.2.5
os-srg | SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000304-GPOS-00121, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221, SRG-OS-000274-GPOS-00104, SRG-OS-000275-GPOS-00105, SRG-OS-000276-GPOS-00106, SRG-OS-000277-GPOS-00107
app-srg-ctr | SRG-APP-000495-CTR-001235, SRG-APP-000499-CTR-001255, SRG-APP-000503-CTR-001275
anssi | R73
ism | 0582
pcidss4 | 10.2.1.5, 10.2.1, 10.2
bsi | SYS.1.1.A10, SYS.1.3.A6
cis | 6.3.3.8
stigref | SV-258222r1015133_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/etc/passwd" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/passwd $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/etc/passwd$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /etc/passwd -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"

        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' to list of files for inspection.

    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/passwd" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/audit_rules_usergroup_modification.rules"
        # If the audit_rules_usergroup_modification.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0600 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/etc/passwd" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/passwd $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/etc/passwd$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /etc/passwd -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"

        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654240
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_passwd
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/passwd - Check if
        watch rule for /etc/passwd already exists in /etc/audit/rules.d/
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/etc/passwd\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654240
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_passwd
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/passwd - Search /etc/audit/rules.d
        for other rules with specified key audit_rules_usergroup_modification
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)audit_rules_usergroup_modification$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654240
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_passwd
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/passwd - Use /etc/audit/rules.d/audit_rules_usergroup_modification.rules
        as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - /etc/audit/rules.d/audit_rules_usergroup_modification.rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654240
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_passwd
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/passwd - Use matched
        file as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654240
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_passwd
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/passwd - Add watch
        rule for /etc/passwd in /etc/audit/rules.d/
      ansible.builtin.lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/passwd -p wa -k audit_rules_usergroup_modification
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654240
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_passwd
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/passwd - Check if
        watch rule for /etc/passwd already exists in /etc/audit/audit.rules
      ansible.builtin.find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/etc/passwd\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654240
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_passwd
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/passwd - Add watch
        rule for /etc/passwd in /etc/audit/audit.rules
      ansible.builtin.lineinfile:
        line: -w /etc/passwd -p wa -k audit_rules_usergroup_modification
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654240
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_passwd
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Record Events that Modify User/Group Information - /etc/shadow [ref]
If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -w /etc/shadow -p wa -k audit_rules_usergroup_modification
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules`:
    -w /etc/shadow -p wa -k audit_rules_usergroup_modification
Rationale: | In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_usergroup_modification_shadow
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9
cjis | 5.4.1.1
cobit5 | APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS06.03, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01
cui | 3.1.7
hipaa | 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e)
isa-62443-2009 | 4.2.3.10, 4.3.2.6.7, 4.3.3.2.2, 4.3.3.3.9, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.8, 4.3.3.6.6, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 1.1, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6
iso27001-2013 | A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.2, A.6.2.1, A.6.2.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5
nerc-cip | CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3
nist | AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-1, PR.AC-3, PR.AC-4, PR.AC-6, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4
pcidss | Req-10.2.5
os-srg | SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221
app-srg-ctr | SRG-APP-000495-CTR-001235, SRG-APP-000499-CTR-001255, SRG-APP-000503-CTR-001275
anssi | R73
ism | 0582
pcidss4 | 10.2.1.5, 10.2.1, 10.2
bsi | SYS.1.1.A10, SYS.1.3.A6
cis | 6.3.3.8
stigref | SV-258223r1015134_rule
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/etc/shadow" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/shadow $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/etc/shadow$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /etc/shadow -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"

        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' to list of files for inspection.

    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/shadow" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/audit_rules_usergroup_modification.rules"
        # If the audit_rules_usergroup_modification.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0600 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/etc/shadow" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/shadow $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/etc/shadow$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /etc/shadow -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"

        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654245
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_shadow
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/shadow - Check if
        watch rule for /etc/shadow already exists in /etc/audit/rules.d/
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/etc/shadow\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654245
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_shadow
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/shadow - Search /etc/audit/rules.d
        for other rules with specified key audit_rules_usergroup_modification
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)audit_rules_usergroup_modification$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654245
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_shadow
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/shadow - Use /etc/audit/rules.d/audit_rules_usergroup_modification.rules
        as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - /etc/audit/rules.d/audit_rules_usergroup_modification.rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654245
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_shadow
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/shadow - Use matched
        file as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654245
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_shadow
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/shadow - Add watch
        rule for /etc/shadow in /etc/audit/rules.d/
      ansible.builtin.lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/shadow -p wa -k audit_rules_usergroup_modification
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654245
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_shadow
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/shadow - Check if
        watch rule for /etc/shadow already exists in /etc/audit/audit.rules
      ansible.builtin.find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/etc/shadow\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654245
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_shadow
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Events that Modify User/Group Information - /etc/shadow - Add watch
        rule for /etc/shadow in /etc/audit/audit.rules
      ansible.builtin.lineinfile:
        line: -w /etc/shadow -p wa -k audit_rules_usergroup_modification
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-09-654245
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.5
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.5
      - audit_rules_usergroup_modification_shadow
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
#### Rule Record Attempts to perform maintenance activities [ref]
The Red Hat Enterprise Linux 9 operating system must generate audit records for privileged activities, nonlocal maintenance, diagnostic sessions and other system-level access. Verify the operating system audits activities performed during nonlocal maintenance and diagnostic sessions. Run the following command:
    $ sudo auditctl -l | grep sudo.log
    -w /var/log/sudo.log -p wa -k maintenance
If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -w /var/log/sudo.log -p wa -k maintenance
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules`:
    -w /var/log/sudo.log -p wa -k maintenance
Rationale: | If events associated with nonlocal administrative access or diagnostic sessions are not logged, a major tool for assessing and investigating attacks would not be available. This requirement addresses auditing-related issues associated with maintenance tools used specifically for diagnostic and repair actions on organizational information systems. Nonlocal maintenance and diagnostic activities are those activities conducted by individuals communicating through a network, either an external network (e.g., the internet) or an internal network. Local maintenance and diagnostic activities are those activities carried out by individuals physically present at the information system or information system component and not communicating across a network connection. This requirement applies to hardware/software diagnostic test equipment or tools. This requirement does not cover hardware/software components that may support information system maintenance, yet are a part of the system, for example, the software implementing "ping," "ls," "ipconfig," or the hardware and software implementing the monitoring port of an Ethernet switch.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_sudo_log_events
References: |
pcidss | Req-10.2.2, Req-10.2.5.b
os-srg | SRG-OS-000392-GPOS-00172, SRG-OS-000471-GPOS-00215
anssi | R73
pcidss4 | 10.2.1.3, 10.2.1, 10.2
bsi | SYS.1.1.A10
cis | 6.3.3.3
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/var/log/sudo.log" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/sudo.log $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/var/log/sudo.log$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/sudo.log -p wa -k maintenance" >> "$audit_rules_file"

        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/maintenance.rules' to list of files for inspection.

    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/sudo.log" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/maintenance.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/maintenance.rules"
        # If the maintenance.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0600 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present

        if grep -q -P -- "^[\s]*-w[\s]+/var/log/sudo.log" "$audit_rules_file"

        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule

            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/sudo.log $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")

            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule

            sed -i "s#\($sp*-w$sp\+/var/log/sudo.log$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"

        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/sudo.log -p wa -k maintenance" >> "$audit_rules_file"

        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_sudo_log_events
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to perform maintenance activities - Check if watch rule for
        /var/log/sudo.log already exists in /etc/audit/rules.d/
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/var/log/sudo.log\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_sudo_log_events
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to perform maintenance activities - Search /etc/audit/rules.d
        for other rules with specified key maintenance
      ansible.builtin.find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)maintenance$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_sudo_log_events
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to perform maintenance activities - Use /etc/audit/rules.d/maintenance.rules
        as the recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - /etc/audit/rules.d/maintenance.rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_sudo_log_events
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to perform maintenance activities - Use matched file as the
        recipient for the rule
      ansible.builtin.set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_sudo_log_events
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to perform maintenance activities - Add watch rule for /var/log/sudo.log
        in /etc/audit/rules.d/
      ansible.builtin.lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /var/log/sudo.log -p wa -k maintenance
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_sudo_log_events
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to perform maintenance activities - Check if watch rule for
        /var/log/sudo.log already exists in /etc/audit/audit.rules
      ansible.builtin.find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/var/log/sudo.log\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_sudo_log_events
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Record Attempts to perform maintenance activities - Add watch rule for /var/log/sudo.log
        in /etc/audit/audit.rules
      ansible.builtin.lineinfile:
        line: -w /var/log/sudo.log -p wa -k maintenance
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0600'
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2
      - PCI-DSSv4-10.2.1
      - PCI-DSSv4-10.2.1.3
      - audit_sudo_log_events
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
Group Configure auditd Data Retention Group contains 1 rule
[ref] The audit system writes data to `/var/log/audit/audit.log`. By default, `auditd` rotates 5 logs by size (6MB), retaining a maximum of 30MB of data in total, and refuses to write entries when the disk is too full. This minimizes the risk of audit data filling its partition and impacting other services. This also minimizes the risk of the audit daemon temporarily disabling the system if it cannot write audit log (which it can be configured to do). For a busy system or a system which is thoroughly auditing system activity, the default settings for data retention may be insufficient. The log file size needed will depend heavily on what types of events are being audited. First configure auditing to log all the events of interest. Then monitor the log size manually for awhile to determine what file size will allow you to keep the required data for the correct time period. Using a dedicated partition for `/var/log/audit` prevents the `auditd` logs from disrupting system functionality if they fill, and, more importantly, prevents other activity in `/var` from filling the partition and stopping the audit trail. (The audit logs are size-limited and therefore unlikely to grow without bound unless configured to do so.) Some machines may have requirements that no actions occur which cannot be audited. If this is the case, then `auditd` can be configured to halt the machine if it runs out of space. Note: Since older logs are rotated, configuring `auditd` this way does not prevent older logs from being rotated away before they can be viewed. If your system is configured to halt when logging cannot be performed, make sure this can never happen under normal circumstances! Ensure that `/var/log/audit` is on its own partition, and that this partition is larger than the maximum amount of data `auditd` will retain normally.
#### Rule Configure auditd max_log_file_action Upon Reaching Maximum Log Size [ref]
The default action to take when the logs reach their maximum size is to rotate the log files, discarding the oldest one. To configure the action taken by `auditd`, add or correct the line in `/etc/audit/auditd.conf`:
    max_log_file_action = ACTION
Possible values for ACTION are described in the `auditd.conf` man page. These include: `ignore` `syslog` `suspend` `rotate` `keep_logs` Set the `ACTION` to `rotate`. The setting is case-insensitive.
Rationale: | Automatically rotating logs (by setting this to `rotate`) minimizes the chances of the system unexpectedly running out of disk space by being overwhelmed with log data. However, for systems that must never discard log data, or which use external processes to transfer it and reclaim space, `keep_logs` can be employed.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_auditd_data_retention_max_log_file_action
References: |
cis-csc | 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8
cjis | 5.4.1.1
cobit5 | APO11.04, APO12.06, APO13.01, BAI03.05, BAI04.04, BAI08.02, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.04, DSS05.07, MEA02.01
hipaa | 164.312(a)(2)(ii)
isa-62443-2009 | 4.2.3.10, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4
isa-62443-2013 | SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 7.1, SR 7.2
iso27001-2013 | A.12.1.3, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.16.1.4, A.16.1.5, A.16.1.7, A.17.2.1
nist | AU-5(b), AU-5(2), AU-5(1), AU-5(4), CM-6(a)
nist-csf | DE.AE-3, DE.AE-5, PR.DS-4, PR.PT-1, RS.AN-1, RS.AN-4
pcidss | Req-10.7
os-srg | SRG-OS-000047-GPOS-00023
cis | 6.3.2.2
Remediation Kubernetes snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }}
            mode: 0640
            path: /etc/audit/auditd.conf
            overwrite: true
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit && rpm --quiet -q kernel-core; then

    var_auditd_max_log_file_action='rotate'

    AUDITCONFIG=/etc/audit/auditd.conf

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^max_log_file_action")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$var_auditd_max_log_file_action"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^max_log_file_action\\>" "$AUDITCONFIG"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^max_log_file_action\\>.*/$escaped_formatted_output/gi" "$AUDITCONFIG"
    else
        if [[ -s "$AUDITCONFIG" ]] && [[ -n "$(tail -c 1 -- "$AUDITCONFIG" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "$AUDITCONFIG"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "$AUDITCONFIG" >> "$AUDITCONFIG"
        printf '%s\n' "$formatted_output" >> "$AUDITCONFIG"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - NIST-800-53-AU-5(1)
      - NIST-800-53-AU-5(2)
      - NIST-800-53-AU-5(4)
      - NIST-800-53-AU-5(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.7
      - auditd_data_retention_max_log_file_action
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_auditd_max_log_file_action # promote to variable
      set_fact:
        var_auditd_max_log_file_action: !!str rotate
      tags:
        - always

    - name: Configure auditd max_log_file_action Upon Reaching Maximum Log Size
      ansible.builtin.lineinfile:
        dest: /etc/audit/auditd.conf
        line: max_log_file_action = {{ var_auditd_max_log_file_action }}
        regexp: ^\s*max_log_file_action\s*=\s*.*$
        state: present
        create: true
      when:
      - '"audit" in ansible_facts.packages'
      - '"kernel-core" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - NIST-800-53-AU-5(1)
      - NIST-800-53-AU-5(2)
      - NIST-800-53-AU-5(4)
      - NIST-800-53-AU-5(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.7
      - auditd_data_retention_max_log_file_action
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Red Hat and Red Hat Enterprise Linux are either registered trademarks or trademarks of Red Hat, Inc. in the United States and other countries. All other names are registered trademarks or trademarks of their respective companies.