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

> with profile Health Insurance Portability and Accountability Act (HIPAA)
> The HIPAA Security Rule establishes U.S. national standards to protect individuals’ electronic personal health information that is created, received, used, or maintained by a covered entity. The Security Rule requires appropriate administrative, physical and technical safeguards to ensure the confidentiality, integrity, and security of electronic protected health information. This profile configures Red Hat Enterprise Linux 9 to the HIPAA Security Rule identified for securing of electronic protected health information. Use of this profile in no way guarantees or makes claims against legal compliance against the HIPAA Security Rule(s).

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.
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 | Health Insurance Portability and Accountability Act (HIPAA)
Profile ID | xccdf_org.ssgproject.content_profile_hipaa

#### CPE Platforms

- cpe:/o:redhat:enterprise_linux:9

## Revision History

Current version: 0.1.61

- draft (as of 2022-04-01)

## Table of Contents

1. System Settings
   1. Installing and Maintaining Software
   2. Account and Access Control
   3. System Accounting with auditd
   4. GRUB2 bootloader configuration
   5. Configure Syslog
   6. Network Configuration and Firewalls
   7. File Permissions and Masks
   8. SELinux
2. Services
   1. Base Services
   2. Cron and At Daemons
   3. NFS and RPC
   4. Obsolete Services
   5. SSH Server

## Checklist

Group Guide to the Secure Configuration of Red Hat Enterprise Linux 9 Group contains 52 groups and 135 rules
Group System Settings Group contains 39 groups and 111 rules
[ref] Contains rules that check correct system settings.
Group Installing and Maintaining Software Group contains 8 groups and 12 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 3 groups and 4 rules
[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 Software Integrity Checking Group contains 1 group and 2 rules
[ref] Both the AIDE (Advanced Intrusion Detection Environment) software and the RPM package management system provide mechanisms for verifying the integrity of installed software. AIDE uses snapshots of file metadata (such as hashes) and compares these to current system files in order to detect changes. The RPM package management system can conduct integrity checks by comparing information in its metadata database with files installed on the system.
Group Verify Integrity with RPM Group contains 2 rules
[ref] The RPM package management system includes the ability to verify the integrity of installed packages by comparing the installed files with information about the files taken from the package metadata stored in the RPM database. Although an attacker could corrupt the RPM database (analogous to attacking the AIDE database as described above), this check can still reveal modification of important files. To list which files on the system differ from what is expected by the RPM database:
    $ rpm -qVa
See the man page for `rpm` to see a complete explanation of each column.
#### Rule Verify File Hashes with RPM [ref]
Without cryptographic integrity protections, system executables and files can be altered by unauthorized users without detection. The RPM package management system can check the hashes of installed software packages, including many that are important to system security. To verify that the cryptographic hash of system files and commands matches vendor values, run the following command to list which files on the system have hashes that differ from what is expected by the RPM database:
    $ rpm -Va --noconfig | grep '^..5'
A "c" in the second column indicates that a file is a configuration file, which may appropriately be expected to change. If the file was not expected to change, investigate the cause of the change using audit logs or other means. The package can then be reinstalled to restore the file. Run the following command to determine which package owns the file:
    $ rpm -qf FILENAME
The package can be reinstalled from a dnf repository using the command:
    $ sudo dnf reinstall PACKAGENAME
Alternatively, the package can be reinstalled from trusted media using the command:
    $ sudo rpm -Uvh PACKAGENAME
Rationale: | The hashes of important files like system executables should match the information given by the RPM database. Executables with erroneous hashes could be a sign of nefarious activity on the system.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_rpm_verify_hashes
Identifiers and References | Identifiers: CCE-90841-8 References: 11, 2, 3, 9, 5.10.4.1, APO01.06, BAI03.05, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS06.02, 3.3.8, 3.4.1, CCI-000366, CCI-001749, 164.308(a)(1)(ii)(D), 164.312(b), 164.312(c)(1), 164.312(c)(2), 164.312(e)(2)(i), 4.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4, SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 7.6, A.11.2.4, A.12.1.2, A.12.2.1, A.12.5.1, A.12.6.2, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, CM-6(d), CM-6(c), SI-7, SI-7(1), SI-7(6), AU-9(3), PR.DS-6, PR.DS-8, PR.IP-1, Req-11.5, SRG-OS-000480-GPOS-00227
Remediation Shell script ⇲
    # Find which files have incorrect hash (not in /etc, because of the system related config files) and then get files names
    files_with_incorrect_hash="$(rpm -Va --noconfig | grep -E '^..5' | awk '{print $NF}' )"

    # From files names get package names and change newline to space, because rpm writes each package to new line
    packages_to_reinstall="$(rpm -qf $files_with_incorrect_hash | tr '\n' ' ')"

    dnf reinstall -y $packages_to_reinstall
#### Rule Verify and Correct File Permissions with RPM [ref]
The RPM package management system can check file access permissions of installed software packages, including many that are important to system security. Verify that the file permissions of system files and commands match vendor values. Check the file permissions with the following command:
    $ sudo rpm -Va | awk '{ if (substr($0,2,1)=="M") print $NF }'
Output indicates files that do not match vendor defaults. After locating a file with incorrect permissions, run the following command to determine which package owns it:
    $ rpm -qf FILENAME
Next, run the following command to reset its permissions to the correct values:
    $ sudo rpm --setperms PACKAGENAME
Warning: Profiles may require that specific files have stricter file permissions than defined by the vendor. Such files will be reported as a finding and need to be evaluated according to your policy and deployment environment.
Rationale: | Permissions on system binaries and configuration files that are too generous could allow an unauthorized user to gain privileges that they should not have. The permissions set by the vendor should be maintained. Any deviations from this baseline should be investigated.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_rpm_verify_permissions
Identifiers and References | Identifiers: CCE-90840-0 References: 1, 11, 12, 13, 14, 15, 16, 18, 3, 5, 6, 9, 5.10.4.1, APO01.06, APO11.04, BAI03.05, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.04, DSS05.07, DSS06.02, MEA02.01, 3.3.8, 3.4.1, CCI-001493, CCI-001494, CCI-001495, CCI-001496, 164.308(a)(1)(ii)(D), 164.312(b), 164.312(c)(1), 164.312(c)(2), 164.312(e)(2)(i), 4.3.3.3.9, 4.3.3.5.8, 4.3.3.7.3, 4.3.4.3.2, 4.3.4.3.3, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 5.2, SR 7.6, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.5.1, A.12.6.2, A.12.7.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.14.2.2, A.14.2.3, A.14.2.4, 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, CIP-003-8 R4.2, CIP-003-8 R6, CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CM-6(d), CM-6(c), SI-7, SI-7(1), SI-7(6), AU-9(3), CM-6(a), PR.AC-4, PR.DS-5, PR.IP-1, PR.PT-1, Req-11.5, SRG-OS-000256-GPOS-00097, SRG-OS-000257-GPOS-00098, SRG-OS-000258-GPOS-00099, SRG-OS-000278-GPOS-00108
Remediation Shell script ⇲
Complexity: | high
Disruption: | medium
Strategy: | restrict
    # Declare array to hold set of RPM packages we need to correct permissions for
    declare -A SETPERMS_RPM_DICT

    # Create a list of files on the system having permissions different from what
    # is expected by the RPM database
    readarray -t FILES_WITH_INCORRECT_PERMS < <(rpm -Va --nofiledigest | awk '{ if (substr($0,2,1)=="M") print $NF }')

    for FILE_PATH in "${FILES_WITH_INCORRECT_PERMS[@]}"
    do
            # NOTE: some files maybe controlled by more then one package
            readarray -t RPM_PACKAGES < <(rpm -qf "${FILE_PATH}")
            for RPM_PACKAGE in "${RPM_PACKAGES[@]}"
            do
                    # Use an associative array to store packages as it's keys, not having to care about duplicates.
                    SETPERMS_RPM_DICT["$RPM_PACKAGE"]=1
            done
    done

    # For each of the RPM packages left in the list -- reset its permissions to the
    # correct values
    for RPM_PACKAGE in "${!SETPERMS_RPM_DICT[@]}"
    do
    	rpm --restore "${RPM_PACKAGE}"
    done
Remediation Ansible snippet ⇲
Complexity: | high
Disruption: | medium
Strategy: | restrict
    - name: Read list of files with incorrect permissions
      command: rpm -Va --nodeps --nosignature --nofiledigest --nosize --nomtime --nordev
        --nocaps --nolinkto --nouser --nogroup
      args:
        warn: false
      register: files_with_incorrect_permissions
      failed_when: files_with_incorrect_permissions.rc > 1
      changed_when: false
      check_mode: false
      tags:
        - CCE-90840-0
        - CJIS-5.10.4.1
        - NIST-800-171-3.3.8
        - NIST-800-171-3.4.1
        - NIST-800-53-AU-9(3)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-6(c)
        - NIST-800-53-CM-6(d)
        - NIST-800-53-SI-7
        - NIST-800-53-SI-7(1)
        - NIST-800-53-SI-7(6)
        - PCI-DSS-Req-11.5
        - high_complexity
        - high_severity
        - medium_disruption
        - no_reboot_needed
        - restrict_strategy
        - rpm_verify_permissions

    - name: Create list of packages
      command: rpm -qf "{{ item }}"
      args:
        warn: false
      with_items: '{{ files_with_incorrect_permissions.stdout_lines | map(''regex_findall'',
        ''^[.]+[M]+.* (\/.*)'', ''\1'') | map(''join'') | select(''match'', ''(\/.*)'')
        | list | unique }}'
      register: list_of_packages
      changed_when: false
      check_mode: false
      when: (files_with_incorrect_permissions.stdout_lines | length > 0)
      tags:
        - CCE-90840-0
        - CJIS-5.10.4.1
        - NIST-800-171-3.3.8
        - NIST-800-171-3.4.1
        - NIST-800-53-AU-9(3)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-6(c)
        - NIST-800-53-CM-6(d)
        - NIST-800-53-SI-7
        - NIST-800-53-SI-7(1)
        - NIST-800-53-SI-7(6)
        - PCI-DSS-Req-11.5
        - high_complexity
        - high_severity
        - medium_disruption
        - no_reboot_needed
        - restrict_strategy
        - rpm_verify_permissions

    - name: Correct file permissions with RPM
      command: rpm --setperms '{{ item }}'
      args:
        warn: false
      with_items: '{{ list_of_packages.results | map(attribute=''stdout_lines'') | list
        | unique }}'
      when: (files_with_incorrect_permissions.stdout_lines | length > 0)
      tags:
        - CCE-90840-0
        - CJIS-5.10.4.1
        - NIST-800-171-3.3.8
        - NIST-800-171-3.4.1
        - NIST-800-53-AU-9(3)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-6(c)
        - NIST-800-53-CM-6(d)
        - NIST-800-53-SI-7
        - NIST-800-53-SI-7(1)
        - NIST-800-53-SI-7(6)
        - PCI-DSS-Req-11.5
        - high_complexity
        - high_severity
        - medium_disruption
        - no_reboot_needed
        - restrict_strategy
        - rpm_verify_permissions
Group System Cryptographic Policies Group contains 2 rules
[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 `FIPS` policy, run the following command:
    $ sudo update-crypto-policies --set FIPS
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
Identifiers and References | Identifiers: CCE-83450-7 References: 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.312(e)(1), 164.312(e)(2)(ii), 1446, CIP-003-8 R4.2, CIP-007-3 R5.1, CIP-007-3 R7.1, AC-17(a), AC-17(2), CM-6(a), MA-4(6), SC-13, SC-12(2), SC-12(3), 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, SRG-OS-000396-GPOS-00176, SRG-OS-000393-GPOS-00173, SRG-OS-000394-GPOS-00174
Remediation Shell script ⇲
    var_system_crypto_policy='FIPS'

    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
Strategy: | restrict
    - name: XCCDF Value var_system_crypto_policy # promote to variable
      set_fact:
        var_system_crypto_policy: !!str FIPS
      tags:
        - always

    - name: Configure System Cryptography Policy
      lineinfile:
        path: /etc/crypto-policies/config
        regexp: ^(?!#)(\S+)$
        line: '{{ var_system_crypto_policy }}'
        create: true
      tags:
        - CCE-83450-7
        - 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
        - configure_crypto_policy
        - high_severity
        - low_complexity
        - low_disruption
        - no_reboot_needed
        - restrict_strategy

    - name: Verify that Crypto Policy is Set (runtime)
      command: /usr/bin/update-crypto-policies --set {{ var_system_crypto_policy }}
      tags:
        - CCE-83450-7
        - 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
        - configure_crypto_policy
        - high_severity
        - low_complexity
        - low_disruption
        - no_reboot_needed
        - restrict_strategy
#### Rule Configure SSH to use System Crypto Policy [ref]
Crypto Policies provide a centralized control over crypto algorithms usage of many packages. SSH is supported by crypto policy, but the SSH configuration may be set up to ignore it. To check that Crypto Policies settings are configured correctly, ensure that the `CRYPTO_POLICY` variable is either commented or not set at all in the `/etc/sysconfig/sshd`.
Rationale: | Overriding the system crypto policy makes the behavior of the SSH service violate expectations, and makes system configuration more fragmented.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_configure_ssh_crypto_policy
Identifiers and References | Identifiers: CCE-83445-7 References: 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.312(e)(1), 164.312(e)(2)(ii), CIP-003-8 R4.2, CIP-007-3 R5.1, CIP-007-3 R7.1, AC-17(a), AC-17(2), CM-6(a), MA-4(6), SC-13, SRG-OS-000250-GPOS-00093
Remediation Shell script ⇲
    SSH_CONF="/etc/sysconfig/sshd"

    sed -i "/^\s*CRYPTO_POLICY.*$/d" $SSH_CONF
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Configure SSH to use System Crypto Policy
      lineinfile:
        dest: /etc/sysconfig/sshd
        state: absent
        regexp: ^\s*CRYPTO_POLICY.*$
      tags:
        - CCE-83445-7
        - 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-13
        - configure_ssh_crypto_policy
        - disable_strategy
        - low_complexity
        - medium_disruption
        - medium_severity
        - reboot_required
Group Disk Partitioning Group contains 1 rule
[ref] To ensure separation and protection of data, there are top-level system directories which should be placed on their own physical partition or logical volume. The installer's default partitioning scheme creates separate logical volumes for `/`, `/boot`, and `swap`. If starting with any of the default layouts, check the box to \"Review and modify partitioning.\" This allows for the easy creation of additional logical volumes inside the volume group already created, though it may require making `/`'s logical volume smaller to create space. In general, using logical volumes is preferable to using partitions because they can be more easily adjusted later. If creating a custom layout, create the partitions mentioned in the previous paragraph (which the installer will require anyway), as well as separate ones described in the following sections. If a system has already been installed, and the default partitioning scheme was used, it is possible but nontrivial to modify it to create separate logical volumes for the directories listed above. The Logical Volume Manager (LVM) makes this possible. See the LVM HOWTO at http://tldp.org/HOWTO/LVM-HOWTO/ for more detailed information on LVM.
#### Rule Encrypt Partitions [ref]
Red Hat Enterprise Linux 9 natively supports partition encryption through the Linux Unified Key Setup-on-disk-format (LUKS) technology. The easiest way to encrypt a partition is during installation time. For manual installations, select the `Encrypt` checkbox during partition creation to encrypt the partition. When this option is selected the system will prompt for a passphrase to use in decrypting the partition. The passphrase will subsequently need to be entered manually every time the system boots. For automated/unattended installations, it is possible to use Kickstart by adding the `--encrypted` and `--passphrase=` options to the definition of each partition to be encrypted. For example, the following line would encrypt the root partition:
    part / --fstype=ext4 --size=100 --onpart=hda1 --encrypted --passphrase=PASSPHRASE
Any PASSPHRASE is stored in the Kickstart in plaintext, and the Kickstart must then be protected accordingly. Omitting the `--passphrase=` option from the partition definition will cause the installer to pause and interactively ask for the passphrase during installation. By default, the `Anaconda` installer uses `aes-xts-plain64` cipher with a minimum `512` bit key size which should be compatible with FIPS enabled. Detailed information on encrypting partitions using LUKS or LUKS ciphers can be found on the Red Hat Enterprise Linux 9 Documentation web site: https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/security_hardening/encrypting-block-devices-using-luks_security-hardening.
Rationale: | The risk of a system's physical compromise, particularly mobile systems such as laptops, places its data at risk of compromise. Encrypting this data mitigates the risk of its loss if the system is lost.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_encrypt_partitions
Identifiers and References | Identifiers: CCE-90849-1 References: 13, 14, APO01.06, BAI02.01, BAI06.01, DSS04.07, DSS05.03, DSS05.04, DSS05.07, DSS06.02, DSS06.06, 3.13.16, CCI-001199, CCI-002475, CCI-002476, 164.308(a)(1)(ii)(D), 164.308(b)(1), 164.310(d), 164.312(a)(1), 164.312(a)(2)(iii), 164.312(a)(2)(iv), 164.312(b), 164.312(c), 164.314(b)(2)(i), 164.312(d), SR 3.4, SR 4.1, SR 5.2, 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, CIP-003-8 R4.2, CIP-007-3 R5.1, CM-6(a), SC-28, SC-28(1), SC-13, AU-9(3), PR.DS-1, PR.DS-5, SRG-OS-000405-GPOS-00184, SRG-OS-000185-GPOS-00079, SRG-OS-000404-GPOS-00183, SRG-OS-000404-VMM-001650, SRG-OS-000405-VMM-001660
Group GNOME Desktop Environment Group contains 1 group and 3 rules
[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.
Group GNOME Remote Access Settings Group contains 2 rules
[ref] GNOME remote access settings that apply to the graphical interface.
#### Rule Require Credential Prompting for Remote Access in GNOME3 [ref]
By default, `GNOME` does not require credentials when using `Vino` for remote access. To configure the system to require remote credentials, add or set `authentication-methods` to `['vnc']` in `/etc/dconf/db/local.d/00-security-settings`. For example:
    [org/gnome/Vino]
    authentication-methods=['vnc']
Once the settings have been added, add a lock to `/etc/dconf/db/local.d/locks/00-security-settings-lock` to prevent user modification. For example:
    /org/gnome/Vino/authentication-methods
After the settings have been set, run `dconf update`.
Rationale: | Username and password prompting is required for remote access. Otherwise, non-authorized and nefarious users can access the system freely.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_dconf_gnome_remote_access_credential_prompt
Identifiers and References | Identifiers: CCE-87524-5 References: 3.1.12, 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)
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q gdm && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; 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/Vino\\]" "/etc/dconf/db/" | grep -v 'distro\|ibus' | cut -d":" -f1)
    DCONFFILE="/etc/dconf/db/local.d/00-security-settings"
    DBDIR="/etc/dconf/db/local.d"

    mkdir -p "${DBDIR}"

    if [ "${#SETTINGSFILES[@]}" -eq 0 ]
    then
        [ ! -z ${DCONFFILE} ] || echo "" >> ${DCONFFILE}
        printf '%s\n' "[org/gnome/Vino]" >> ${DCONFFILE}
        printf '%s=%s\n' "authentication-methods" "['vnc']" >> ${DCONFFILE}
    else
        escaped_value="$(sed -e 's/\\/\\\\/g' <<< "['vnc']")"
        if grep -q "^\\s*authentication-methods\\s*=" "${SETTINGSFILES[@]}"
        then

            sed -i "s/\\s*authentication-methods\\s*=\\s*.*/authentication-methods=${escaped_value}/g" "${SETTINGSFILES[@]}"
        else
            sed -i "\\|\\[org/gnome/Vino\\]|a\\authentication-methods=${escaped_value}" "${SETTINGSFILES[@]}"
        fi
    fi

    dconf update
    # Check for setting in any of the DConf db directories
    LOCKFILES=$(grep -r "^/org/gnome/Vino/authentication-methods$" "/etc/dconf/db/" | grep -v 'distro\|ibus' | cut -d":" -f1)
    LOCKSFOLDER="/etc/dconf/db/local.d/locks"

    mkdir -p "${LOCKSFOLDER}"

    if [[ -z "${LOCKFILES}" ]]
    then
        echo "/org/gnome/Vino/authentication-methods" >> "/etc/dconf/db/local.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
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-87524-5
        - NIST-800-171-3.1.12
        - dconf_gnome_remote_access_credential_prompt
        - low_complexity
        - medium_disruption
        - medium_severity
        - no_reboot_needed
        - unknown_strategy

    - name: Require Credential Prompting for Remote Access in GNOME3
      ini_file:
        dest: /etc/dconf/db/local.d/00-security-settings
        section: org/gnome/Vino
        option: authentication-methods
        value: '[''vnc'']'
        create: true
        no_extra_spaces: true
      when:
        - '"gdm" in ansible_facts.packages'
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-87524-5
        - NIST-800-171-3.1.12
        - dconf_gnome_remote_access_credential_prompt
        - low_complexity
        - medium_disruption
        - medium_severity
        - no_reboot_needed
        - unknown_strategy

    - name: Prevent user modification of GNOME3 Credential Prompting for Remote Access
      lineinfile:
        path: /etc/dconf/db/local.d/locks/00-security-settings-lock
        regexp: ^/org/gnome/Vino/authentication-methods$
        line: /org/gnome/Vino/authentication-methods
        create: true
      when:
        - '"gdm" in ansible_facts.packages'
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-87524-5
        - NIST-800-171-3.1.12
        - dconf_gnome_remote_access_credential_prompt
        - low_complexity
        - medium_disruption
        - medium_severity
        - no_reboot_needed
        - unknown_strategy

    - name: Dconf Update
      command: dconf update
      when:
        - '"gdm" in ansible_facts.packages'
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-87524-5
        - NIST-800-171-3.1.12
        - dconf_gnome_remote_access_credential_prompt
        - low_complexity
        - medium_disruption
        - medium_severity
        - no_reboot_needed
        - unknown_strategy
#### Rule Require Encryption for Remote Access in GNOME3 [ref]
By default, `GNOME` requires encryption when using `Vino` for remote access. To prevent remote access encryption from being disabled, add or set `require-encryption` to `true` in `/etc/dconf/db/local.d/00-security-settings`. For example:
    [org/gnome/Vino]
    require-encryption=true
Once the settings have been added, add a lock to `/etc/dconf/db/local.d/locks/00-security-settings-lock` to prevent user modification. For example:
    /org/gnome/Vino/require-encryption
After the settings have been set, run `dconf update`.
Rationale: | Open X displays allow an attacker to capture keystrokes and to execute commands remotely.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_dconf_gnome_remote_access_encryption
Identifiers and References | Identifiers: CCE-88756-2 References: 1, 11, 12, 13, 15, 16, 18, 20, 3, 4, 6, 9, BAI03.08, BAI07.04, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS03.01, 3.1.13, CCI-000366, 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), 4.3.4.3.2, 4.3.4.3.3, 4.4.3.3, SR 7.6, A.12.1.1, A.12.1.2, A.12.1.4, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.2, A.14.2.2, A.14.2.3, A.14.2.4, CM-6(a), AC-17(a), AC-17(2), DE.AE-1, PR.DS-7, PR.IP-1, SRG-OS-000480-GPOS-00227
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q gdm && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; 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/Vino\\]" "/etc/dconf/db/" | grep -v 'distro\|ibus' | cut -d":" -f1)
    DCONFFILE="/etc/dconf/db/local.d/00-security-settings"
    DBDIR="/etc/dconf/db/local.d"

    mkdir -p "${DBDIR}"

    if [ "${#SETTINGSFILES[@]}" -eq 0 ]
    then
        [ ! -z ${DCONFFILE} ] || echo "" >> ${DCONFFILE}
        printf '%s\n' "[org/gnome/Vino]" >> ${DCONFFILE}
        printf '%s=%s\n' "require-encryption" "true" >> ${DCONFFILE}
    else
        escaped_value="$(sed -e 's/\\/\\\\/g' <<< "true")"
        if grep -q "^\\s*require-encryption\\s*=" "${SETTINGSFILES[@]}"
        then

            sed -i "s/\\s*require-encryption\\s*=\\s*.*/require-encryption=${escaped_value}/g" "${SETTINGSFILES[@]}"
        else
            sed -i "\\|\\[org/gnome/Vino\\]|a\\require-encryption=${escaped_value}" "${SETTINGSFILES[@]}"
        fi
    fi

    dconf update
    # Check for setting in any of the DConf db directories
    LOCKFILES=$(grep -r "^/org/gnome/Vino/require-encryption$" "/etc/dconf/db/" | grep -v 'distro\|ibus' | cut -d":" -f1)
    LOCKSFOLDER="/etc/dconf/db/local.d/locks"

    mkdir -p "${LOCKSFOLDER}"

    if [[ -z "${LOCKFILES}" ]]
    then
        echo "/org/gnome/Vino/require-encryption" >> "/etc/dconf/db/local.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
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-88756-2
        - NIST-800-171-3.1.13
        - NIST-800-53-AC-17(2)
        - NIST-800-53-AC-17(a)
        - NIST-800-53-CM-6(a)
        - dconf_gnome_remote_access_encryption
        - low_complexity
        - medium_disruption
        - medium_severity
        - no_reboot_needed
        - unknown_strategy

    - name: Require Encryption for Remote Access in GNOME3
      ini_file:
        dest: /etc/dconf/db/local.d/00-security-settings
        section: org/gnome/Vino
        option: require-encryption
        value: 'true'
        create: true
        no_extra_spaces: true
      when:
        - '"gdm" in ansible_facts.packages'
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-88756-2
        - NIST-800-171-3.1.13
        - NIST-800-53-AC-17(2)
        - NIST-800-53-AC-17(a)
        - NIST-800-53-CM-6(a)
        - dconf_gnome_remote_access_encryption
        - low_complexity
        - medium_disruption
        - medium_severity
        - no_reboot_needed
        - unknown_strategy

    - name: Prevent user modification of GNOME3 Encryption for Remote Access
      lineinfile:
        path: /etc/dconf/db/local.d/locks/00-security-settings-lock
        regexp: ^/org/gnome/Vino/require-encryption$
        line: /org/gnome/Vino/require-encryption
        create: true
      when:
        - '"gdm" in ansible_facts.packages'
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-88756-2
        - NIST-800-171-3.1.13
        - NIST-800-53-AC-17(2)
        - NIST-800-53-AC-17(a)
        - NIST-800-53-CM-6(a)
        - dconf_gnome_remote_access_encryption
        - low_complexity
        - medium_disruption
        - medium_severity
        - no_reboot_needed
        - unknown_strategy

    - name: Dconf Update
      command: dconf update
      when:
        - '"gdm" in ansible_facts.packages'
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-88756-2
        - NIST-800-171-3.1.13
        - NIST-800-53-AC-17(2)
        - NIST-800-53-AC-17(a)
        - NIST-800-53-CM-6(a)
        - dconf_gnome_remote_access_encryption
        - low_complexity
        - medium_disruption
        - medium_severity
        - no_reboot_needed
        - unknown_strategy
#### 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
Identifiers and References | Identifiers: CCE-87295-2 References: 164.308(a)(1)(ii)(B), 164.308(a)(5)(ii)(A), SRG-OS-000480-GPOS-00227
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q gdm && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

    dconf update

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Group Updating Software Group contains 4 rules
[ref] The `dnf` command line tool is used to install and update software packages. The system also provides a graphical software update tool in the System menu, in the Administration submenu, called Software Update. Red Hat Enterprise Linux 9 systems contain an installed software catalog called the RPM database, which records metadata of installed packages. Consistently using `dnf` or the graphical Software Update for all software installation allows for insight into the current inventory of installed software on the system.
#### Rule Ensure gpgcheck Enabled In Main dnf Configuration [ref]
The `gpgcheck` option controls whether RPM packages' signatures are always checked prior to installation. To configure dnf to check package signatures before installing them, ensure the following line appears in `/etc/dnf/dnf.conf` in the `[main]` section:
    gpgcheck=1
Rationale: | Changes to any software components can have significant effects on the overall security of the operating system. This requirement ensures the software has not been tampered with and that it has been provided by a trusted vendor. Accordingly, patches, service packs, device drivers, or operating system components must be signed with a certificate recognized and approved by the organization. Verifying the authenticity of the software prior to installation validates the integrity of the patch or upgrade received from a vendor. This ensures the software has not been tampered with and that it has been provided by a trusted vendor. Self-signed certificates are disallowed by this requirement. Certificates used to verify the software must be from an approved Certificate Authority (CA).
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_ensure_gpgcheck_globally_activated
Identifiers and References | Identifiers: CCE-83457-2 References: BP28(R15), 11, 2, 3, 9, 5.10.4.1, APO01.06, BAI03.05, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS06.02, 3.4.8, CCI-001749, 164.308(a)(1)(ii)(D), 164.312(b), 164.312(c)(1), 164.312(c)(2), 164.312(e)(2)(i), 4.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4, SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 7.6, A.11.2.4, A.12.1.2, A.12.2.1, A.12.5.1, A.12.6.2, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, CM-5(3), SI-7, SC-12, SC-12(3), CM-6(a), SA-12, SA-12(10), CM-11(a), CM-11(b), PR.DS-6, PR.DS-8, PR.IP-1, FPT_TUD_EXT.1, FPT_TUD_EXT.2, Req-6.2, SRG-OS-000366-GPOS-00153, SRG-OS-000366-VMM-001430, SRG-OS-000370-VMM-001460, SRG-OS-000404-VMM-001650
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q yum; then

    # Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
    # Otherwise, regular sed command will do.
    sed_command=('sed' '-i')
    if test -L "/etc/dnf/dnf.conf"; then
        sed_command+=('--follow-symlinks')
    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' <<< "^gpgcheck")

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

    # 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 "^gpgcheck\\>" "/etc/dnf/dnf.conf"; then
        "${sed_command[@]}" "s/^gpgcheck\\>.*/$formatted_output/gi" "/etc/dnf/dnf.conf"
    else
        # \n is precaution for case where file ends without trailing newline
        cce="CCE-83457-2"
        printf '\n# Per %s: Set %s in %s\n' "$cce" "$formatted_output" "/etc/dnf/dnf.conf" >> "/etc/dnf/dnf.conf"
        printf '%s\n' "$formatted_output" >> "/etc/dnf/dnf.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83457-2
        - CJIS-5.10.4.1
        - NIST-800-171-3.4.8
        - NIST-800-53-CM-11(a)
        - NIST-800-53-CM-11(b)
        - NIST-800-53-CM-5(3)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SA-12
        - NIST-800-53-SA-12(10)
        - NIST-800-53-SC-12
        - NIST-800-53-SC-12(3)
        - NIST-800-53-SI-7
        - PCI-DSS-Req-6.2
        - configure_strategy
        - ensure_gpgcheck_globally_activated
        - high_severity
        - low_complexity
        - medium_disruption
        - no_reboot_needed

    - name: Ensure GPG check is globally activated
      ini_file:
        dest: /etc/dnf/dnf.conf
        section: main
        option: gpgcheck
        value: 1
        no_extra_spaces: true
        create: false
      when: '"yum" in ansible_facts.packages'
      tags:
        - CCE-83457-2
        - CJIS-5.10.4.1
        - NIST-800-171-3.4.8
        - NIST-800-53-CM-11(a)
        - NIST-800-53-CM-11(b)
        - NIST-800-53-CM-5(3)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SA-12
        - NIST-800-53-SA-12(10)
        - NIST-800-53-SC-12
        - NIST-800-53-SC-12(3)
        - NIST-800-53-SI-7
        - PCI-DSS-Req-6.2
        - configure_strategy
        - ensure_gpgcheck_globally_activated
        - high_severity
        - low_complexity
        - medium_disruption
        - no_reboot_needed
#### Rule Ensure gpgcheck Enabled for Local Packages [ref]
`dnf` should be configured to verify the signature(s) of local packages prior to installation. To configure `dnf` to verify signatures of local packages, set the `localpkg_gpgcheck` to `1` in `/etc/dnf/dnf.conf`.
Rationale: | Changes to any software components can have significant effects to the overall security of the operating system. This requirement ensures the software has not been tampered and has been provided by a trusted vendor. Accordingly, patches, service packs, device drivers, or operating system components must be signed with a certificate recognized and approved by the organization.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_ensure_gpgcheck_local_packages
Identifiers and References | Identifiers: CCE-83463-0 References: BP28(R15), 11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.4.8, CCI-001749, 164.308(a)(1)(ii)(D), 164.312(b), 164.312(c)(1), 164.312(c)(2), 164.312(e)(2)(i), 4.3.4.3.2, 4.3.4.3.3, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, CM-11(a), CM-11(b), CM-6(a), CM-5(3), SA-12, SA-12(10), PR.IP-1, FPT_TUD_EXT.1, FPT_TUD_EXT.2, SRG-OS-000366-GPOS-00153, SRG-OS-000366-VMM-001430, SRG-OS-000370-VMM-001460, SRG-OS-000404-VMM-001650
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q yum; then

    # Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
    # Otherwise, regular sed command will do.
    sed_command=('sed' '-i')
    if test -L "/etc/dnf/dnf.conf"; then
        sed_command+=('--follow-symlinks')
    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' <<< "^localpkg_gpgcheck")

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

    # 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 "^localpkg_gpgcheck\\>" "/etc/dnf/dnf.conf"; then
        "${sed_command[@]}" "s/^localpkg_gpgcheck\\>.*/$formatted_output/gi" "/etc/dnf/dnf.conf"
    else
        # \n is precaution for case where file ends without trailing newline
        cce="CCE-83463-0"
        printf '\n# Per %s: Set %s in %s\n' "$cce" "$formatted_output" "/etc/dnf/dnf.conf" >> "/etc/dnf/dnf.conf"
        printf '%s\n' "$formatted_output" >> "/etc/dnf/dnf.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83463-0
        - NIST-800-171-3.4.8
        - NIST-800-53-CM-11(a)
        - NIST-800-53-CM-11(b)
        - NIST-800-53-CM-5(3)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SA-12
        - NIST-800-53-SA-12(10)
        - ensure_gpgcheck_local_packages
        - high_severity
        - low_complexity
        - medium_disruption
        - no_reboot_needed
        - unknown_strategy

    - name: Ensure GPG check Enabled for Local Packages (Yum)
      ini_file:
        dest: /etc/dnf/dnf.conf
        section: main
        option: localpkg_gpgcheck
        value: 1
        no_extra_spaces: true
        create: true
      when: '"yum" in ansible_facts.packages'
      tags:
        - CCE-83463-0
        - NIST-800-171-3.4.8
        - NIST-800-53-CM-11(a)
        - NIST-800-53-CM-11(b)
        - NIST-800-53-CM-5(3)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SA-12
        - NIST-800-53-SA-12(10)
        - ensure_gpgcheck_local_packages
        - high_severity
        - low_complexity
        - medium_disruption
        - no_reboot_needed
        - unknown_strategy
#### Rule Ensure gpgcheck Enabled for All dnf Package Repositories [ref]
To ensure signature checking is not disabled for any repos, remove any lines from files in `/etc/yum.repos.d` of the form:
    gpgcheck=0
Rationale: | Verifying the authenticity of the software prior to installation validates the integrity of the patch or upgrade received from a vendor. This ensures the software has not been tampered with and that it has been provided by a trusted vendor. Self-signed certificates are disallowed by this requirement. Certificates used to verify the software must be from an approved Certificate Authority (CA)."
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_ensure_gpgcheck_never_disabled
Identifiers and References | Identifiers: CCE-83464-8 References: BP28(R15), 11, 2, 3, 9, 5.10.4.1, APO01.06, BAI03.05, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS06.02, 3.4.8, CCI-001749, 164.308(a)(1)(ii)(D), 164.312(b), 164.312(c)(1), 164.312(c)(2), 164.312(e)(2)(i), 4.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4, SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 7.6, A.11.2.4, A.12.1.2, A.12.2.1, A.12.5.1, A.12.6.2, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, CM-5(3), SI-7, SC-12, SC-12(3), CM-6(a), SA-12, SA-12(10), CM-11(a), CM-11(b), PR.DS-6, PR.DS-8, PR.IP-1, FPT_TUD_EXT.1, FPT_TUD_EXT.2, Req-6.2, SRG-OS-000366-GPOS-00153, SRG-OS-000366-VMM-001430, SRG-OS-000370-VMM-001460, SRG-OS-000404-VMM-001650
Remediation Shell script ⇲
    sed -i 's/gpgcheck\s*=.*/gpgcheck=1/g' /etc/yum.repos.d/*
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Strategy: | enable
    - name: Grep for dnf repo section names
      shell: |
        set -o pipefail
        grep -HEr '^\[.+\]' -r /etc/yum.repos.d/
      register: repo_grep_results
      ignore_errors: true
      changed_when: false
      tags:
        - CCE-83464-8
        - CJIS-5.10.4.1
        - NIST-800-171-3.4.8
        - NIST-800-53-CM-11(a)
        - NIST-800-53-CM-11(b)
        - NIST-800-53-CM-5(3)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SA-12
        - NIST-800-53-SA-12(10)
        - NIST-800-53-SC-12
        - NIST-800-53-SC-12(3)
        - NIST-800-53-SI-7
        - PCI-DSS-Req-6.2
        - enable_strategy
        - ensure_gpgcheck_never_disabled
        - high_severity
        - low_complexity
        - medium_disruption
        - no_reboot_needed

    - name: Set gpgcheck=1 for each dnf repo
      ini_file:
        path: '{{ item[0] }}'
        section: '{{ item[1] }}'
        option: gpgcheck
        value: '1'
        no_extra_spaces: true
      loop: '{{ repo_grep_results.stdout | regex_findall( ''(.+\.repo):\[(.+)\]\n?'' )
        }}'
      tags:
        - CCE-83464-8
        - CJIS-5.10.4.1
        - NIST-800-171-3.4.8
        - NIST-800-53-CM-11(a)
        - NIST-800-53-CM-11(b)
        - NIST-800-53-CM-5(3)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SA-12
        - NIST-800-53-SA-12(10)
        - NIST-800-53-SC-12
        - NIST-800-53-SC-12(3)
        - NIST-800-53-SI-7
        - PCI-DSS-Req-6.2
        - enable_strategy
        - ensure_gpgcheck_never_disabled
        - high_severity
        - low_complexity
        - medium_disruption
        - no_reboot_needed
#### Rule Ensure Red Hat GPG Key Installed [ref]
To ensure the system can cryptographically verify base software packages come from Red Hat (and to connect to the Red Hat Network to receive them), the Red Hat GPG key must properly be installed. To install the Red Hat GPG key, run:
    $ sudo subscription-manager register
If the system is not connected to the Internet or an RHN Satellite, then install the Red Hat GPG key from trusted media such as the Red Hat installation CD-ROM or DVD. Assuming the disc is mounted in `/media/cdrom`, use the following command as the root user to import it into the keyring:
    $ sudo rpm --import /media/cdrom/RPM-GPG-KEY
Alternatively, the key may be pre-loaded during the RHEL installation. In such cases, the key can be installed by running the following command:
    sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
Rationale: | Changes to software components can have significant effects on the overall security of the operating system. This requirement ensures the software has not been tampered with and that it has been provided by a trusted vendor. The Red Hat GPG key is necessary to cryptographically verify packages are from Red Hat.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_ensure_redhat_gpgkey_installed
Identifiers and References | Identifiers: CCE-84180-9 References: BP28(R15), 11, 2, 3, 9, 5.10.4.1, APO01.06, BAI03.05, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS06.02, 3.4.8, CCI-001749, 164.308(a)(1)(ii)(D), 164.312(b), 164.312(c)(1), 164.312(c)(2), 164.312(e)(2)(i), 4.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4, SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 7.6, A.11.2.4, A.12.1.2, A.12.2.1, A.12.5.1, A.12.6.2, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, CIP-003-8 R4.2, CIP-003-8 R6, CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1, CM-5(3), SI-7, SC-12, SC-12(3), CM-6(a), PR.DS-6, PR.DS-8, PR.IP-1, FPT_TUD_EXT.1, FPT_TUD_EXT.2, Req-6.2, SRG-OS-000366-GPOS-00153, SRG-OS-000366-VMM-001430, SRG-OS-000370-VMM-001460, SRG-OS-000404-VMM-001650
Remediation Shell script ⇲
    # The two fingerprints below are retrieved from https://access.redhat.com/security/team/key
    readonly REDHAT_RELEASE_FINGERPRINT="567E347AD0044ADE55BA8A5F199E2F91FD431D51"
    readonly REDHAT_AUXILIARY_FINGERPRINT="7E4624258C406535D56D6F135054E4A45A6340B3"

    # Location of the key we would like to import (once it's integrity verified)
    readonly REDHAT_RELEASE_KEY="/etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release"

    RPM_GPG_DIR_PERMS=$(stat -c %a "$(dirname "$REDHAT_RELEASE_KEY")")

    # Verify /etc/pki/rpm-gpg directory permissions are safe
    if [ "${RPM_GPG_DIR_PERMS}" -le "755" ]
    then
      # If they are safe, try to obtain fingerprints from the key file
      # (to ensure there won't be e.g. CRC error).

      readarray -t GPG_OUT < <(gpg --with-fingerprint --with-colons "$REDHAT_RELEASE_KEY" | grep "^fpr" | cut -d ":" -f 10)

      GPG_RESULT=$?
      # No CRC error, safe to proceed
      if [ "${GPG_RESULT}" -eq "0" ]
      then
        echo "${GPG_OUT[*]}" | grep -vE "${REDHAT_RELEASE_FINGERPRINT}|${REDHAT_AUXILIARY_FINGERPRINT}" || {
          # If $REDHAT_RELEASE_KEY file doesn't contain any keys with unknown fingerprint, import it
          rpm --import "${REDHAT_RELEASE_KEY}"
        }
      fi
    fi
Remediation Ansible snippet ⇲
Complexity: | medium
Disruption: | medium
Strategy: | restrict
    - name: Read permission of GPG key directory
      stat:
        path: /etc/pki/rpm-gpg/
      register: gpg_key_directory_permission
      check_mode: false
      tags:
        - CCE-84180-9
        - CJIS-5.10.4.1
        - NIST-800-171-3.4.8
        - NIST-800-53-CM-5(3)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SC-12
        - NIST-800-53-SC-12(3)
        - NIST-800-53-SI-7
        - PCI-DSS-Req-6.2
        - ensure_redhat_gpgkey_installed
        - high_severity
        - medium_complexity
        - medium_disruption
        - no_reboot_needed
        - restrict_strategy

    - name: Read signatures in GPG key
      command: gpg --show-keys --with-fingerprint --with-colons "/etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release"
      args:
        warn: false
      changed_when: false
      register: gpg_fingerprints
      check_mode: false
      tags:
        - CCE-84180-9
        - CJIS-5.10.4.1
        - NIST-800-171-3.4.8
        - NIST-800-53-CM-5(3)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SC-12
        - NIST-800-53-SC-12(3)
        - NIST-800-53-SI-7
        - PCI-DSS-Req-6.2
        - ensure_redhat_gpgkey_installed
        - high_severity
        - medium_complexity
        - medium_disruption
        - no_reboot_needed
        - restrict_strategy

    - name: Set Fact - Installed GPG Fingerprints
      set_fact:
        gpg_installed_fingerprints: |-
          {{ gpg_fingerprints.stdout | regex_findall('^pub.*
          (?:^fpr[:]*)([0-9A-Fa-f]*)', '\1') | list }}
      tags:
        - CCE-84180-9
        - CJIS-5.10.4.1
        - NIST-800-171-3.4.8
        - NIST-800-53-CM-5(3)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SC-12
        - NIST-800-53-SC-12(3)
        - NIST-800-53-SI-7
        - PCI-DSS-Req-6.2
        - ensure_redhat_gpgkey_installed
        - high_severity
        - medium_complexity
        - medium_disruption
        - no_reboot_needed
        - restrict_strategy

    - name: Set Fact - Valid fingerprints
      set_fact:
        gpg_valid_fingerprints: ("567E347AD0044ADE55BA8A5F199E2F91FD431D51" "7E4624258C406535D56D6F135054E4A45A6340B3")
      tags:
        - CCE-84180-9
        - CJIS-5.10.4.1
        - NIST-800-171-3.4.8
        - NIST-800-53-CM-5(3)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SC-12
        - NIST-800-53-SC-12(3)
        - NIST-800-53-SI-7
        - PCI-DSS-Req-6.2
        - ensure_redhat_gpgkey_installed
        - high_severity
        - medium_complexity
        - medium_disruption
        - no_reboot_needed
        - restrict_strategy

    - name: Import RedHat GPG key
      rpm_key:
        state: present
        key: /etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
      when:
        - gpg_key_directory_permission.stat.mode <= '0755'
        - (gpg_installed_fingerprints | difference(gpg_valid_fingerprints)) | length ==
          0
        - gpg_installed_fingerprints | length > 0
        - ansible_distribution == "RedHat"
      tags:
        - CCE-84180-9
        - CJIS-5.10.4.1
        - NIST-800-171-3.4.8
        - NIST-800-53-CM-5(3)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SC-12
        - NIST-800-53-SC-12(3)
        - NIST-800-53-SI-7
        - PCI-DSS-Req-6.2
        - ensure_redhat_gpgkey_installed
        - high_severity
        - medium_complexity
        - medium_disruption
        - no_reboot_needed
        - restrict_strategy
Group Account and Access Control Group contains 4 groups and 9 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 Protect Physical Console Access Group contains 5 rules
[ref] It is impossible to fully protect a system from an attacker with physical access, so securing the space in which the system is located should be considered a necessary step. However, there are some steps which, if taken, make it more difficult for an attacker to quickly or undetectably modify a system from its console.
#### Rule Disable debug-shell SystemD Service [ref]
SystemD's `debug-shell` service is intended to diagnose SystemD related boot issues with various `systemctl` commands. Once enabled and following a system reboot, the root shell will be available on `tty9` which is access by pressing `CTRL-ALT-F9`. The `debug-shell` service should only be used for SystemD related issues and should otherwise be disabled. By default, the `debug-shell` SystemD service is already disabled. The `debug-shell` service can be disabled with the following command:
    $ sudo systemctl mask --now debug-shell.service
Rationale: | This prevents attackers with physical access from trivially bypassing security on the machine through valid troubleshooting configurations and gaining root access when the system is rebooted.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_debug-shell_disabled
Identifiers and References | Identifiers: CCE-90724-6 References: 3.4.5, CCI-000366, 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), CM-6, FIA_UAU.1, SRG-OS-000324-GPOS-00125, SRG-OS-000480-GPOS-00227
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" stop 'debug-shell.service'
    "$SYSTEMCTL_EXEC" disable 'debug-shell.service'
    "$SYSTEMCTL_EXEC" mask 'debug-shell.service'
    # Disable socket activation if we have a unit file for it
    if "$SYSTEMCTL_EXEC" list-unit-files | grep -q '^debug-shell.socket'; then
        "$SYSTEMCTL_EXEC" stop 'debug-shell.socket'
        "$SYSTEMCTL_EXEC" mask 'debug-shell.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 'debug-shell.service' || true

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    - name: Disable service debug-shell
      block:

        - name: Disable service debug-shell
          systemd:
            name: debug-shell.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
          ignore_errors: 'yes'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-90724-6
        - NIST-800-171-3.4.5
        - NIST-800-53-CM-6
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_debug-shell_disabled

    - name: Unit Socket Exists - debug-shell.socket
      command: systemctl list-unit-files debug-shell.socket
      args:
        warn: false
      register: socket_file_exists
      changed_when: false
      ignore_errors: true
      check_mode: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-90724-6
        - NIST-800-171-3.4.5
        - NIST-800-53-CM-6
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_debug-shell_disabled

    - name: Disable socket debug-shell
      systemd:
        name: debug-shell.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - '"debug-shell.socket" in socket_file_exists.stdout_lines[1]'
      tags:
        - CCE-90724-6
        - NIST-800-171-3.4.5
        - NIST-800-53-CM-6
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_debug-shell_disabled
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    include disable_debug-shell

    class disable_debug-shell {
      service {'debug-shell':
        enable => false,
        ensure => 'stopped',
      }
    }
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        systemd:
          units:
          - enabled: false
            name: debug-shell.service
Remediation script ⇲
    [customizations.services]
    disabled = ["debug-shell"]
#### Rule Disable Ctrl-Alt-Del Burst Action [ref]
By default, `SystemD` will reboot the system if the `Ctrl-Alt-Del` key sequence is pressed Ctrl-Alt-Delete more than 7 times in 2 seconds. To configure the system to ignore the `CtrlAltDelBurstAction` setting, add or modify the following to `/etc/systemd/system.conf`:
    CtrlAltDelBurstAction=none
Warning: Disabling the `Ctrl-Alt-Del` key sequence in `/etc/init/control-alt-delete.conf` DOES NOT disable the `Ctrl-Alt-Del` key sequence if running in `runlevel 6` (e.g. in GNOME, KDE, etc.)! The `Ctrl-Alt-Del` key sequence will only be disabled if running in the non-graphical `runlevel 3`.
Rationale: | A locally logged-in user who presses Ctrl-Alt-Del, when at the console, can reboot the system. If accidentally pressed, as could happen in the case of mixed OS environment, this can create the risk of short-term loss of availability of systems due to unintentional reboot.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_disable_ctrlaltdel_burstaction
Identifiers and References | Identifiers: CCE-90308-8 References: 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 3.4.5, CCI-000366, 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), 4.3.3.7.3, SR 2.1, SR 5.2, 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, 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, CM-6(a), AC-6(1), CM-6(a), PR.AC-4, PR.DS-5, SRG-OS-000324-GPOS-00125, SRG-OS-000480-GPOS-00227
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q systemd; then

    # Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
    # Otherwise, regular sed command will do.
    sed_command=('sed' '-i')
    if test -L "/etc/systemd/system.conf"; then
        sed_command+=('--follow-symlinks')
    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' <<< "^CtrlAltDelBurstAction=")

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

    # 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 "^CtrlAltDelBurstAction=\\>" "/etc/systemd/system.conf"; then
        "${sed_command[@]}" "s/^CtrlAltDelBurstAction=\\>.*/$formatted_output/gi" "/etc/systemd/system.conf"
    else
        # \n is precaution for case where file ends without trailing newline
        cce="CCE-90308-8"
        printf '\n# Per %s: Set %s in %s\n' "$cce" "$formatted_output" "/etc/systemd/system.conf" >> "/etc/systemd/system.conf"
        printf '%s\n' "$formatted_output" >> "/etc/systemd/system.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,CtrlAltDelBurstAction%3Dnone
            mode: 0644
            path: /etc/systemd/system.conf.d/disable_ctrlaltdelete_burstaction.conf
            overwrite: true
#### Rule Disable Ctrl-Alt-Del Reboot Activation [ref]
By default, `SystemD` will reboot the system if the `Ctrl-Alt-Del` key sequence is pressed. To configure the system to ignore the `Ctrl-Alt-Del` key sequence from the command line instead of rebooting the system, do either of the following:
    ln -sf /dev/null /etc/systemd/system/ctrl-alt-del.target
or
    systemctl mask ctrl-alt-del.target
Do not simply delete the `/usr/lib/systemd/system/ctrl-alt-del.service` file, as this file may be restored during future system updates.
Rationale: | A locally logged-in user who presses Ctrl-Alt-Del, when at the console, can reboot the system. If accidentally pressed, as could happen in the case of mixed OS environment, this can create the risk of short-term loss of availability of systems due to unintentional reboot.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_disable_ctrlaltdel_reboot
Identifiers and References | Identifiers: CCE-86667-3 References: 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 3.4.5, CCI-000366, 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), 4.3.3.7.3, SR 2.1, SR 5.2, 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, 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, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, SRG-OS-000324-GPOS-00125, SRG-OS-000480-GPOS-00227
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    systemctl disable --now ctrl-alt-del.target
    systemctl mask --now ctrl-alt-del.target

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
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: ctrl-alt-del.target
            mask: true
#### Rule Verify that Interactive Boot is Disabled [ref]
Red Hat Enterprise Linux 9 systems support an "interactive boot" option that can be used to prevent services from being started. On a Red Hat Enterprise Linux 9 system, interactive boot can be enabled by providing a `1`, `yes`, `true`, or `on` value to the `systemd.confirm_spawn` kernel argument in `/etc/default/grub`. Remove any instance of
    systemd.confirm_spawn=(1|yes|true|on)
from the kernel arguments in that file to disable interactive boot. It is also required to change the runtime configuration, run:
    /sbin/grubby --update-kernel=ALL --remove-args="systemd.confirm_spawn"
Rationale: | Using interactive boot, the console user could disable auditing, firewalls, or other services, weakening system security.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_grub2_disable_interactive_boot
Identifiers and References | Identifiers: CCE-87114-5 References: 11, 12, 14, 15, 16, 18, 3, 5, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.03, DSS06.06, 3.1.2, 3.4.5, CCI-000213, 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), 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, 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, 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, SC-2(1), CM-6(a), PR.AC-4, PR.AC-6, PR.PT-3, FIA_UAU.1, SRG-OS-000480-GPOS-00227
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q grub2-common; then

    CONFIRM_SPAWN_YES="systemd.confirm_spawn=\(1\|yes\|true\|on\)"
    CONFIRM_SPAWN_NO="systemd.confirm_spawn=no"

    if grep -q "\(GRUB_CMDLINE_LINUX\|GRUB_CMDLINE_LINUX_DEFAULT\)" /etc/default/grub
    then
    	sed -i "s/${CONFIRM_SPAWN_YES}/${CONFIRM_SPAWN_NO}/" /etc/default/grub
    fi
    # Remove 'systemd.confirm_spawn' kernel argument also from runtime settings
    /sbin/grubby --update-kernel=ALL --remove-args="systemd.confirm_spawn"

    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:
        - CCE-87114-5
        - NIST-800-171-3.1.2
        - NIST-800-171-3.4.5
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SC-2(1)
        - grub2_disable_interactive_boot
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Verify that Interactive Boot is Disabled in /etc/default/grub
      replace:
        dest: /etc/default/grub
        regexp: systemd.confirm_spawn=(1|yes|true|on)
        replace: systemd.confirm_spawn=no
      when: '"grub2-common" in ansible_facts.packages'
      tags:
        - CCE-87114-5
        - NIST-800-171-3.1.2
        - NIST-800-171-3.4.5
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SC-2(1)
        - grub2_disable_interactive_boot
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Verify that Interactive Boot is Disabled (runtime)
      command: /sbin/grubby --update-kernel=ALL --remove-args="systemd.confirm_spawn"
      when: '"grub2-common" in ansible_facts.packages'
      tags:
        - CCE-87114-5
        - NIST-800-171-3.1.2
        - NIST-800-171-3.4.5
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SC-2(1)
        - grub2_disable_interactive_boot
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
#### Rule Require Authentication for Single User Mode [ref]
Single-user mode is intended as a system recovery method, providing a single user root access to the system by providing a boot option at startup. By default, no authentication is performed if single-user mode is selected. By default, single-user mode is protected by requiring a password and is set in `/usr/lib/systemd/system/rescue.service`.
Rationale: | This prevents attackers with physical access from trivially bypassing security on the machine and gaining root access. Such accesses are further prevented by configuring the bootloader password.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_require_singleuser_auth
Identifiers and References | Identifiers: CCE-83594-2 References: 1, 11, 12, 14, 15, 16, 18, 3, 5, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.06, DSS06.10, 3.1.1, 3.4.5, CCI-000213, 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), 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, 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, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, 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, 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, IA-2, AC-3, CM-6(a), PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.PT-3, FIA_UAU.1, SRG-OS-000080-GPOS-00048
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    service_file="/usr/lib/systemd/system/rescue.service"

    sulogin="/usr/lib/systemd/systemd-sulogin-shell rescue"

    if grep "^ExecStart=.*" "$service_file" ; then
        sed -i "s%^ExecStart=.*%ExecStart=-$sulogin%" "$service_file"
    else
        echo "ExecStart=-$sulogin" >> "$service_file"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: require single user mode password
      lineinfile:
        create: true
        dest: /usr/lib/systemd/system/rescue.service
        regexp: ^#?ExecStart=
        line: ExecStart=-/usr/lib/systemd/systemd-sulogin-shell rescue
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83594-2
        - NIST-800-171-3.1.1
        - NIST-800-171-3.4.5
        - NIST-800-53-AC-3
        - NIST-800-53-CM-6(a)
        - NIST-800-53-IA-2
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - require_singleuser_auth
        - restrict_strategy
Group Protect Accounts by Restricting Password-Based Login Group contains 2 groups and 4 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 Verify Proper Storage and Existence of Password Hashes Group contains 1 rule
[ref] By default, password hashes for local accounts are stored in the second field (colon-separated) in `/etc/shadow`. This file should be readable only by processes running with root credentials, preventing users from casually accessing others' password hashes and attempting to crack them. However, it remains possible to misconfigure the system and store password hashes in world-readable files such as `/etc/passwd`, or to even store passwords themselves in plaintext on the system. Using system-provided tools for password change/creation should allow administrators to avoid such misconfiguration.
#### Rule Prevent Login to Accounts With Empty Password [ref]
If an account is configured for password authentication but does not have an assigned password, it may be possible to log into the account without authentication. Remove any instances of the `nullok` in `/etc/pam.d/system-auth` to prevent logins with empty passwords. 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. Note that this rule is not applicable for systems running within a container. Having user with empty password within a container is not considered a risk, because it should not be possible to directly login into a container anyway.
Rationale: | If an account has an empty password, anyone could log in and run commands with the privileges of that account. Accounts with empty passwords should never be used in operational environments.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_no_empty_passwords
Identifiers and References | Identifiers: CCE-83611-4 References: 1, 12, 13, 14, 15, 16, 18, 3, 5, 5.5.2, APO01.06, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.02, DSS06.03, DSS06.10, 3.1.1, 3.1.5, CCI-000366, 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), 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, 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 5.2, 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.18.1.4, 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.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, IA-5(1)(a), IA-5(c), CM-6(a), PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.DS-5, FIA_UAU.1, Req-8.2.3, SRG-OS-000480-GPOS-00227
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Strategy: | configure
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    SYSTEM_AUTH="/etc/pam.d/system-auth"
    PASSWORD_AUTH="/etc/pam.d/password-auth"

    if [ -f /usr/bin/authselect ]; then
        if authselect check; then
            authselect enable-feature without-nullok
            authselect apply-changes
        else
            echo "
    authselect integrity check failed. Remediation aborted!
    This remediation could not be applied because the authselect profile is not intact.
    It is not recommended to manually edit the PAM files when authselect is available
    In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
            false
        fi
    else
        sed --follow-symlinks -i 's/\<nullok\>//g' $SYSTEM_AUTH
        sed --follow-symlinks -i 's/\<nullok\>//g' $PASSWORD_AUTH
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Strategy: | configure
    - name: Check if system relies on authselect
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83611-4
        - CJIS-5.5.2
        - NIST-800-171-3.1.1
        - NIST-800-171-3.1.5
        - NIST-800-53-CM-6(a)
        - NIST-800-53-IA-5(1)(a)
        - NIST-800-53-IA-5(c)
        - PCI-DSS-Req-8.2.3
        - configure_strategy
        - high_severity
        - low_complexity
        - medium_disruption
        - no_empty_passwords
        - no_reboot_needed

    - name: Check integrity of authselect current profile
      ansible.builtin.command:
        cmd: authselect check
      register: result_authselect_check_cmd
      changed_when: false
      ignore_errors: true
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - result_authselect_present.stat.exists
      tags:
        - CCE-83611-4
        - CJIS-5.5.2
        - NIST-800-171-3.1.1
        - NIST-800-171-3.1.5
        - NIST-800-53-CM-6(a)
        - NIST-800-53-IA-5(1)(a)
        - NIST-800-53-IA-5(c)
        - PCI-DSS-Req-8.2.3
        - configure_strategy
        - high_severity
        - low_complexity
        - medium_disruption
        - no_empty_passwords
        - no_reboot_needed

    - name: Get authselect current features
      ansible.builtin.shell:
        cmd: authselect current | tail -n+3 | awk '{ print $2 }'
      register: result_authselect_features
      changed_when: false
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - result_authselect_present.stat.exists
        - result_authselect_check_cmd is success
      tags:
        - CCE-83611-4
        - CJIS-5.5.2
        - NIST-800-171-3.1.1
        - NIST-800-171-3.1.5
        - NIST-800-53-CM-6(a)
        - NIST-800-53-IA-5(1)(a)
        - NIST-800-53-IA-5(c)
        - PCI-DSS-Req-8.2.3
        - configure_strategy
        - high_severity
        - low_complexity
        - medium_disruption
        - no_empty_passwords
        - no_reboot_needed

    - name: Ensure nullok property is absent via authselect tool
      ansible.builtin.command:
        cmd: authselect enable-feature without-nullok
      register: result_authselect_cmd
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - result_authselect_present.stat.exists
        - result_authselect_check_cmd is success
        - result_authselect_features.stdout is not search("without-nullok")
      tags:
        - CCE-83611-4
        - CJIS-5.5.2
        - NIST-800-171-3.1.1
        - NIST-800-171-3.1.5
        - NIST-800-53-CM-6(a)
        - NIST-800-53-IA-5(1)(a)
        - NIST-800-53-IA-5(c)
        - PCI-DSS-Req-8.2.3
        - configure_strategy
        - high_severity
        - low_complexity
        - medium_disruption
        - no_empty_passwords
        - no_reboot_needed

    - name: Informative message based on the authselect integrity check result
      ansible.builtin.assert:
        that:
          - result_authselect_check_cmd is success
        fail_msg:
          - authselect integrity check failed. Remediation aborted!
          - This remediation could not be applied because the authselect profile is not
            intact.
          - It is not recommended to manually edit the PAM files when authselect 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
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83611-4
        - CJIS-5.5.2
        - NIST-800-171-3.1.1
        - NIST-800-171-3.1.5
        - NIST-800-53-CM-6(a)
        - NIST-800-53-IA-5(1)(a)
        - NIST-800-53-IA-5(c)
        - PCI-DSS-Req-8.2.3
        - configure_strategy
        - high_severity
        - low_complexity
        - medium_disruption
        - no_empty_passwords
        - no_reboot_needed

    - name: Ensure nullok property is absent by directly editing pam files
      replace:
        dest: '{{ item }}'
        regexp: nullok
      loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - not result_authselect_present.stat.exists
      tags:
        - CCE-83611-4
        - CJIS-5.5.2
        - NIST-800-171-3.1.1
        - NIST-800-171-3.1.5
        - NIST-800-53-CM-6(a)
        - NIST-800-53-IA-5(1)(a)
        - NIST-800-53-IA-5(c)
        - PCI-DSS-Req-8.2.3
        - configure_strategy
        - high_severity
        - low_complexity
        - medium_disruption
        - no_empty_passwords
        - no_reboot_needed
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,%23%20Generated%20by%20authselect%20on%20Sat%20Oct%2027%2014%3A59%3A36%202018%0A%23%20Do%20not%20modify%20this%20file%20manually.%0A%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_env.so%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_faildelay.so%20delay%3D2000000%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_fprintd.so%0Aauth%20%20%20%20%20%20%20%20%5Bdefault%3D1%20ignore%3Dignore%20success%3Dok%5D%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3E%3D%201000%20quiet%0Aauth%20%20%20%20%20%20%20%20%5Bdefault%3D1%20ignore%3Dignore%20success%3Dok%5D%20%20%20%20%20%20%20%20%20pam_localuser.so%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%20try_first_pass%0Aauth%20%20%20%20%20%20%20%20requisite%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3E%3D%201000%20quiet_success%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%20forward_pass%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_deny.so%0A%0Aaccount%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%0Aaccount%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_localuser.so%0Aaccount%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3C%201000%20quiet%0Aaccount%20%20%20%20%20%5Bdefault%3Dbad%20success%3Dok%20user_unknown%3Dignore%5D%20pam_sss.so%0Aaccount%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_permit.so%0A%0Apassword%20%20%20%20requisite%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_pwquality.so%20try_first_pass%20local_users_only%0Apassword%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%20sha512%20shadow%20try_first_pass%20use_authtok%0Apassword%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%20use_authtok%0Apassword%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_deny.so%0A%0Asession%20%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_keyinit.so%20revoke%0Asession%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_limits.so%0A-session%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_systemd.so%0Asession%20%20%20%20%20%5Bsuccess%3D1%20default%3Dignore%5D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20service%20in%20crond%20quiet%20use_uid%0Asession%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%0Asession%20%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%0A
            mode: 0644
            path: /etc/pam.d/password-auth
            overwrite: true
          - contents:
              source: data:,%23%20Generated%20by%20authselect%20on%20Sat%20Oct%2027%2014%3A59%3A36%202018%0A%23%20Do%20not%20modify%20this%20file%20manually.%0A%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_env.so%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_faildelay.so%20delay%3D2000000%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_fprintd.so%0Aauth%20%20%20%20%20%20%20%20%5Bdefault%3D1%20ignore%3Dignore%20success%3Dok%5D%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3E%3D%201000%20quiet%0Aauth%20%20%20%20%20%20%20%20%5Bdefault%3D1%20ignore%3Dignore%20success%3Dok%5D%20%20%20%20%20%20%20%20%20pam_localuser.so%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%20try_first_pass%0Aauth%20%20%20%20%20%20%20%20requisite%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3E%3D%201000%20quiet_success%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%20forward_pass%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_deny.so%0A%0Aaccount%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%0Aaccount%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_localuser.so%0Aaccount%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3C%201000%20quiet%0Aaccount%20%20%20%20%20%5Bdefault%3Dbad%20success%3Dok%20user_unknown%3Dignore%5D%20pam_sss.so%0Aaccount%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_permit.so%0A%0Apassword%20%20%20%20requisite%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_pwquality.so%20try_first_pass%20local_users_only%0Apassword%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%20sha512%20shadow%20try_first_pass%20use_authtok%0Apassword%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%20use_authtok%0Apassword%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_deny.so%0A%0Asession%20%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_keyinit.so%20revoke%0Asession%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_limits.so%0A-session%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_systemd.so%0Asession%20%20%20%20%20%5Bsuccess%3D1%20default%3Dignore%5D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20service%20in%20crond%20quiet%20use_uid%0Asession%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%0Asession%20%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%0A
            mode: 0644
            path: /etc/pam.d/system-auth
            overwrite: true
Group Restrict Root Logins Group contains 3 rules
[ref] Direct root logins should be allowed only for emergency use. In normal situations, the administrator should access the system via a unique unprivileged account, and then use `su` or `sudo` to execute privileged commands. Discouraging administrators from accessing the root account directly ensures an audit trail in organizations with multiple administrators. Locking down the channels through which root can connect directly also reduces opportunities for password-guessing against the root account. The `login` program uses the file `/etc/securetty` to determine which interfaces should allow root logins. The virtual devices `/dev/console` and `/dev/tty*` represent the system consoles (accessible via the Ctrl-Alt-F1 through Ctrl-Alt-F6 keyboard sequences on a default installation). The default securetty file also contains `/dev/vc/*`. These are likely to be deprecated in most environments, but may be retained for compatibility. Root should also be prohibited from connecting via network protocols. Other sections of this document include guidance describing how to prevent root from logging in via SSH.
#### Rule Direct root Logins Not Allowed [ref]
To further limit access to the `root` account, administrators can disable root logins at the console by editing the `/etc/securetty` file. This file lists all devices the root user is allowed to login to. If the file does not exist at all, the root user can login through any communication device on the system, whether via the console or via a raw network interface. This is dangerous as user can login to the system as root via Telnet, which sends the password in plain text over the network. By default, Red Hat Enterprise Linux 9's `/etc/securetty` file only allows the root user to login at the console physically attached to the system. To prevent root from logging in, remove the contents of this file. To prevent direct root logins, remove the contents of this file by typing the following command:
    $ sudo echo > /etc/securetty
Rationale: | Disabling direct root logins ensures proper accountability and multifactor authentication to privileged accounts. Users will first login, then escalate to privileged (root) access via su / sudo. This is required for FISMA Low and FISMA Moderate systems.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_no_direct_root_logins
Identifiers and References | Identifiers: CCE-83625-4 References: BP28(R19), 1, 12, 15, 16, 5, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.1.1, 3.1.6, 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), 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, 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, 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, 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, IA-2, CM-6(a), PR.AC-1, PR.AC-6, PR.AC-7
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    echo > /etc/securetty

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Direct root Logins Not Allowed
      copy:
        dest: /etc/securetty
        content: ''
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83625-4
        - NIST-800-171-3.1.1
        - NIST-800-171-3.1.6
        - NIST-800-53-CM-6(a)
        - NIST-800-53-IA-2
        - low_complexity
        - low_disruption
        - medium_severity
        - no_direct_root_logins
        - no_reboot_needed
        - restrict_strategy
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,
            mode: 0600
            path: /etc/securetty
            overwrite: true
#### Rule Restrict Serial Port Root Logins [ref]
To restrict root logins on serial ports, ensure lines of this form do not appear in `/etc/securetty`:
    ttyS0
    ttyS1
Rationale: | Preventing direct root login to serial port interfaces helps ensure accountability for actions taken on the systems using the root account.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_restrict_serial_port_logins
Identifiers and References | Identifiers: CCE-83622-1 References: 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 3.1.1, 3.1.5, CCI-000770, 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), 4.3.3.7.3, SR 2.1, SR 5.2, 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, 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, AC-6, CM-6(a), PR.AC-4, PR.DS-5
Remediation Shell script ⇲
    sed -i '/ttyS/d' /etc/securetty
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Restrict Serial Port Root Logins
      lineinfile:
        dest: /etc/securetty
        regexp: ttyS[0-9]
        state: absent
      tags:
        - CCE-83622-1
        - NIST-800-171-3.1.1
        - NIST-800-171-3.1.5
        - NIST-800-53-AC-6
        - NIST-800-53-CM-6(a)
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_serial_port_logins
        - restrict_strategy
#### Rule Restrict Virtual Console Root Logins [ref]
To restrict root logins through the (deprecated) virtual console devices, ensure lines of this form do not appear in `/etc/securetty`:
    vc/1
    vc/2
    vc/3
    vc/4
Rationale: | Preventing direct root login to virtual console devices helps ensure accountability for actions taken on the system using the root account.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_securetty_root_login_console_only
Identifiers and References | Identifiers: CCE-83626-2 References: 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 3.1.1, 3.1.5, CCI-000770, 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), 4.3.3.7.3, SR 2.1, SR 5.2, 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, 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, AC-6, CM-6(a), PR.AC-4, PR.DS-5, SRG-OS-000324-GPOS-00125
Remediation Shell script ⇲
    sed -i '/^vc\//d' /etc/securetty
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Restrict Virtual Console Root Logins
      lineinfile:
        dest: /etc/securetty
        regexp: ^vc
        state: absent
      tags:
        - CCE-83626-2
        - NIST-800-171-3.1.1
        - NIST-800-171-3.1.5
        - NIST-800-53-AC-6
        - NIST-800-53-CM-6(a)
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
        - securetty_root_login_console_only
Group System Accounting with auditd Group contains 10 groups and 70 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 Fedora 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 8 groups and 66 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
Identifiers and References | Identifiers: CCE-83830-0 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000126, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.5.5, 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-000458-VMM-001810, SRG-OS-000474-VMM-001940
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83830-0
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_chmod
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit chmod tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83830-0
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_chmod
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for chmod for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - chmod
            syscall_grouping:
              - chmod
              - fchmod
              - fchmodat

        - name: Check existence of chmod in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - chmod
            syscall_grouping:
              - chmod
              - fchmod
              - fchmodat

        - name: Check existence of chmod in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83830-0
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_chmod
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for chmod for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - chmod
            syscall_grouping:
              - chmod
              - fchmod
              - fchmodat

        - name: Check existence of chmod in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - chmod
            syscall_grouping:
              - chmod
              - fchmod
              - fchmodat

        - name: Check existence of chmod in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83830-0
        - CJIS-5.4.1.1
        - 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
        - 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
Identifiers and References | Identifiers: CCE-83812-8 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000126, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.5.5, 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, SRG-OS-000458-VMM-001810, SRG-OS-000474-VMM-001940
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83812-8
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_chown
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit chown tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83812-8
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_chown
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for chown for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - chown
            syscall_grouping:
              - chown
              - fchown
              - fchownat
              - lchown

        - name: Check existence of chown in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - chown
            syscall_grouping:
              - chown
              - fchown
              - fchownat
              - lchown

        - name: Check existence of chown in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83812-8
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_chown
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for chown for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - chown
            syscall_grouping:
              - chown
              - fchown
              - fchownat
              - lchown

        - name: Check existence of chown in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - chown
            syscall_grouping:
              - chown
              - fchown
              - fchownat
              - lchown

        - name: Check existence of chown in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83812-8
        - CJIS-5.4.1.1
        - 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
        - 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
Identifiers and References | Identifiers: CCE-83832-6 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000126, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.5.5, 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-000458-VMM-001810, SRG-OS-000474-VMM-001940
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83832-6
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fchmod
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit fchmod tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83832-6
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fchmod
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for fchmod for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fchmod
            syscall_grouping:
              - chmod
              - fchmod
              - fchmodat

        - name: Check existence of fchmod in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fchmod
            syscall_grouping:
              - chmod
              - fchmod
              - fchmodat

        - name: Check existence of fchmod in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83832-6
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fchmod
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for fchmod for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fchmod
            syscall_grouping:
              - chmod
              - fchmod
              - fchmodat

        - name: Check existence of fchmod in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fchmod
            syscall_grouping:
              - chmod
              - fchmod
              - fchmodat

        - name: Check existence of fchmod in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83832-6
        - CJIS-5.4.1.1
        - 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
        - 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
Identifiers and References | Identifiers: CCE-83822-7 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000126, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.5.5, 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-000458-VMM-001810, SRG-OS-000474-VMM-001940
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83822-7
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fchmodat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit fchmodat tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83822-7
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fchmodat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for fchmodat for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fchmodat
            syscall_grouping:
              - chmod
              - fchmod
              - fchmodat

        - name: Check existence of fchmodat in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fchmodat
            syscall_grouping:
              - chmod
              - fchmod
              - fchmodat

        - name: Check existence of fchmodat in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83822-7
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fchmodat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for fchmodat for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fchmodat
            syscall_grouping:
              - chmod
              - fchmod
              - fchmodat

        - name: Check existence of fchmodat in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fchmodat
            syscall_grouping:
              - chmod
              - fchmod
              - fchmodat

        - name: Check existence of fchmodat in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83822-7
        - CJIS-5.4.1.1
        - 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
        - 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
Identifiers and References | Identifiers: CCE-83829-2 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000126, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.5.5, 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, SRG-OS-000458-VMM-001810, SRG-OS-000474-VMM-001940
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83829-2
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fchown
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit fchown tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83829-2
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fchown
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for fchown for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fchown
            syscall_grouping:
              - chown
              - fchown
              - fchownat
              - lchown

        - name: Check existence of fchown in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fchown
            syscall_grouping:
              - chown
              - fchown
              - fchownat
              - lchown

        - name: Check existence of fchown in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83829-2
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fchown
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for fchown for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fchown
            syscall_grouping:
              - chown
              - fchown
              - fchownat
              - lchown

        - name: Check existence of fchown in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fchown
            syscall_grouping:
              - chown
              - fchown
              - fchownat
              - lchown

        - name: Check existence of fchown in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83829-2
        - CJIS-5.4.1.1
        - 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
        - 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
Identifiers and References | Identifiers: CCE-83831-8 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000126, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.5.5, 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, SRG-OS-000458-VMM-001810, SRG-OS-000474-VMM-001940
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83831-8
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fchownat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit fchownat tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83831-8
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fchownat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for fchownat for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fchownat
            syscall_grouping:
              - chown
              - fchown
              - fchownat
              - lchown

        - name: Check existence of fchownat in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fchownat
            syscall_grouping:
              - chown
              - fchown
              - fchownat
              - lchown

        - name: Check existence of fchownat in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83831-8
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fchownat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for fchownat for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fchownat
            syscall_grouping:
              - chown
              - fchown
              - fchownat
              - lchown

        - name: Check existence of fchownat in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fchownat
            syscall_grouping:
              - chown
              - fchown
              - fchownat
              - lchown

        - name: Check existence of fchownat in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83831-8
        - CJIS-5.4.1.1
        - 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
        - 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
Identifiers and References | Identifiers: CCE-83821-9 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.5.5, 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-000064-GPOS-00033, SRG-OS-000458-VMM-001810, SRG-OS-000474-VMM-001940
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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 o-rwx ${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 o-rwx ${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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83821-9
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fremovexattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit fremovexattr tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83821-9
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fremovexattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for fremovexattr for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fremovexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of fremovexattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fremovexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of fremovexattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fremovexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of fremovexattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fremovexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of fremovexattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83821-9
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fremovexattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for fremovexattr for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fremovexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of fremovexattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fremovexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of fremovexattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fremovexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of fremovexattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fremovexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of fremovexattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83821-9
        - CJIS-5.4.1.1
        - 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
        - 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
Identifiers and References | Identifiers: CCE-83817-7 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000126, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.5.5, 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-000064-GPOS-00033, SRG-OS-000458-VMM-001810, SRG-OS-000474-VMM-001940
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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 o-rwx ${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 o-rwx ${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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83817-7
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fsetxattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit fsetxattr tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83817-7
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fsetxattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for fsetxattr for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fsetxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of fsetxattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fsetxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of fsetxattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fsetxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of fsetxattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fsetxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of fsetxattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83817-7
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_fsetxattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for fsetxattr for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fsetxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of fsetxattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fsetxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of fsetxattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fsetxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of fsetxattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - fsetxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of fsetxattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83817-7
        - CJIS-5.4.1.1
        - 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
        - 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
Identifiers and References | Identifiers: CCE-83833-4 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000126, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.5.5, 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, SRG-OS-000458-VMM-001810, SRG-OS-000474-VMM-001940
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83833-4
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_lchown
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit lchown tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83833-4
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_lchown
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for lchown for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lchown
            syscall_grouping:
              - chown
              - fchown
              - fchownat
              - lchown

        - name: Check existence of lchown in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lchown
            syscall_grouping:
              - chown
              - fchown
              - fchownat
              - lchown

        - name: Check existence of lchown in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83833-4
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_lchown
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for lchown for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lchown
            syscall_grouping:
              - chown
              - fchown
              - fchownat
              - lchown

        - name: Check existence of lchown in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lchown
            syscall_grouping:
              - chown
              - fchown
              - fchownat
              - lchown

        - name: Check existence of lchown in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83833-4
        - CJIS-5.4.1.1
        - 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
        - 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
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
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
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
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
Identifiers and References | Identifiers: CCE-83814-4 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.5.5, 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, SRG-OS-000458-VMM-001810, SRG-OS-000474-VMM-001940
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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 o-rwx ${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 o-rwx ${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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83814-4
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_lremovexattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit lremovexattr tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83814-4
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_lremovexattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for lremovexattr for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lremovexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of lremovexattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lremovexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of lremovexattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lremovexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of lremovexattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lremovexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of lremovexattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83814-4
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_lremovexattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for lremovexattr for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lremovexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of lremovexattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lremovexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of lremovexattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lremovexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of lremovexattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lremovexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of lremovexattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83814-4
        - CJIS-5.4.1.1
        - 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
        - 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
Identifiers and References | Identifiers: CCE-83808-6 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000126, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.5.5, 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-000064-GPOS-00033, SRG-OS-000458-VMM-001810, SRG-OS-000474-VMM-001940
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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 o-rwx ${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 o-rwx ${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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83808-6
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_lsetxattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit lsetxattr tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83808-6
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_lsetxattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for lsetxattr for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lsetxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of lsetxattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lsetxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of lsetxattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lsetxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of lsetxattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lsetxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of lsetxattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83808-6
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_lsetxattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for lsetxattr for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lsetxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of lsetxattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lsetxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of lsetxattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lsetxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of lsetxattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - lsetxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of lsetxattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83808-6
        - CJIS-5.4.1.1
        - 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
        - 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
Identifiers and References | Identifiers: CCE-83807-8 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.5.5, 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, SRG-OS-000458-VMM-001810, SRG-OS-000474-VMM-001940
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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 o-rwx ${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 o-rwx ${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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83807-8
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_removexattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit removexattr tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83807-8
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_removexattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for removexattr for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - removexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of removexattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - removexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of removexattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - removexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of removexattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - removexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of removexattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83807-8
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_removexattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for removexattr for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - removexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of removexattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - removexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of removexattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - removexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of removexattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - removexattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of removexattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83807-8
        - CJIS-5.4.1.1
        - 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
        - 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
Identifiers and References | Identifiers: CCE-83811-0 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000126, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.5.5, 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-000458-GPOS-00203, SRG-OS-000458-VMM-001810, SRG-OS-000474-VMM-001940
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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 o-rwx ${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 o-rwx ${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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83811-0
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_setxattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit setxattr tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83811-0
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_setxattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for setxattr for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - setxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of setxattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - setxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of setxattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - setxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of setxattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - setxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of setxattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83811-0
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_setxattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for setxattr for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - setxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of setxattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - setxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of setxattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - setxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of setxattr in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - setxattr
            syscall_grouping:
              - fremovexattr
              - lremovexattr
              - removexattr
              - fsetxattr
              - lsetxattr
              - setxattr

        - name: Check existence of setxattr in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid=0 -F
              key=perm_mod
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83811-0
        - CJIS-5.4.1.1
        - 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
        - audit_rules_dac_modification_setxattr
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
Group Record Execution Attempts to Run SELinux Privileged Commands Group contains 4 rules
[ref] At a minimum, the audit system should collect the execution of SELinux privileged commands for all users and root.
#### Rule Record Any Attempts to Run chcon [ref]
At a minimum, the audit system should collect any execution attempt of the `chcon` command 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`:
    -a always,exit -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
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:
    -a always,exit -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_execution_chcon
Identifiers and References | Identifiers: CCE-83748-4 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, 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-000468-GPOS-00212, SRG-OS-000471-GPOS-00215, SRG-OS-000463-GPOS-00207, SRG-OS-000465-GPOS-00209, SRG-OS-000463-VMM-001850
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/bin/chcon -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83748-4
        - 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)
        - audit_rules_execution_chcon
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/bin/chcon
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/chcon -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chcon -F perm=x
              -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chcon -F perm=x
              -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/chcon -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chcon -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chcon -F perm=x
              -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83748-4
        - 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)
        - audit_rules_execution_chcon
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Record Any Attempts to Run restorecon [ref]
At a minimum, the audit system should collect any execution attempt of the `restorecon` command 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`:
    -a always,exit -F path=/usr/sbin/restorecon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
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:
    -a always,exit -F path=/usr/sbin/restorecon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_execution_restorecon
Identifiers and References | Identifiers: CCE-83749-2 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000392-GPOS-00172, SRG-OS-000463-GPOS-00207, SRG-OS-000465-GPOS-00209, SRG-OS-000463-VMM-001850
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/sbin/restorecon -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83749-2
        - 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)
        - audit_rules_execution_restorecon
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/sbin/restorecon
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/sbin/restorecon -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/restorecon -F perm=x
              -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/restorecon
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/sbin/restorecon -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/restorecon -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/restorecon
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83749-2
        - 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)
        - audit_rules_execution_restorecon
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Record Any Attempts to Run semanage [ref]
At a minimum, the audit system should collect any execution attempt of the `semanage` command 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`:
    -a always,exit -F path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
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:
    -a always,exit -F path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_execution_semanage
Identifiers and References | Identifiers: CCE-83750-0 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, 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, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, 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-000463-GPOS-00207, SRG-OS-000465-GPOS-00209, SRG-OS-000463-VMM-001850
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/sbin/semanage -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83750-0
        - 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)
        - audit_rules_execution_semanage
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/sbin/semanage
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/sbin/semanage -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/semanage -F perm=x
              -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/semanage
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/sbin/semanage -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/semanage -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/semanage
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83750-0
        - 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)
        - audit_rules_execution_semanage
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Record Any Attempts to Run setsebool [ref]
At a minimum, the audit system should collect any execution attempt of the `setsebool` command 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`:
    -a always,exit -F path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
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:
    -a always,exit -F path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_execution_setsebool
Identifiers and References | Identifiers: CCE-83751-8 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, 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-000463-GPOS-00207, SRG-OS-000465-GPOS-00209, SRG-OS-000463-VMM-001850
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/sbin/setsebool -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83751-8
        - 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)
        - audit_rules_execution_setsebool
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/sbin/setsebool
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/sbin/setsebool -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/setsebool -F perm=x
              -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/setsebool
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/sbin/setsebool -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/setsebool -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/setsebool
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83751-8
        - 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)
        - audit_rules_execution_setsebool
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
Group Record File Deletion Events by User Group contains 5 rules
[ref] At a minimum, the audit system should collect file deletion events 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`, setting ARCH to either b32 or b64 as appropriate for your system:
    -a always,exit -F arch=ARCH -S rmdir,unlink,unlinkat,rename,renameat -F auid>=1000 -F auid!=unset -F key=delete
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, setting ARCH to either b32 or b64 as appropriate for your system:
    -a always,exit -F arch=ARCH -S rmdir,unlink,unlinkat,rename,renameat -F auid>=1000 -F auid!=unset -F key=delete
#### Rule Ensure auditd Collects File Deletion Events by User - rename [ref]
At a minimum, the audit system should collect file deletion events 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`, setting ARCH to either b32 or b64 as appropriate for your system:
    -a always,exit -F arch=ARCH -S rename -F auid>=1000 -F auid!=unset -F key=delete
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, setting ARCH to either b32 or b64 as appropriate for your system:
    -a always,exit -F arch=ARCH -S rename -F auid>=1000 -F auid!=unset -F key=delete
Rationale: | Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_file_deletion_events_rename
Identifiers and References | Identifiers: CCE-83754-2 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 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, 3.1.7, CCI-000169, CCI-000172, CCI-000366, CCI-002884, 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), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.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, 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, A.11.2.4, 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.1.1, 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, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.MA-2, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.7, 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-000466-GPOS-00210, SRG-OS-000467-GPOS-00211, SRG-OS-000468-GPOS-00212, SRG-OS-000466-VMM-001870, SRG-OS-000468-VMM-001890
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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="rename"
    	KEY="delete"
    	SYSCALL_GROUPING="unlink unlinkat rename renameat rmdir"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83754-2
        - 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.2.7
        - audit_rules_file_deletion_events_rename
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit rename tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83754-2
        - 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.2.7
        - audit_rules_file_deletion_events_rename
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for rename for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - rename
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of rename in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/delete.rules
          set_fact: audit_file="/etc/audit/rules.d/delete.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - rename
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of rename in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83754-2
        - 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.2.7
        - audit_rules_file_deletion_events_rename
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for rename for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - rename
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of rename in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/delete.rules
          set_fact: audit_file="/etc/audit/rules.d/delete.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - rename
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of rename in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83754-2
        - 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.2.7
        - audit_rules_file_deletion_events_rename
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
#### Rule Ensure auditd Collects File Deletion Events by User - renameat [ref]
At a minimum, the audit system should collect file deletion events 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`, setting ARCH to either b32 or b64 as appropriate for your system:
    -a always,exit -F arch=ARCH -S renameat -F auid>=1000 -F auid!=unset -F key=delete
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, setting ARCH to either b32 or b64 as appropriate for your system:
    -a always,exit -F arch=ARCH -S renameat -F auid>=1000 -F auid!=unset -F key=delete
Rationale: | Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_file_deletion_events_renameat
Identifiers and References | Identifiers: CCE-83756-7 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 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, 3.1.7, CCI-000169, CCI-000172, CCI-000366, CCI-002884, 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), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.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, 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, A.11.2.4, 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.1.1, 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, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.MA-2, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.7, 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-000466-GPOS-00210, SRG-OS-000467-GPOS-00211, SRG-OS-000468-GPOS-00212, SRG-OS-000466-VMM-001870, SRG-OS-000468-VMM-001890
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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="renameat"
    	KEY="delete"
    	SYSCALL_GROUPING="unlink unlinkat rename renameat rmdir"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83756-7
        - 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.2.7
        - audit_rules_file_deletion_events_renameat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit renameat tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83756-7
        - 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.2.7
        - audit_rules_file_deletion_events_renameat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for renameat for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - renameat
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of renameat in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/delete.rules
          set_fact: audit_file="/etc/audit/rules.d/delete.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - renameat
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of renameat in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83756-7
        - 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.2.7
        - audit_rules_file_deletion_events_renameat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for renameat for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - renameat
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of renameat in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/delete.rules
          set_fact: audit_file="/etc/audit/rules.d/delete.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - renameat
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of renameat in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83756-7
        - 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.2.7
        - audit_rules_file_deletion_events_renameat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
#### Rule Ensure auditd Collects File Deletion Events by User - rmdir [ref]
At a minimum, the audit system should collect file deletion events 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`, setting ARCH to either b32 or b64 as appropriate for your system:
    -a always,exit -F arch=ARCH -S rmdir -F auid>=1000 -F auid!=unset -F key=delete
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, setting ARCH to either b32 or b64 as appropriate for your system:
    -a always,exit -F arch=ARCH -S rmdir -F auid>=1000 -F auid!=unset -F key=delete
Rationale: | Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_file_deletion_events_rmdir
Identifiers and References | Identifiers: CCE-83758-3 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 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, 3.1.7, CCI-000169, CCI-000172, CCI-000366, CCI-002884, 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), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.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, 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, A.11.2.4, 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.1.1, 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, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.MA-2, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.7, 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-000466-GPOS-00210, SRG-OS-000467-GPOS-00211, SRG-OS-000468-GPOS-00212, SRG-OS-000466-VMM-001870, SRG-OS-000468-VMM-001890
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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="rmdir"
    	KEY="delete"
    	SYSCALL_GROUPING="unlink unlinkat rename renameat rmdir"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83758-3
        - 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.2.7
        - audit_rules_file_deletion_events_rmdir
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit rmdir tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83758-3
        - 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.2.7
        - audit_rules_file_deletion_events_rmdir
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for rmdir for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - rmdir
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of rmdir in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/delete.rules
          set_fact: audit_file="/etc/audit/rules.d/delete.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - rmdir
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of rmdir in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83758-3
        - 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.2.7
        - audit_rules_file_deletion_events_rmdir
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for rmdir for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - rmdir
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of rmdir in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/delete.rules
          set_fact: audit_file="/etc/audit/rules.d/delete.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - rmdir
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of rmdir in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83758-3
        - 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.2.7
        - audit_rules_file_deletion_events_rmdir
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
#### Rule Ensure auditd Collects File Deletion Events by User - unlink [ref]
At a minimum, the audit system should collect file deletion events 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`, setting ARCH to either b32 or b64 as appropriate for your system:
    -a always,exit -F arch=ARCH -S unlink -F auid>=1000 -F auid!=unset -F key=delete
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, setting ARCH to either b32 or b64 as appropriate for your system:
    -a always,exit -F arch=ARCH -S unlink -F auid>=1000 -F auid!=unset -F key=delete
Rationale: | Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_file_deletion_events_unlink
Identifiers and References | Identifiers: CCE-83757-5 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 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, 3.1.7, CCI-000169, CCI-000172, CCI-000366, CCI-002884, 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), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.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, 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, A.11.2.4, 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.1.1, 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, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.MA-2, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.7, 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-000466-GPOS-00210, SRG-OS-000467-GPOS-00211, SRG-OS-000468-GPOS-00212, SRG-OS-000466-VMM-001870, SRG-OS-000468-VMM-001890
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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="unlink"
    	KEY="delete"
    	SYSCALL_GROUPING="unlink unlinkat rename renameat rmdir"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83757-5
        - 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.2.7
        - audit_rules_file_deletion_events_unlink
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit unlink tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83757-5
        - 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.2.7
        - audit_rules_file_deletion_events_unlink
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for unlink for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - unlink
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of unlink in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/delete.rules
          set_fact: audit_file="/etc/audit/rules.d/delete.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - unlink
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of unlink in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83757-5
        - 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.2.7
        - audit_rules_file_deletion_events_unlink
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for unlink for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - unlink
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of unlink in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/delete.rules
          set_fact: audit_file="/etc/audit/rules.d/delete.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - unlink
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of unlink in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83757-5
        - 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.2.7
        - audit_rules_file_deletion_events_unlink
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
#### Rule Ensure auditd Collects File Deletion Events by User - unlinkat [ref]
At a minimum, the audit system should collect file deletion events 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`, setting ARCH to either b32 or b64 as appropriate for your system:
    -a always,exit -F arch=ARCH -S unlinkat -F auid>=1000 -F auid!=unset -F key=delete
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, setting ARCH to either b32 or b64 as appropriate for your system:
    -a always,exit -F arch=ARCH -S unlinkat -F auid>=1000 -F auid!=unset -F key=delete
Rationale: | Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_file_deletion_events_unlinkat
Identifiers and References | Identifiers: CCE-83755-9 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 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, 3.1.7, CCI-000169, CCI-000172, CCI-000366, CCI-002884, 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), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.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, 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, A.11.2.4, 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.1.1, 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, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.MA-2, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.7, 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-000466-GPOS-00210, SRG-OS-000467-GPOS-00211, SRG-OS-000468-GPOS-00212, SRG-OS-000466-VMM-001870, SRG-OS-000468-VMM-001890
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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="unlinkat"
    	KEY="delete"
    	SYSCALL_GROUPING="unlink unlinkat rename renameat rmdir"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83755-9
        - 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.2.7
        - audit_rules_file_deletion_events_unlinkat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit unlinkat tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83755-9
        - 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.2.7
        - audit_rules_file_deletion_events_unlinkat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for unlinkat for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - unlinkat
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of unlinkat in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/delete.rules
          set_fact: audit_file="/etc/audit/rules.d/delete.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - unlinkat
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of unlinkat in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83755-9
        - 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.2.7
        - audit_rules_file_deletion_events_unlinkat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for unlinkat for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - unlinkat
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of unlinkat in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/delete.rules
          set_fact: audit_file="/etc/audit/rules.d/delete.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - unlinkat
            syscall_grouping:
              - unlink
              - unlinkat
              - rename
              - renameat
              - rmdir

        - name: Check existence of unlinkat in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
              -F auid!=unset -F key=delete
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83755-9
        - 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.2.7
        - audit_rules_file_deletion_events_unlinkat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
Group Record Unauthorized Access Attempts Events to Files (unsuccessful) Group contains 6 rules
[ref] At a minimum, the audit system should collect unauthorized file accesses 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 creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
        -a always,exit -F arch=b32 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
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 creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
        -a always,exit -F arch=b64 -S creat,open,openat,open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
#### Rule Record Unsuccessful Access Attempts to Files - creat [ref]
At a minimum, the audit system should collect unauthorized file accesses 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`:
    -a always,exit -F arch=b32 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b32 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
    -a always,exit -F arch=b64 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b64 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
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:
    -a always,exit -F arch=b32 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b32 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
    -a always,exit -F arch=b64 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b64 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
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: | Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_unsuccessful_file_modification_creat
Identifiers and References | Identifiers: CCE-83786-4 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 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, 3.1.7, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.4, Req-10.2.1, 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-000458-GPOS-00203, SRG-OS-000461-GPOS-00205, SRG-OS-000458-VMM-001810, SRG-OS-000461-VMM-001830
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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")

    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL="creat"
    KEY="access"
    SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS="-F exit=-EACCES"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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="-F exit=-EPERM"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83786-4
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_creat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit creat tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83786-4
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_creat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for creat EACCES for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - creat
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of creat in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - creat
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of creat in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83786-4
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_creat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for creat EACCES for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - creat
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of creat in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - creat
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of creat in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83786-4
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_creat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for creat EPERM for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - creat
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of creat in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - creat
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of creat in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83786-4
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_creat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for creat EPERM for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - creat
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of creat in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - creat
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of creat in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83786-4
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_creat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
#### Rule Record Unsuccessful Access Attempts to Files - ftruncate [ref]
At a minimum, the audit system should collect unauthorized file accesses 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`:
    -a always,exit -F arch=b32 -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b32 -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
    -a always,exit -F arch=b64 -S ftruncate -F exiu=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b64 -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
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:
    -a always,exit -F arch=b32 -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b32 -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
    -a always,exit -F arch=b64 -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b64 -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
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: | Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_unsuccessful_file_modification_ftruncate
Identifiers and References | Identifiers: CCE-83800-3 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 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, 3.1.7, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.4, Req-10.2.1, 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-000458-GPOS-00203, SRG-OS-000461-GPOS-00205, SRG-OS-000458-VMM-001810, SRG-OS-000461-VMM-001830
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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")

    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL="ftruncate"
    KEY="access"
    SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS="-F exit=-EACCES"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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="-F exit=-EPERM"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83800-3
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_ftruncate
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit ftruncate tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83800-3
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_ftruncate
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for ftruncate EACCES for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - ftruncate
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of ftruncate in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - ftruncate
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of ftruncate in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83800-3
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_ftruncate
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for ftruncate EACCES for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - ftruncate
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of ftruncate in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - ftruncate
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of ftruncate in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83800-3
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_ftruncate
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for ftruncate EPERM for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - ftruncate
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of ftruncate in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - ftruncate
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of ftruncate in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83800-3
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_ftruncate
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for ftruncate EPERM for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - ftruncate
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of ftruncate in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - ftruncate
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of ftruncate in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83800-3
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_ftruncate
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
#### Rule Record Unsuccessful Access Attempts to Files - open [ref]
At a minimum, the audit system should collect unauthorized file accesses 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`:
    -a always,exit -F arch=b32 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b32 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
    -a always,exit -F arch=b64 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b64 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
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:
    -a always,exit -F arch=b32 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b32 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
    -a always,exit -F arch=b64 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b64 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
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: | Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_unsuccessful_file_modification_open
Identifiers and References | Identifiers: CCE-83801-1 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 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, 3.1.7, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.4, Req-10.2.1, 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-000458-GPOS-00203, SRG-OS-000461-GPOS-00205, SRG-OS-000458-VMM-001810, SRG-OS-000461-VMM-001830
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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")

    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL="open"
    KEY="access"
    SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS="-F exit=-EACCES"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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="-F exit=-EPERM"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83801-1
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_open
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit open tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83801-1
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_open
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for open EACCES for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - open
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of open in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - open
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of open in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83801-1
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_open
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for open EACCES for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - open
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of open in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - open
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of open in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83801-1
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_open
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for open EPERM for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - open
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of open in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - open
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of open in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83801-1
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_open
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for open EPERM for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - open
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of open in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - open
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of open in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83801-1
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_open
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
#### Rule Record Unsuccessful Access Attempts to Files - open_by_handle_at [ref]
At a minimum, the audit system should collect unauthorized file accesses 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`:
    -a always,exit -F arch=b32 -S open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b32 -S open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
    -a always,exit -F arch=b64 -S open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b64 -S open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
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:
    -a always,exit -F arch=b32 -S open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b32 -S open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
    -a always,exit -F arch=b64 -S open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b64 -S open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
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: | Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_unsuccessful_file_modification_open_by_handle_at
Identifiers and References | Identifiers: CCE-83796-3 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 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, 3.1.7, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.4, Req-10.2.1, 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-000458-GPOS-00203, SRG-OS-000461-GPOS-00205, SRG-OS-000458-VMM-001810, SRG-OS-000461-VMM-001830
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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")

    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL="open_by_handle_at"
    KEY="access"
    SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS="-F exit=-EACCES"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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="-F exit=-EPERM"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83796-3
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_open_by_handle_at
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit open_by_handle_at tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83796-3
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_open_by_handle_at
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for open_by_handle_at EACCES for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - open_by_handle_at
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of open_by_handle_at in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - open_by_handle_at
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of open_by_handle_at in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83796-3
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_open_by_handle_at
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for open_by_handle_at EACCES for x86_64
        platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - open_by_handle_at
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of open_by_handle_at in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - open_by_handle_at
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of open_by_handle_at in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83796-3
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_open_by_handle_at
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for open_by_handle_at EPERM for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - open_by_handle_at
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of open_by_handle_at in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - open_by_handle_at
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of open_by_handle_at in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83796-3
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_open_by_handle_at
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for open_by_handle_at EPERM for x86_64
        platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - open_by_handle_at
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of open_by_handle_at in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - open_by_handle_at
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of open_by_handle_at in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83796-3
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_open_by_handle_at
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
#### Rule Record Unsuccessful Access Attempts to Files - openat [ref]
At a minimum, the audit system should collect unauthorized file accesses 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`:
    -a always,exit -F arch=b32 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b32 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
    -a always,exit -F arch=b64 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b64 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
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:
    -a always,exit -F arch=b32 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b32 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
    -a always,exit -F arch=b64 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b64 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
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: | Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_unsuccessful_file_modification_openat
Identifiers and References | Identifiers: CCE-83794-8 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 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, 3.1.7, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.4, Req-10.2.1, 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-000458-GPOS-00203, SRG-OS-000461-GPOS-00205, SRG-OS-000458-VMM-001810, SRG-OS-000461-VMM-001830
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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")

    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL="openat"
    KEY="access"
    SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS="-F exit=-EACCES"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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="-F exit=-EPERM"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83794-8
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_openat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit openat tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83794-8
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_openat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for openat EACCES for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - openat
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of openat in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - openat
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of openat in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83794-8
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_openat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for openat EACCES for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - openat
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of openat in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - openat
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of openat in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83794-8
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_openat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for openat EPERM for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - openat
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of openat in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - openat
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of openat in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83794-8
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_openat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for openat EPERM for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - openat
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of openat in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - openat
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of openat in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83794-8
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_openat
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
#### Rule Record Unsuccessful Access Attempts to Files - truncate [ref]
At a minimum, the audit system should collect unauthorized file accesses 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`:
    -a always,exit -F arch=b32 -S truncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b32 -S truncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
    -a always,exit -F arch=b64 -S truncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b64 -S truncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
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:
    -a always,exit -F arch=b32 -S truncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b32 -S truncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
    -a always,exit -F arch=b64 -S truncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
    -a always,exit -F arch=b64 -S truncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
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: | Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_unsuccessful_file_modification_truncate
Identifiers and References | Identifiers: CCE-83792-2 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 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, 3.1.7, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.4, Req-10.2.1, 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-000458-GPOS-00203, SRG-OS-000461-GPOS-00205, SRG-OS-000458-VMM-001810, SRG-OS-000461-VMM-001830
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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")

    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL="truncate"
    KEY="access"
    SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

    for ARCH in "${RULE_ARCHS[@]}"
    do
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS="-F exit=-EACCES"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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="-F exit=-EPERM"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83792-2
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_truncate
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit truncate tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83792-2
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_truncate
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for truncate EACCES for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - truncate
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of truncate in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - truncate
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of truncate in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83792-2
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_truncate
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for truncate EACCES for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - truncate
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of truncate in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - truncate
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of truncate in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EACCES -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83792-2
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_truncate
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for truncate EPERM for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - truncate
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of truncate in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - truncate
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of truncate in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83792-2
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_truncate
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for truncate EPERM for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - truncate
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of truncate in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/access.rules
          set_fact: audit_file="/etc/audit/rules.d/access.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - truncate
            syscall_grouping:
              - creat
              - ftruncate
              - truncate
              - open
              - openat
              - open_by_handle_at

        - name: Check existence of truncate in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F exit=-EPERM -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
              (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
              -F auid>=1000 -F auid!=unset -F key=access
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83792-2
        - 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.2.1
        - PCI-DSS-Req-10.2.4
        - audit_rules_unsuccessful_file_modification_truncate
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
Group Record Information on Kernel Modules Loading and Unloading Group contains 2 rules
[ref] To capture kernel module loading and unloading events, use following lines, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit:
    -a always,exit -F arch=ARCH -S init_module,delete_module -F key=modules
Place to add the lines depends on a way `auditd` daemon is configured. If it is configured to use the `augenrules` program (the default), add the lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`. If the `auditd` daemon is configured to use the `auditctl` utility, add the lines to file `/etc/audit/audit.rules`.
#### Rule Ensure auditd Collects Information on Kernel Module Unloading - delete_module [ref]
To capture kernel module unloading events, use following line, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit:
    -a always,exit -F arch=ARCH -S delete_module -F key=modules
Place to add the line depends on a way `auditd` daemon is configured. If it is configured to use the `augenrules` program (the default), add the line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`. If the `auditd` daemon is configured to use the `auditctl` utility, add the line to file `/etc/audit/audit.rules`.
Rationale: | The removal of kernel modules can be used to alter the behavior of the kernel and potentially introduce malicious code into kernel space. It is important to have an audit trail of modules that have been introduced into the kernel.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_kernel_module_loading_delete
Identifiers and References | Identifiers: CCE-83802-9 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 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, 3.1.7, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.7, 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-000471-GPOS-00216, SRG-OS-000477-GPOS-00222, SRG-OS-000477-VMM-001970
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    # First perform the remediation of the syscall rule
    # Retrieve hardware architecture of the underlying system
    # Note: 32-bit and 64-bit kernel syscall numbers not always line up =>
    #       it's required on a 64-bit system to check also for the presence
    #       of 32-bit's equivalent of the corresponding rule.
    #       (See `man 7 audit.rules` for details )
    [ "$(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=""
    	SYSCALL="delete_module"
    	KEY="modules"
    	SYSCALL_GROUPING="delete_module"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83802-9
        - 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.7
        - audit_rules_kernel_module_loading_delete
        - configure_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed

    - name: Set architecture for audit delete_module tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83802-9
        - 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.7
        - audit_rules_kernel_module_loading_delete
        - configure_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed

    - name: Perform remediation of Audit rules for delete_module for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - delete_module
            syscall_grouping: []

        - name: Check existence of delete_module in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
            patterns: '*.rules'
          register: find_command
          loop: '{{ (syscall_grouping + syscalls) | unique }}'

        - name: Reset syscalls found per file
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/module-change.rules
          set_fact: audit_file="/etc/audit/rules.d/module-change.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=module-change
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - delete_module
            syscall_grouping: []

        - name: Check existence of delete_module in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=module-change
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83802-9
        - 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.7
        - audit_rules_kernel_module_loading_delete
        - configure_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed

    - name: Perform remediation of Audit rules for delete_module for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - delete_module
            syscall_grouping: []

        - name: Check existence of delete_module in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
            patterns: '*.rules'
          register: find_command
          loop: '{{ (syscall_grouping + syscalls) | unique }}'

        - name: Reset syscalls found per file
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/module-change.rules
          set_fact: audit_file="/etc/audit/rules.d/module-change.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=module-change
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - delete_module
            syscall_grouping: []

        - name: Check existence of delete_module in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=module-change
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83802-9
        - 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.7
        - audit_rules_kernel_module_loading_delete
        - configure_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20delete_module%20-k%20module-change%0A-a%20always%2Cexit%20-F%20arch%3Db64%20-S%20delete_module%20-k%20module-change%0A
            mode: 0600
            path: /etc/audit/rules.d/75-kernel-module-loading-delete.rules
            overwrite: true
#### Rule Ensure auditd Collects Information on Kernel Module Loading - init_module [ref]
To capture kernel module loading events, use following line, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit:
    -a always,exit -F arch=ARCH -S init_module -F key=modules
Place to add the line depends on a way `auditd` daemon is configured. If it is configured to use the `augenrules` program (the default), add the line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`. If the `auditd` daemon is configured to use the `auditctl` utility, add the line to file `/etc/audit/audit.rules`.
Rationale: | The addition of kernel modules can be used to alter the behavior of the kernel and potentially introduce malicious code into kernel space. It is important to have an audit trail of modules that have been introduced into the kernel.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_kernel_module_loading_init
Identifiers and References | Identifiers: CCE-90835-0 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 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, 3.1.7, CCI-000130, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.7, 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-000471-GPOS-00216, SRG-OS-000477-GPOS-00222, SRG-OS-000477-VMM-001970
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    # First perform the remediation of the syscall rule
    # Retrieve hardware architecture of the underlying system
    # Note: 32-bit and 64-bit kernel syscall numbers not always line up =>
    #       it's required on a 64-bit system to check also for the presence
    #       of 32-bit's equivalent of the corresponding rule.
    #       (See `man 7 audit.rules` for details )
    [ "$(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=""
    	SYSCALL="init_module"
    	KEY="modules"
    	SYSCALL_GROUPING="init_module finit_module"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-90835-0
        - 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.7
        - audit_rules_kernel_module_loading_init
        - configure_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed

    - name: Set architecture for audit init_module tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-90835-0
        - 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.7
        - audit_rules_kernel_module_loading_init
        - configure_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed

    - name: Perform remediation of Audit rules for init_module for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - init_module
            syscall_grouping:
              - init_module
              - finit_module

        - name: Check existence of init_module in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
            patterns: '*.rules'
          register: find_command
          loop: '{{ (syscall_grouping + syscalls) | unique }}'

        - name: Reset syscalls found per file
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/module-change.rules
          set_fact: audit_file="/etc/audit/rules.d/module-change.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=module-change
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - init_module
            syscall_grouping:
              - init_module
              - finit_module

        - name: Check existence of init_module in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=module-change
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-90835-0
        - 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.7
        - audit_rules_kernel_module_loading_init
        - configure_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed

    - name: Perform remediation of Audit rules for init_module for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - init_module
            syscall_grouping:
              - init_module
              - finit_module

        - name: Check existence of init_module in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
            patterns: '*.rules'
          register: find_command
          loop: '{{ (syscall_grouping + syscalls) | unique }}'

        - name: Reset syscalls found per file
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/module-change.rules
          set_fact: audit_file="/etc/audit/rules.d/module-change.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=module-change
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - init_module
            syscall_grouping:
              - init_module
              - finit_module

        - name: Check existence of init_module in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=module-change
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-90835-0
        - 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.7
        - audit_rules_kernel_module_loading_init
        - configure_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20init_module%20-k%20module-change%0A-a%20always%2Cexit%20-F%20arch%3Db64%20-S%20init_module%20-k%20module-change%0A
            mode: 0600
            path: /etc/audit/rules.d/75-kernel-module-loading-init.rules
            overwrite: true
Group Record Attempts to Alter Logon and Logout Events Group contains 3 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/run/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/run/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` in order to watch for attempted manual edits of files involved in storing logon events:
    -w /var/run/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` file in order to watch for unattempted manual edits of files involved in storing logon events:
    -w /var/run/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
Identifiers and References | Identifiers: CCE-83783-1 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 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, 3.1.7, CCI-000126, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.3, SRG-OS-000392-GPOS-00172, SRG-OS-000470-GPOS-00214, SRG-OS-000473-GPOS-00218, SRG-OS-000473-VMM-001930, SRG-OS-000470-VMM-001900
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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/faillock" "$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/faillock $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/faillock$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/faillock -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/run/faillock" /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 0640 "$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/faillock" "$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/faillock $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/faillock$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/faillock -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: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83783-1
        - 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
        - audit_rules_login_events_faillock
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other user/group modification audit rules
      find:
        paths: /etc/audit/rules.d
        recurse: false
        contains: -k logins$
        patterns: '*.rules'
      register: find_faillock
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83783-1
        - 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
        - audit_rules_login_events_faillock
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: If existing user/group modification ruleset not found, use /etc/audit/rules.d/logins.rules
        as the recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/logins.rules
      when:
        - '"audit" in ansible_facts.packages'
        - find_faillock.matched is defined and find_faillock.matched == 0
      tags:
        - CCE-83783-1
        - 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
        - audit_rules_login_events_faillock
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_faillock.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" in ansible_facts.packages'
        - find_faillock.matched is defined and find_faillock.matched > 0
      tags:
        - CCE-83783-1
        - 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
        - audit_rules_login_events_faillock
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Inserts/replaces the faillock rule in rules.d when on x86
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /var/run/faillock -p wa -k logins
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83783-1
        - 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
        - audit_rules_login_events_faillock
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Inserts/replaces the faillock rule in /etc/audit/audit.rules
      lineinfile:
        line: -w /var/run/faillock -p wa -k logins
        state: present
        dest: /etc/audit/audit.rules
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83783-1
        - 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
        - audit_rules_login_events_faillock
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - 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` in order to watch for attempted manual edits of files involved in storing logon events:
    -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/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
Identifiers and References | Identifiers: CCE-83785-6 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 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, 3.1.7, CCI-000126, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.3, 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, SRG-OS-000473-VMM-001930, SRG-OS-000470-VMM-001900
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83785-6
        - 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
        - audit_rules_login_events_lastlog
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other user/group modification audit rules
      find:
        paths: /etc/audit/rules.d
        recurse: false
        contains: -k logins$
        patterns: '*.rules'
      register: find_lastlog
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83785-6
        - 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
        - audit_rules_login_events_lastlog
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: If existing user/group modification ruleset not found, use /etc/audit/rules.d/logins.rules
        as the recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/logins.rules
      when:
        - '"audit" in ansible_facts.packages'
        - find_lastlog.matched is defined and find_lastlog.matched == 0
      tags:
        - CCE-83785-6
        - 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
        - audit_rules_login_events_lastlog
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_lastlog.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" in ansible_facts.packages'
        - find_lastlog.matched is defined and find_lastlog.matched > 0
      tags:
        - CCE-83785-6
        - 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
        - audit_rules_login_events_lastlog
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Inserts/replaces the lastlog rule in rules.d when on x86
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /var/log/lastlog -p wa -k logins
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83785-6
        - 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
        - audit_rules_login_events_lastlog
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Inserts/replaces the lastlog rule in /etc/audit/audit.rules
      lineinfile:
        line: -w /var/log/lastlog -p wa -k logins
        state: present
        dest: /etc/audit/audit.rules
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83785-6
        - 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
        - audit_rules_login_events_lastlog
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
#### Rule Record Attempts to Alter Logon and Logout Events - tallylog [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
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
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_tallylog
Identifiers and References | Identifiers: CCE-83782-3 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 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, 3.1.7, CCI-000172, CCI-002884, CCI-000126, 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), 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, 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, 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, AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.3, SRG-OS-000392-GPOS-00172, SRG-OS-000470-GPOS-00214, SRG-OS-000473-GPOS-00218, SRG-OS-000473-VMM-001930, SRG-OS-000470-VMM-001900
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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/tallylog" "$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/tallylog $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/tallylog$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/tallylog -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/tallylog" /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 0640 "$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/tallylog" "$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/tallylog $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/tallylog$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/tallylog -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: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83782-3
        - 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
        - audit_rules_login_events_tallylog
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other user/group modification audit rules
      find:
        paths: /etc/audit/rules.d
        recurse: false
        contains: -k logins$
        patterns: '*.rules'
      register: find_tallylog
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83782-3
        - 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
        - audit_rules_login_events_tallylog
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: If existing user/group modification ruleset not found, use /etc/audit/rules.d/logins.rules
        as the recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/logins.rules
      when:
        - '"audit" in ansible_facts.packages'
        - find_tallylog.matched is defined and find_tallylog.matched == 0
      tags:
        - CCE-83782-3
        - 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
        - audit_rules_login_events_tallylog
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_tallylog.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" in ansible_facts.packages'
        - find_tallylog.matched is defined and find_tallylog.matched > 0
      tags:
        - CCE-83782-3
        - 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
        - audit_rules_login_events_tallylog
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Inserts/replaces the tallylog rule in rules.d when on x86
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /var/log/tallylog -p wa -k logins
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83782-3
        - 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
        - audit_rules_login_events_tallylog
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Inserts/replaces the tallylog rule in /etc/audit/audit.rules
      lineinfile:
        line: -w /var/log/tallylog -p wa -k logins
        state: present
        dest: /etc/audit/audit.rules
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83782-3
        - 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
        - audit_rules_login_events_tallylog
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
Group Record Information on the Use of Privileged Commands Group contains 16 rules
[ref] At a minimum, the audit system should collect the execution of privileged commands for all users and root.
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - chage [ref]
At a minimum, the audit system should collect the execution of privileged commands 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 a line of the following form to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F path=/usr/bin/chage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add a line of the following form to `/etc/audit/audit.rules`:
    -a always,exit -F path=/usr/bin/chage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_chage
Identifiers and References | Identifiers: CCE-83765-8 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, 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, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, 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-000468-GPOS-00212, SRG-OS-000471-GPOS-00215, SRG-OS-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/bin/chage -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83765-8
        - 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)
        - audit_rules_privileged_commands_chage
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/bin/chage
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/chage -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chage -F perm=x
              -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chage -F perm=x
              -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/chage -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chage -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chage -F perm=x
              -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83765-8
        - 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)
        - audit_rules_privileged_commands_chage
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - chsh [ref]
At a minimum, the audit system should collect the execution of privileged commands 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 a line of the following form to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F path=/usr/bin/chsh -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add a line of the following form to `/etc/audit/audit.rules`:
    -a always,exit -F path=/usr/bin/chsh -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_chsh
Identifiers and References | Identifiers: CCE-83763-3 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, 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, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, 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-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/bin/chsh -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83763-3
        - 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)
        - audit_rules_privileged_commands_chsh
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/bin/chsh
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/chsh -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chsh -F perm=x -F
              auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chsh -F perm=x
              -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/chsh -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chsh -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chsh -F perm=x
              -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83763-3
        - 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)
        - audit_rules_privileged_commands_chsh
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - crontab [ref]
At a minimum, the audit system should collect the execution of privileged commands 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 a line of the following form to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F path=/usr/bin/crontab -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add a line of the following form to `/etc/audit/audit.rules`:
    -a always,exit -F path=/usr/bin/crontab -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_crontab
Identifiers and References | Identifiers: CCE-83761-7 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, 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-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/bin/crontab -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83761-7
        - 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)
        - audit_rules_privileged_commands_crontab
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/bin/crontab
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/crontab -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/crontab -F perm=x
              -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/crontab -F
              perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/crontab -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/crontab -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/crontab -F
              perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83761-7
        - 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)
        - audit_rules_privileged_commands_crontab
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - gpasswd [ref]
At a minimum, the audit system should collect the execution of privileged commands 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 a line of the following form to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F path=/usr/bin/gpasswd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add a line of the following form to `/etc/audit/audit.rules`:
    -a always,exit -F path=/usr/bin/gpasswd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_gpasswd
Identifiers and References | Identifiers: CCE-83773-2 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, 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, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, 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-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/bin/gpasswd -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83773-2
        - 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)
        - audit_rules_privileged_commands_gpasswd
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/bin/gpasswd
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/gpasswd -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/gpasswd -F perm=x
              -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/gpasswd -F
              perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/gpasswd -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/gpasswd -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/gpasswd -F
              perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83773-2
        - 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)
        - audit_rules_privileged_commands_gpasswd
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - newgrp [ref]
At a minimum, the audit system should collect the execution of privileged commands 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 a line of the following form to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F path=/usr/bin/newgrp -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add a line of the following form to `/etc/audit/audit.rules`:
    -a always,exit -F path=/usr/bin/newgrp -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_newgrp
Identifiers and References | Identifiers: CCE-83766-6 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000169, CCI-000135, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, 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, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, 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-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/bin/newgrp -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83766-6
        - 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)
        - audit_rules_privileged_commands_newgrp
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/bin/newgrp
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/newgrp -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/newgrp -F perm=x
              -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/newgrp -F
              perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/newgrp -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/newgrp -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/newgrp -F
              perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83766-6
        - 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)
        - audit_rules_privileged_commands_newgrp
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - pam_timestamp_check [ref]
At a minimum, the audit system should collect the execution of privileged commands 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 a line of the following form to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F path=/usr/sbin/pam_timestamp_check
    -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add a line of the following form to `/etc/audit/audit.rules`:
    -a always,exit -F path=/usr/sbin/pam_timestamp_check
    -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_pam_timestamp_check
Identifiers and References | Identifiers: CCE-83767-4 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, 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-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/sbin/pam_timestamp_check -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83767-4
        - 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)
        - audit_rules_privileged_commands_pam_timestamp_check
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/sbin/pam_timestamp_check
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/sbin/pam_timestamp_check -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/pam_timestamp_check
              -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/pam_timestamp_check
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/sbin/pam_timestamp_check -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/pam_timestamp_check -F perm=x
              -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/pam_timestamp_check
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83767-4
        - 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)
        - audit_rules_privileged_commands_pam_timestamp_check
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - passwd [ref]
At a minimum, the audit system should collect the execution of privileged commands 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 a line of the following form to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F path=/usr/bin/passwd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add a line of the following form to `/etc/audit/audit.rules`:
    -a always,exit -F path=/usr/bin/passwd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_passwd
Identifiers and References | Identifiers: CCE-83781-5 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, 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, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, 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-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/bin/passwd -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83781-5
        - 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)
        - audit_rules_privileged_commands_passwd
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/bin/passwd
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/passwd -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/passwd -F perm=x
              -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/passwd -F
              perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/passwd -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/passwd -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/passwd -F
              perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83781-5
        - 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)
        - audit_rules_privileged_commands_passwd
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - postdrop [ref]
At a minimum, the audit system should collect the execution of privileged commands 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 a line of the following form to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F path=/usr/sbin/postdrop -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add a line of the following form to `/etc/audit/audit.rules`:
    -a always,exit -F path=/usr/sbin/postdrop -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_postdrop
Identifiers and References | Identifiers: CCE-83769-0 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, 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-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/sbin/postdrop -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83769-0
        - 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)
        - audit_rules_privileged_commands_postdrop
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/sbin/postdrop
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/sbin/postdrop -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/postdrop -F perm=x
              -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/postdrop
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/sbin/postdrop -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/postdrop -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/postdrop
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83769-0
        - 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)
        - audit_rules_privileged_commands_postdrop
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - postqueue [ref]
At a minimum, the audit system should collect the execution of privileged commands 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 a line of the following form to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F path=/usr/sbin/postqueue -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add a line of the following form to `/etc/audit/audit.rules`:
    -a always,exit -F path=/usr/sbin/postqueue -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_postqueue
Identifiers and References | Identifiers: CCE-83770-8 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, 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-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/sbin/postqueue -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83770-8
        - 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)
        - audit_rules_privileged_commands_postqueue
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/sbin/postqueue
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/sbin/postqueue -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/postqueue -F perm=x
              -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/postqueue
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/sbin/postqueue -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/postqueue -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/postqueue
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83770-8
        - 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)
        - audit_rules_privileged_commands_postqueue
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - ssh-keysign [ref]
At a minimum, the audit system should collect the execution of privileged commands 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 a line of the following form to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F path=/usr/libexec/openssh/ssh-keysign -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add a line of the following form to `/etc/audit/audit.rules`:
    -a always,exit -F path=/usr/libexec/openssh/ssh-keysign -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_ssh_keysign
Identifiers and References | Identifiers: CCE-83776-5 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, 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-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/libexec/openssh/ssh-keysign -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83776-5
        - 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)
        - audit_rules_privileged_commands_ssh_keysign
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/libexec/openssh/ssh-keysign
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/libexec/openssh/ssh-keysign -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/libexec/openssh/ssh-keysign
              -F perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/libexec/openssh/ssh-keysign
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/libexec/openssh/ssh-keysign -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/libexec/openssh/ssh-keysign -F perm=x
              -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/libexec/openssh/ssh-keysign
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83776-5
        - 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)
        - audit_rules_privileged_commands_ssh_keysign
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - su [ref]
At a minimum, the audit system should collect the execution of privileged commands 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 a line of the following form to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F path=/usr/bin/su -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add a line of the following form to `/etc/audit/audit.rules`:
    -a always,exit -F path=/usr/bin/su -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_su
Identifiers and References | Identifiers: CCE-83771-6 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000064-GPOS-0003, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000466-GPOS-00210, SRG-OS-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/bin/su -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83771-6
        - 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)
        - audit_rules_privileged_commands_su
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/bin/su
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/su -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/su -F perm=x -F
              auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/su -F perm=x
              -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/su -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/su -F perm=x -F auid>=1000 -F
              auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/su -F perm=x
              -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83771-6
        - 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)
        - audit_rules_privileged_commands_su
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - sudo [ref]
At a minimum, the audit system should collect the execution of privileged commands 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 a line of the following form to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F path=/usr/bin/sudo -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add a line of the following form to `/etc/audit/audit.rules`:
    -a always,exit -F path=/usr/bin/sudo -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_sudo
Identifiers and References | Identifiers: CCE-83780-7 References: BP28(R19), 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, 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-000466-GPOS-00210, SRG-OS-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/bin/sudo -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83780-7
        - 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)
        - audit_rules_privileged_commands_sudo
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/bin/sudo
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/sudo -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/sudo -F perm=x -F
              auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/sudo -F perm=x
              -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/sudo -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/sudo -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/sudo -F perm=x
              -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83780-7
        - 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)
        - audit_rules_privileged_commands_sudo
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - sudoedit [ref]
At a minimum, the audit system should collect the execution of privileged commands 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 a line of the following form to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F path=/usr/bin/sudoedit -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add a line of the following form to `/etc/audit/audit.rules`:
    -a always,exit -F path=/usr/bin/sudoedit -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_sudoedit
Identifiers and References | Identifiers: CCE-83764-1 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, 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-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/bin/sudoedit -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83764-1
        - 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)
        - audit_rules_privileged_commands_sudoedit
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/bin/sudoedit
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/sudoedit -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/sudoedit -F perm=x
              -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/sudoedit -F
              perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/sudoedit -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/sudoedit -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/sudoedit -F
              perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83764-1
        - 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)
        - audit_rules_privileged_commands_sudoedit
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - umount [ref]
At a minimum, the audit system should collect the execution of privileged commands 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 a line of the following form to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F path=/usr/bin/umount -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add a line of the following form to `/etc/audit/audit.rules`:
    -a always,exit -F path=/usr/bin/umount -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_umount
Identifiers and References | Identifiers: CCE-83762-5 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000135, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000392-GPOS-00172, SRG-OS-000471-GPOS-00215, SRG-OS-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/bin/umount -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83762-5
        - 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)
        - audit_rules_privileged_commands_umount
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/bin/umount
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/umount -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/umount -F perm=x
              -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/umount -F
              perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/bin/umount -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/umount -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/umount -F
              perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83762-5
        - 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)
        - audit_rules_privileged_commands_umount
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - unix_chkpwd [ref]
At a minimum, the audit system should collect the execution of privileged commands 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 a line of the following form to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F path=/usr/sbin/unix_chkpwd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add a line of the following form to `/etc/audit/audit.rules`:
    -a always,exit -F path=/usr/sbin/unix_chkpwd -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_unix_chkpwd
Identifiers and References | Identifiers: CCE-83768-2 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, 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, CIP-007-3 R6.5, AC-2(4), AU-2(d), AU-3, AU-3.1, AU-12(a), AU-12(c), AU-12.1(ii), AU-12.1(iv), AC-6(9), CM-6(a), MA-4(1)(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, 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-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/sbin/unix_chkpwd -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83768-2
        - NIST-800-171-3.1.7
        - NIST-800-53-AC-2(4)
        - NIST-800-53-AC-6(9)
        - NIST-800-53-AU-12(a)
        - NIST-800-53-AU-12(c)
        - NIST-800-53-AU-12.1(ii)
        - NIST-800-53-AU-12.1(iv)
        - NIST-800-53-AU-2(d)
        - NIST-800-53-AU-3
        - NIST-800-53-AU-3.1
        - NIST-800-53-CM-6(a)
        - NIST-800-53-MA-4(1)(a)
        - audit_rules_privileged_commands_unix_chkpwd
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/sbin/unix_chkpwd
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/sbin/unix_chkpwd -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/unix_chkpwd -F
              perm=x -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/unix_chkpwd
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/sbin/unix_chkpwd -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/unix_chkpwd -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/unix_chkpwd
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83768-2
        - NIST-800-171-3.1.7
        - NIST-800-53-AC-2(4)
        - NIST-800-53-AC-6(9)
        - NIST-800-53-AU-12(a)
        - NIST-800-53-AU-12(c)
        - NIST-800-53-AU-12.1(ii)
        - NIST-800-53-AU-12.1(iv)
        - NIST-800-53-AU-2(d)
        - NIST-800-53-AU-3
        - NIST-800-53-AU-3.1
        - NIST-800-53-CM-6(a)
        - NIST-800-53-MA-4(1)(a)
        - audit_rules_privileged_commands_unix_chkpwd
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - userhelper [ref]
At a minimum, the audit system should collect the execution of privileged commands 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 a line of the following form to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:
    -a always,exit -F path=/usr/sbin/userhelper -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add a line of the following form to `/etc/audit/audit.rules`:
    -a always,exit -F path=/usr/sbin/userhelper -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
Rationale: | Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats. Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_privileged_commands_userhelper
Identifiers and References | Identifiers: CCE-83760-9 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, 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-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/sbin/userhelper -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # 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 0640 "$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 o-rwx ${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 o-rwx ${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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83760-9
        - 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)
        - audit_rules_privileged_commands_userhelper
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/sbin/userhelper
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/sbin/userhelper -F perm=x -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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/privileged.rules
          set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/userhelper -F perm=x
              -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/userhelper
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls: []
            syscall_grouping: []

        - name: Check existence of  in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
              -F path=/usr/sbin/userhelper -F perm=x -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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
              }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/userhelper -F perm=x -F auid>=1000
              -F auid!=unset (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/userhelper
              -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83760-9
        - 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)
        - audit_rules_privileged_commands_userhelper
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
Group Records Events that Modify Date and Time Information Group contains 5 rules
[ref] Arbitrary changes to the system time can be used to obfuscate nefarious activities in log files, as well as to confuse network services that are highly dependent upon an accurate system time. All changes to the system time should be audited.
#### Rule Record attempts to alter time through adjtimex [ref]
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 adjtimex -F key=audit_time_rules
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S adjtimex -F key=audit_time_rules
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 adjtimex -F key=audit_time_rules
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S adjtimex -F key=audit_time_rules
The -k option allows for the specification of a key in string form that can be used for better reporting capability through ausearch and aureport. Multiple system calls can be defined on the same line to save space if desired, but is not required. See an example of multiple combined syscalls:
    -a always,exit -F arch=b64 -S adjtimex,settimeofday -F key=audit_time_rules
Rationale: | Arbitrary changes to the system time can be used to obfuscate nefarious activities in log files, as well as to confuse network services that are highly dependent upon an accurate system time (such as sshd). All changes to the system time should be audited.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_time_adjtimex
Identifiers and References | Identifiers: CCE-83840-9 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-001487, CCI-000169, 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), 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, 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, 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, AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, Req-10.4.2.b
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    # Retrieve hardware architecture of the underlying system
    [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

    for ARCH in "${RULE_ARCHS[@]}"
    do
        # Create expected audit group and audit rule form for particular system call & architecture
        if [ ${ARCH} = "b32" ]
        then
            ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
            # stime system call is known at 32-bit arch (see e.g "$ ausyscall i386 stime" 's output)
            # so append it to the list of time group system calls to be audited
            SYSCALL="adjtimex settimeofday stime"
            SYSCALL_GROUPING="adjtimex settimeofday stime"
        elif [ ${ARCH} = "b64" ]
        then
            ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
            # stime system call isn't known at 64-bit arch (see "$ ausyscall x86_64 stime" 's output)
            # therefore don't add it to the list of time group system calls to be audited
            SYSCALL="adjtimex settimeofday"
            SYSCALL_GROUPING="adjtimex settimeofday"
        fi
        OTHER_FILTERS=""
        AUID_FILTERS=""
        KEY="audit_time_rules"
        # 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 0640 "$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 o-rwx ${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 o-rwx ${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
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83840-9
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_adjtimex
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Set architecture for audit tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83840-9
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_adjtimex
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for adjtimex for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - adjtimex
            syscall_grouping:
              - adjtimex
              - settimeofday
              - stime

        - name: Check existence of adjtimex in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
            patterns: '*.rules'
          register: find_command
          loop: '{{ (syscall_grouping + syscalls) | unique }}'

        - name: Reset syscalls found per file
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/audit_time_rules.rules
          set_fact: audit_file="/etc/audit/rules.d/audit_time_rules.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=audit_time_rules
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - adjtimex
            syscall_grouping:
              - adjtimex
              - settimeofday
              - stime

        - name: Check existence of adjtimex in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=audit_time_rules
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83840-9
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_adjtimex
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for adjtimex for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - adjtimex
            syscall_grouping:
              - adjtimex
              - settimeofday

        - name: Check existence of adjtimex in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
            patterns: '*.rules'
          register: find_command
          loop: '{{ (syscall_grouping + syscalls) | unique }}'

        - name: Reset syscalls found per file
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/audit_time_rules.rules
          set_fact: audit_file="/etc/audit/rules.d/audit_time_rules.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=audit_time_rules
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - adjtimex
            syscall_grouping:
              - adjtimex
              - settimeofday
              - stime

        - name: Check existence of adjtimex in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=audit_time_rules
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83840-9
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_adjtimex
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
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:,{{ -a%20always%2Cexit%20-F%20arch%3Db64%20-S%20adjtimex%20-k%20audit_time_rules%0A-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20adjtimex%20-k%20audit_time_rules%0A }}
            mode: 0600
            path: /etc/audit/rules.d/75-syscall-adjtimex.rules
            overwrite: true
#### Rule Record Attempts to Alter Time Through clock_settime [ref]
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 clock_settime -F a0=0x0 -F key=time-change
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S clock_settime -F a0=0x0 -F key=time-change
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 clock_settime -F a0=0x0 -F key=time-change
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S clock_settime -F a0=0x0 -F key=time-change
The -k option allows for the specification of a key in string form that can be used for better reporting capability through ausearch and aureport. Multiple system calls can be defined on the same line to save space if desired, but is not required. See an example of multiple combined syscalls:
    -a always,exit -F arch=b64 -S adjtimex,settimeofday -F key=audit_time_rules
Rationale: | Arbitrary changes to the system time can be used to obfuscate nefarious activities in log files, as well as to confuse network services that are highly dependent upon an accurate system time (such as sshd). All changes to the system time should be audited.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_time_clock_settime
Identifiers and References | Identifiers: CCE-83837-5 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-001487, CCI-000169, 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), 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, 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, 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, AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, Req-10.4.2.b
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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="-F a0=0x0"
    	AUID_FILTERS=""
    	SYSCALL="clock_settime"
    	KEY="time-change"
    	SYSCALL_GROUPING=""
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83837-5
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_clock_settime
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Set architecture for audit tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83837-5
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_clock_settime
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for clock_settime for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - clock_settime
            syscall_grouping: []

        - name: Check existence of clock_settime in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F a0=0x0 (-k\s+|-F\s+key=)\S+\s*$
            patterns: '*.rules'
          register: find_command
          loop: '{{ (syscall_grouping + syscalls) | unique }}'

        - name: Reset syscalls found per file
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/time-change.rules
          set_fact: audit_file="/etc/audit/rules.d/time-change.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a0=0x0 (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a0=0x0 -F
              key=time-change
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - clock_settime
            syscall_grouping: []

        - name: Check existence of clock_settime in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F a0=0x0 (-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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a0=0x0 (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F a0=0x0 -F
              key=time-change
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83837-5
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_clock_settime
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for clock_settime for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - clock_settime
            syscall_grouping: []

        - name: Check existence of clock_settime in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F a0=0x0 (-k\s+|-F\s+key=)\S+\s*$
            patterns: '*.rules'
          register: find_command
          loop: '{{ (syscall_grouping + syscalls) | unique }}'

        - name: Reset syscalls found per file
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/time-change.rules
          set_fact: audit_file="/etc/audit/rules.d/time-change.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a0=0x0 (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a0=0x0 -F
              key=time-change
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - clock_settime
            syscall_grouping: []

        - name: Check existence of clock_settime in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* -F a0=0x0 (-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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( -F a0=0x0 (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F a0=0x0 -F
              key=time-change
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83837-5
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_clock_settime
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
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:,{{ -a%20always%2Cexit%20-F%20arch%3Db64%20-S%20clock_settime%20-F%20a0%3D0x0%20-k%20time-change%0A-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20clock_settime%20-F%20a0%3D0x0%20-k%20time-change%0A }}
            mode: 0600
            path: /etc/audit/rules.d/75-syscall-clock-settime.rules
            overwrite: true
#### Rule Record attempts to alter time through settimeofday [ref]
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 settimeofday -F key=audit_time_rules
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S settimeofday -F key=audit_time_rules
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 settimeofday -F key=audit_time_rules
If the system is 64 bit then also add the following line:
    -a always,exit -F arch=b64 -S settimeofday -F key=audit_time_rules
The -k option allows for the specification of a key in string form that can be used for better reporting capability through ausearch and aureport. Multiple system calls can be defined on the same line to save space if desired, but is not required. See an example of multiple combined syscalls:
    -a always,exit -F arch=b64 -S adjtimex,settimeofday -F key=audit_time_rules
Rationale: | Arbitrary changes to the system time can be used to obfuscate nefarious activities in log files, as well as to confuse network services that are highly dependent upon an accurate system time (such as sshd). All changes to the system time should be audited.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_time_settimeofday
Identifiers and References | Identifiers: CCE-83836-7 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-001487, CCI-000169, 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), 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, 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, 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, AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, Req-10.4.2.b
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    # Retrieve hardware architecture of the underlying system
    [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

    for ARCH in "${RULE_ARCHS[@]}"
    do
        # Create expected audit group and audit rule form for particular system call & architecture
        if [ ${ARCH} = "b32" ]
        then
            ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
            # stime system call is known at 32-bit arch (see e.g "$ ausyscall i386 stime" 's output)
            # so append it to the list of time group system calls to be audited
            SYSCALL="adjtimex settimeofday stime"
            SYSCALL_GROUPING="adjtimex settimeofday stime"
        elif [ ${ARCH} = "b64" ]
        then
            ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
            # stime system call isn't known at 64-bit arch (see "$ ausyscall x86_64 stime" 's output)
            # therefore don't add it to the list of time group system calls to be audited
            SYSCALL="adjtimex settimeofday"
            SYSCALL_GROUPING="adjtimex settimeofday"
        fi
        OTHER_FILTERS=""
        AUID_FILTERS=""
        KEY="audit_time_rules"
        # 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 0640 "$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 o-rwx ${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 o-rwx ${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
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83836-7
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_settimeofday
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Set architecture for audit tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83836-7
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_settimeofday
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for settimeofday for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - settimeofday
            syscall_grouping:
              - adjtimex
              - settimeofday
              - stime

        - name: Check existence of settimeofday in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
            patterns: '*.rules'
          register: find_command
          loop: '{{ (syscall_grouping + syscalls) | unique }}'

        - name: Reset syscalls found per file
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/audit_time_rules.rules
          set_fact: audit_file="/etc/audit/rules.d/audit_time_rules.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=audit_time_rules
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - settimeofday
            syscall_grouping:
              - adjtimex
              - settimeofday
              - stime

        - name: Check existence of settimeofday in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=audit_time_rules
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83836-7
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_settimeofday
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for settimeofday for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - settimeofday
            syscall_grouping:
              - adjtimex
              - settimeofday
              - stime

        - name: Check existence of settimeofday in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
            patterns: '*.rules'
          register: find_command
          loop: '{{ (syscall_grouping + syscalls) | unique }}'

        - name: Reset syscalls found per file
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/audit_time_rules.rules
          set_fact: audit_file="/etc/audit/rules.d/audit_time_rules.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=audit_time_rules
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - settimeofday
            syscall_grouping:
              - adjtimex
              - settimeofday
              - stime

        - name: Check existence of settimeofday in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=audit_time_rules
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83836-7
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_settimeofday
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
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:,{{ -a%20always%2Cexit%20-F%20arch%3Db64%20-S%20settimeofday%20-k%20audit_time_rules%0A-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20settimeofday%20-k%20audit_time_rules%0A }}
            mode: 0600
            path: /etc/audit/rules.d/75-syscall-settimeofday.rules
            overwrite: true
#### Rule Record Attempts to Alter Time Through stime [ref]
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` for both 32 bit and 64 bit systems:
    -a always,exit -F arch=b32 -S stime -F key=audit_time_rules
Since the 64 bit version of the "stime" system call is not defined in the audit lookup table, the corresponding "-F arch=b64" form of this rule is not expected to be defined on 64 bit systems (the aforementioned "-F arch=b32" stime rule form itself is sufficient for both 32 bit and 64 bit systems). 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 for both 32 bit and 64 bit systems:
    -a always,exit -F arch=b32 -S stime -F key=audit_time_rules
Since the 64 bit version of the "stime" system call is not defined in the audit lookup table, the corresponding "-F arch=b64" form of this rule is not expected to be defined on 64 bit systems (the aforementioned "-F arch=b32" stime rule form itself is sufficient for both 32 bit and 64 bit systems). The -k option allows for the specification of a key in string form that can be used for better reporting capability through ausearch and aureport. Multiple system calls can be defined on the same line to save space if desired, but is not required. See an example of multiple combined system calls:
    -a always,exit -F arch=b64 -S adjtimex,settimeofday -F key=audit_time_rules
Rationale: | Arbitrary changes to the system time can be used to obfuscate nefarious activities in log files, as well as to confuse network services that are highly dependent upon an accurate system time (such as sshd). All changes to the system time should be audited.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_time_stime
Identifiers and References | Identifiers: CCE-83835-9 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-001487, CCI-000169, 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), 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, 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, 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, AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, Req-10.4.2.b
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    # Retrieve hardware architecture of the underlying system
    [ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

    for ARCH in "${RULE_ARCHS[@]}"
    do
        # Create expected audit group and audit rule form for particular system call & architecture
        if [ ${ARCH} = "b32" ]
        then
            ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
            # stime system call is known at 32-bit arch (see e.g "$ ausyscall i386 stime" 's output)
            # so append it to the list of time group system calls to be audited
            SYSCALL="adjtimex settimeofday stime"
            SYSCALL_GROUPING="adjtimex settimeofday stime"
        elif [ ${ARCH} = "b64" ]
        then
            ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
            # stime system call isn't known at 64-bit arch (see "$ ausyscall x86_64 stime" 's output)
            # therefore don't add it to the list of time group system calls to be audited
            SYSCALL="adjtimex settimeofday"
            SYSCALL_GROUPING="adjtimex settimeofday"
        fi
        OTHER_FILTERS=""
        AUID_FILTERS=""
        KEY="audit_time_rules"
        # 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 0640 "$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 o-rwx ${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 o-rwx ${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
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83835-9
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_stime
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for stime syscall for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - stime
            syscall_grouping:
              - adjtimex
              - settimeofday
              - stime

        - name: Check existence of stime in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
            patterns: '*.rules'
          register: find_command
          loop: '{{ (syscall_grouping + syscalls) | unique }}'

        - name: Reset syscalls found per file
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/audit_time_rules.rules
          set_fact: audit_file="/etc/audit/rules.d/audit_time_rules.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=audit_time_rules
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - stime
            syscall_grouping:
              - adjtimex
              - settimeofday
              - stime

        - name: Check existence of stime in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=audit_time_rules
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83835-9
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_stime
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
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:,{{ -a%20always%2Cexit%20-F%20arch%3Db64%20-S%20stime%20-k%20audit_time_rules%0A-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20stime%20-k%20audit_time_rules%0A }}
            mode: 0600
            path: /etc/audit/rules.d/75-syscall-stime.rules
            overwrite: true
#### Rule Record Attempts to Alter the localtime File [ref]
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`:
    -w /etc/localtime -p wa -k audit_time_rules
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:
    -w /etc/localtime -p wa -k audit_time_rules
The -k option allows for the specification of a key in string form that can be used for better reporting capability through ausearch and aureport and should always be used.
Rationale: | Arbitrary changes to the system time can be used to obfuscate nefarious activities in log files, as well as to confuse network services that are highly dependent upon an accurate system time (such as sshd). All changes to the system time should be audited.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_time_watch_localtime
Identifiers and References | Identifiers: CCE-83839-1 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-001487, CCI-000169, 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), 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, 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, 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, AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, Req-10.4.2.b
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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/localtime" "$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/localtime $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/localtime$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/localtime -p wa -k audit_time_rules" >> "$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_time_rules.rules' to list of files for inspection.
    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/localtime" /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_time_rules.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/audit_time_rules.rules"
        # If the audit_time_rules.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0640 "$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/localtime" "$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/localtime $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/localtime$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/localtime -p wa -k audit_time_rules" >> "$audit_rules_file"
        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83839-1
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_watch_localtime
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Check if watch rule for /etc/localtime already exists in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/etc/localtime\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83839-1
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_watch_localtime
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key audit_time_rules
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)audit_time_rules$
        patterns: '*.rules'
      register: find_watch_key
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83839-1
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_watch_localtime
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Use /etc/audit/rules.d/audit_time_rules.rules as the recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/audit_time_rules.rules
      when:
        - '"audit" 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:
        - CCE-83839-1
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_watch_localtime
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" 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:
        - CCE-83839-1
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_watch_localtime
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Add watch rule for /etc/localtime in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/localtime -p wa -k audit_time_rules
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83839-1
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_watch_localtime
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Check if watch rule for /etc/localtime already exists in /etc/audit/audit.rules
      find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/etc/localtime\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83839-1
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_watch_localtime
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Add watch rule for /etc/localtime in /etc/audit/audit.rules
      lineinfile:
        line: -w /etc/localtime -p wa -k audit_time_rules
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
          == 0
      tags:
        - CCE-83839-1
        - CJIS-5.4.1.1
        - 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.4.2.b
        - audit_rules_time_watch_localtime
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
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/localtime%20-p%20wa%20-k%20audit_time_rules%0A }}
            mode: 0600
            path: /etc/audit/rules.d/75-etclocaltime-wa-audit_time_rules.rules
            overwrite: true
#### Rule Make the auditd Configuration Immutable [ref]
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` in order to make the auditd configuration immutable:
    -e 2
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 in order to make the auditd configuration immutable:
    -e 2
With this setting, a reboot will be required to change any audit rules.
Rationale: | Making the audit configuration immutable prevents accidental as well as malicious modification of the audit rules, although it may be problematic if legitimate changes are needed during system operation
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_immutable
Identifiers and References | Identifiers: CCE-83716-1 References: 1, 11, 12, 13, 14, 15, 16, 18, 19, 3, 4, 5, 6, 7, 8, 5.4.1.1, APO01.06, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, BAI03.05, BAI08.02, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.04, DSS05.07, DSS06.02, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.3.1, 3.4.3, CCI-000162, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.310(a)(2)(iv), 164.312(d), 164.310(d)(2)(iii), 164.312(b), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.7.3, 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, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 5.2, SR 6.1, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, 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.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, 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.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, AC-6(9), CM-6(a), DE.AE-3, DE.AE-5, ID.SC-4, PR.AC-4, PR.DS-5, PR.PT-1, RS.AN-1, RS.AN-4, Req-10.5.2, SRG-OS-000057-GPOS-00027, SRG-OS-000058-GPOS-00028, SRG-OS-000059-GPOS-00029
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    # Traverse all of:
    #
    # /etc/audit/audit.rules,			(for auditctl case)
    # /etc/audit/rules.d/*.rules			(for augenrules case)
    #
    # files to check if '-e .*' setting is present in that '*.rules' file already.
    # If found, delete such occurrence since auditctl(8) manual page instructs the
    # '-e 2' rule should be placed as the last rule in the configuration
    find /etc/audit /etc/audit/rules.d -maxdepth 1 -type f -name '*.rules' -exec sed -i '/-e[[:space:]]\+.*/d' {} ';'

    # Append '-e 2' requirement at the end of both:
    # * /etc/audit/audit.rules file 		(for auditctl case)
    # * /etc/audit/rules.d/immutable.rules		(for augenrules case)

    for AUDIT_FILE in "/etc/audit/audit.rules" "/etc/audit/rules.d/immutable.rules"
    do
    	echo '' >> $AUDIT_FILE
    	echo '# Set the audit.rules configuration immutable per security requirements' >> $AUDIT_FILE
    	echo '# Reboot is required to change audit rules once this setting is applied' >> $AUDIT_FILE
    	echo '-e 2' >> $AUDIT_FILE
    	chmod o-rwx $AUDIT_FILE
    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:
        - CCE-83716-1
        - CJIS-5.4.1.1
        - NIST-800-171-3.3.1
        - NIST-800-171-3.4.3
        - NIST-800-53-AC-6(9)
        - NIST-800-53-CM-6(a)
        - PCI-DSS-Req-10.5.2
        - audit_rules_immutable
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Collect all files from /etc/audit/rules.d with .rules extension
      find:
        paths: /etc/audit/rules.d/
        patterns: '*.rules'
      register: find_rules_d
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83716-1
        - CJIS-5.4.1.1
        - NIST-800-171-3.3.1
        - NIST-800-171-3.4.3
        - NIST-800-53-AC-6(9)
        - NIST-800-53-CM-6(a)
        - PCI-DSS-Req-10.5.2
        - audit_rules_immutable
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Remove the -e option from all Audit config files
      lineinfile:
        path: '{{ item }}'
        regexp: ^\s*(?:-e)\s+.*$
        state: absent
      loop: '{{ find_rules_d.files | map(attribute=''path'') | list + [''/etc/audit/audit.rules'']
        }}'
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83716-1
        - CJIS-5.4.1.1
        - NIST-800-171-3.3.1
        - NIST-800-171-3.4.3
        - NIST-800-53-AC-6(9)
        - NIST-800-53-CM-6(a)
        - PCI-DSS-Req-10.5.2
        - audit_rules_immutable
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Add Audit -e option into /etc/audit/rules.d/immutable.rules and /etc/audit/audit.rules
      lineinfile:
        path: '{{ item }}'
        create: true
        line: -e 2
        mode: o-rwx
      loop:
        - /etc/audit/audit.rules
        - /etc/audit/rules.d/immutable.rules
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83716-1
        - CJIS-5.4.1.1
        - NIST-800-171-3.3.1
        - NIST-800-171-3.4.3
        - NIST-800-53-AC-6(9)
        - NIST-800-53-CM-6(a)
        - PCI-DSS-Req-10.5.2
        - audit_rules_immutable
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,-e%202%0A
            mode: 0600
            path: /etc/audit/rules.d/90-immutable.rules
            overwrite: true
#### Rule Record Events that Modify the System's Mandatory Access Controls [ref]
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`:
    -w /etc/selinux/ -p wa -k MAC-policy
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:
    -w /etc/selinux/ -p wa -k MAC-policy
Rationale: | The system's mandatory access policy (SELinux) should not be arbitrarily changed by anything other than administrator action. All changes to MAC policy should be audited.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_mac_modification
Identifiers and References | Identifiers: CCE-83721-1 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.8, 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), 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, 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, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.5.5
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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/selinux/" "$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/selinux/ $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/selinux/$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/selinux/ -p wa -k MAC-policy" >> "$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/MAC-policy.rules' to list of files for inspection.
    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/selinux/" /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/MAC-policy.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/MAC-policy.rules"
        # If the MAC-policy.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0640 "$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/selinux/" "$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/selinux/ $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/selinux/$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/selinux/ -p wa -k MAC-policy" >> "$audit_rules_file"
        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:
        - CCE-83721-1
        - CJIS-5.4.1.1
        - NIST-800-171-3.1.8
        - NIST-800-53-AU-12(c)
        - NIST-800-53-AU-2(d)
        - NIST-800-53-CM-6(a)
        - PCI-DSS-Req-10.5.5
        - audit_rules_mac_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Check if watch rule for /etc/selinux/ already exists in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/etc/selinux/\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83721-1
        - CJIS-5.4.1.1
        - NIST-800-171-3.1.8
        - NIST-800-53-AU-12(c)
        - NIST-800-53-AU-2(d)
        - NIST-800-53-CM-6(a)
        - PCI-DSS-Req-10.5.5
        - audit_rules_mac_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key MAC-policy
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)MAC-policy$
        patterns: '*.rules'
      register: find_watch_key
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83721-1
        - CJIS-5.4.1.1
        - NIST-800-171-3.1.8
        - NIST-800-53-AU-12(c)
        - NIST-800-53-AU-2(d)
        - NIST-800-53-CM-6(a)
        - PCI-DSS-Req-10.5.5
        - audit_rules_mac_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Use /etc/audit/rules.d/MAC-policy.rules as the recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/MAC-policy.rules
      when:
        - '"audit" 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:
        - CCE-83721-1
        - CJIS-5.4.1.1
        - NIST-800-171-3.1.8
        - NIST-800-53-AU-12(c)
        - NIST-800-53-AU-2(d)
        - NIST-800-53-CM-6(a)
        - PCI-DSS-Req-10.5.5
        - audit_rules_mac_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" 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:
        - CCE-83721-1
        - CJIS-5.4.1.1
        - NIST-800-171-3.1.8
        - NIST-800-53-AU-12(c)
        - NIST-800-53-AU-2(d)
        - NIST-800-53-CM-6(a)
        - PCI-DSS-Req-10.5.5
        - audit_rules_mac_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Add watch rule for /etc/selinux/ in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/selinux/ -p wa -k MAC-policy
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83721-1
        - CJIS-5.4.1.1
        - NIST-800-171-3.1.8
        - NIST-800-53-AU-12(c)
        - NIST-800-53-AU-2(d)
        - NIST-800-53-CM-6(a)
        - PCI-DSS-Req-10.5.5
        - audit_rules_mac_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Check if watch rule for /etc/selinux/ already exists in /etc/audit/audit.rules
      find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/etc/selinux/\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83721-1
        - CJIS-5.4.1.1
        - NIST-800-171-3.1.8
        - NIST-800-53-AU-12(c)
        - NIST-800-53-AU-2(d)
        - NIST-800-53-CM-6(a)
        - PCI-DSS-Req-10.5.5
        - audit_rules_mac_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Add watch rule for /etc/selinux/ in /etc/audit/audit.rules
      lineinfile:
        line: -w /etc/selinux/ -p wa -k MAC-policy
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
          == 0
      tags:
        - CCE-83721-1
        - CJIS-5.4.1.1
        - NIST-800-171-3.1.8
        - NIST-800-53-AU-12(c)
        - NIST-800-53-AU-2(d)
        - NIST-800-53-CM-6(a)
        - PCI-DSS-Req-10.5.5
        - audit_rules_mac_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
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/selinux/%20-p%20wa%20-k%20MAC-policy%0A }}
            mode: 0600
            path: /etc/audit/rules.d/75-etcselinux-wa-MAC-policy.rules
            overwrite: true
#### Rule Ensure auditd Collects Information on Exporting to Media (successful) [ref]
At a minimum, the audit system should collect media exportation events 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`, setting ARCH to either b32 or b64 as appropriate for your system:
    -a always,exit -F arch=ARCH -S mount -F auid>=1000 -F auid!=unset -F key=export
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, setting ARCH to either b32 or b64 as appropriate for your system:
    -a always,exit -F arch=ARCH -S mount -F auid>=1000 -F auid!=unset -F key=export
Rationale: | The unauthorized exportation of data to external media could result in an information leak where classified information, Privacy Act information, and intellectual property could be lost. An audit trail should be created each time a filesystem is mounted to help identify and guard against information loss.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_media_export
Identifiers and References | Identifiers: CCE-83735-1 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000135, CCI-000169, CCI-002884, 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), 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, 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, 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, AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, Req-10.2.7, 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
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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="mount"
    	KEY="perm_mod"
    	SYSCALL_GROUPING=""

    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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:
        - CCE-83735-1
        - CJIS-5.4.1.1
        - 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.7
        - audit_rules_media_export
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit mount tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83735-1
        - CJIS-5.4.1.1
        - 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.7
        - audit_rules_media_export
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for mount for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - mount
            syscall_grouping: []

        - name: Check existence of mount in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - mount
            syscall_grouping: []

        - name: Check existence of mount in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83735-1
        - CJIS-5.4.1.1
        - 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.7
        - audit_rules_media_export
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for mount for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - mount
            syscall_grouping: []

        - name: Check existence of mount in /etc/audit/rules.d/
          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
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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
          set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - mount
            syscall_grouping: []

        - name: Check existence of mount in /etc/audit/audit.rules
          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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          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
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          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: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83735-1
        - CJIS-5.4.1.1
        - 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.7
        - audit_rules_media_export
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
#### Rule Record Events that Modify the System's Network Environment [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`, setting ARCH to either b32 or b64 as appropriate for your system:
    -a always,exit -F arch=ARCH -S sethostname,setdomainname -F key=audit_rules_networkconfig_modification
    -w /etc/issue -p wa -k audit_rules_networkconfig_modification
    -w /etc/issue.net -p wa -k audit_rules_networkconfig_modification
    -w /etc/hosts -p wa -k audit_rules_networkconfig_modification
    -w /etc/sysconfig/network -p wa -k audit_rules_networkconfig_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` file, setting ARCH to either b32 or b64 as appropriate for your system:
    -a always,exit -F arch=ARCH -S sethostname,setdomainname -F key=audit_rules_networkconfig_modification
    -w /etc/issue -p wa -k audit_rules_networkconfig_modification
    -w /etc/issue.net -p wa -k audit_rules_networkconfig_modification
    -w /etc/hosts -p wa -k audit_rules_networkconfig_modification
    -w /etc/sysconfig/network -p wa -k audit_rules_networkconfig_modification
Rationale: | The network environment should not be modified by anything other than administrator action. Any change to network parameters should be audited.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_networkconfig_modification
Identifiers and References | Identifiers: CCE-83706-2 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, 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), 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, 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, 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, AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, Req-10.5.5
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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=""
    	SYSCALL="sethostname setdomainname"
    	KEY="audit_rules_networkconfig_modification"
    	SYSCALL_GROUPING="sethostname setdomainname"
    	# 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 0640 "$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 o-rwx ${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 o-rwx ${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

    # Then perform the remediations for the watch rules
    # 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/issue" "$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/issue $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/issue$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/issue -p wa -k audit_rules_networkconfig_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_networkconfig_modification.rules' to list of files for inspection.
    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/issue" /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_networkconfig_modification.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/audit_rules_networkconfig_modification.rules"
        # If the audit_rules_networkconfig_modification.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0640 "$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/issue" "$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/issue $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/issue$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/issue -p wa -k audit_rules_networkconfig_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 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/issue.net" "$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/issue.net $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/issue.net$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/issue.net -p wa -k audit_rules_networkconfig_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_networkconfig_modification.rules' to list of files for inspection.
    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/issue.net" /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_networkconfig_modification.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/audit_rules_networkconfig_modification.rules"
        # If the audit_rules_networkconfig_modification.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0640 "$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/issue.net" "$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/issue.net $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/issue.net$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/issue.net -p wa -k audit_rules_networkconfig_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 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/hosts" "$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/hosts $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/hosts$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/hosts -p wa -k audit_rules_networkconfig_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_networkconfig_modification.rules' to list of files for inspection.
    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/hosts" /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_networkconfig_modification.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/audit_rules_networkconfig_modification.rules"
        # If the audit_rules_networkconfig_modification.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0640 "$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/hosts" "$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/hosts $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/hosts$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/hosts -p wa -k audit_rules_networkconfig_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 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/sysconfig/network" "$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/sysconfig/network $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/sysconfig/network$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/sysconfig/network -p wa -k audit_rules_networkconfig_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_networkconfig_modification.rules' to list of files for inspection.
    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/sysconfig/network" /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_networkconfig_modification.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/audit_rules_networkconfig_modification.rules"
        # If the audit_rules_networkconfig_modification.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0640 "$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/sysconfig/network" "$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/sysconfig/network $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/sysconfig/network$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/sysconfig/network -p wa -k audit_rules_networkconfig_modification" >> "$audit_rules_file"
        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Set architecture for audit tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Remediate audit rules for network configuration for x86
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - sethostname
              - setdomainname
            syscall_grouping:
              - sethostname
              - setdomainname

        - name: Check existence of sethostname, setdomainname in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
            patterns: '*.rules'
          register: find_command
          loop: '{{ (syscall_grouping + syscalls) | unique }}'

        - name: Reset syscalls found per file
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/audit_rules_networkconfig_modification.rules
          set_fact: audit_file="/etc/audit/rules.d/audit_rules_networkconfig_modification.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=audit_rules_networkconfig_modification
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - sethostname
              - setdomainname
            syscall_grouping:
              - sethostname
              - setdomainname

        - name: Check existence of sethostname, setdomainname in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=audit_rules_networkconfig_modification
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Remediate audit rules for network configuration for x86_64
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - sethostname
              - setdomainname
            syscall_grouping:
              - sethostname
              - setdomainname

        - name: Check existence of sethostname, setdomainname in /etc/audit/rules.d/
          find:
            paths: /etc/audit/rules.d
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
            patterns: '*.rules'
          register: find_command
          loop: '{{ (syscall_grouping + syscalls) | unique }}'

        - name: Reset syscalls found per file
          set_fact:
            syscalls_per_file: {}
            found_paths_dict: {}

        - name: Declare syscalls found per file
          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
          set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
            | map(attribute='path') | list }}"

        - name: Count occurrences of syscalls in paths
          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
          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/audit_rules_networkconfig_modification.rules
          set_fact: audit_file="/etc/audit/rules.d/audit_rules_networkconfig_modification.rules"
          when: found_paths | length == 0

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=audit_rules_networkconfig_modification
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - sethostname
              - setdomainname
            syscall_grouping:
              - sethostname
              - setdomainname

        - name: Check existence of sethostname, setdomainname in /etc/audit/audit.rules
          find:
            paths: /etc/audit
            contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
              -S |,)\w+)* (-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
          set_fact: audit_file="/etc/audit/audit.rules"

        - name: Declare found syscalls
          set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
            map(attribute='item') | list }}"

        - name: Declare missing syscalls
          set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

        - name: Replace the audit rule in {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
              | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
            line: \1\2\3{{ missing_syscalls | join("\3") }}\4
            backrefs: true
            state: present
          when: syscalls_found | length > 0 and missing_syscalls | length > 0

        - name: Add the audit rule to {{ audit_file }}
          lineinfile:
            path: '{{ audit_file }}'
            line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=audit_rules_networkconfig_modification
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Check if watch rule for /etc/issue already exists in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/etc/issue\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key audit_rules_networkconfig_modification
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)audit_rules_networkconfig_modification$
        patterns: '*.rules'
      register: find_watch_key
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Use /etc/audit/rules.d/audit_rules_networkconfig_modification.rules as the
        recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/audit_rules_networkconfig_modification.rules
      when:
        - '"audit" 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:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" 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:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Add watch rule for /etc/issue in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/issue -p wa -k audit_rules_networkconfig_modification
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Check if watch rule for /etc/issue already exists in /etc/audit/audit.rules
      find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/etc/issue\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Add watch rule for /etc/issue in /etc/audit/audit.rules
      lineinfile:
        line: -w /etc/issue -p wa -k audit_rules_networkconfig_modification
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
          == 0
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Check if watch rule for /etc/issue.net already exists in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/etc/issue.net\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key audit_rules_networkconfig_modification
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)audit_rules_networkconfig_modification$
        patterns: '*.rules'
      register: find_watch_key
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Use /etc/audit/rules.d/audit_rules_networkconfig_modification.rules as the
        recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/audit_rules_networkconfig_modification.rules
      when:
        - '"audit" 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:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" 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:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Add watch rule for /etc/issue.net in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/issue.net -p wa -k audit_rules_networkconfig_modification
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Check if watch rule for /etc/issue.net already exists in /etc/audit/audit.rules
      find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/etc/issue.net\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Add watch rule for /etc/issue.net in /etc/audit/audit.rules
      lineinfile:
        line: -w /etc/issue.net -p wa -k audit_rules_networkconfig_modification
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
          == 0
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Check if watch rule for /etc/hosts already exists in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/etc/hosts\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key audit_rules_networkconfig_modification
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)audit_rules_networkconfig_modification$
        patterns: '*.rules'
      register: find_watch_key
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Use /etc/audit/rules.d/audit_rules_networkconfig_modification.rules as the
        recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/audit_rules_networkconfig_modification.rules
      when:
        - '"audit" 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:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" 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:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Add watch rule for /etc/hosts in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/hosts -p wa -k audit_rules_networkconfig_modification
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Check if watch rule for /etc/hosts already exists in /etc/audit/audit.rules
      find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/etc/hosts\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Add watch rule for /etc/hosts in /etc/audit/audit.rules
      lineinfile:
        line: -w /etc/hosts -p wa -k audit_rules_networkconfig_modification
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
          == 0
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Check if watch rule for /etc/sysconfig/network already exists in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/etc/sysconfig/network\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key audit_rules_networkconfig_modification
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)audit_rules_networkconfig_modification$
        patterns: '*.rules'
      register: find_watch_key
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Use /etc/audit/rules.d/audit_rules_networkconfig_modification.rules as the
        recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/audit_rules_networkconfig_modification.rules
      when:
        - '"audit" 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:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" 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:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Add watch rule for /etc/sysconfig/network in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/sysconfig/network -p wa -k audit_rules_networkconfig_modification
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Check if watch rule for /etc/sysconfig/network already exists in /etc/audit/audit.rules
      find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/etc/sysconfig/network\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Add watch rule for /etc/sysconfig/network in /etc/audit/audit.rules
      lineinfile:
        line: -w /etc/sysconfig/network -p wa -k audit_rules_networkconfig_modification
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
          == 0
      tags:
        - CCE-83706-2
        - CJIS-5.4.1.1
        - 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.5.5
        - audit_rules_networkconfig_modification
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Record Attempts to Alter Process and Session Initiation Information [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` in order to watch for attempted manual edits of files involved in storing such process information:
    -w /var/run/utmp -p wa -k session
    -w /var/log/btmp -p wa -k session
    -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` file in order to watch for attempted manual edits of files involved in storing such process information:
    -w /var/run/utmp -p wa -k session
    -w /var/log/btmp -p wa -k session
    -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
Identifiers and References | Identifiers: CCE-83713-8 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, 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), 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, 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, 0582, 0584, 05885, 0586, 0846, 0957, 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, AU-2(d), AU-12(c), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.3
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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
    # 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 0640 "$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
    # 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 0640 "$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: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Check if watch rule for /var/run/utmp already exists in /etc/audit/rules.d/
      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'
      tags:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key session
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)session$
        patterns: '*.rules'
      register: find_watch_key
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Use /etc/audit/rules.d/session.rules as the recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/session.rules
      when:
        - '"audit" 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:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" 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:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Add watch rule for /var/run/utmp in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /var/run/utmp -p wa -k session
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Check if watch rule for /var/run/utmp already exists in /etc/audit/audit.rules
      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'
      tags:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Add watch rule for /var/run/utmp in /etc/audit/audit.rules
      lineinfile:
        line: -w /var/run/utmp -p wa -k session
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
          == 0
      tags:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Check if watch rule for /var/log/btmp already exists in /etc/audit/rules.d/
      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'
      tags:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key session
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)session$
        patterns: '*.rules'
      register: find_watch_key
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Use /etc/audit/rules.d/session.rules as the recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/session.rules
      when:
        - '"audit" 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:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" 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:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Add watch rule for /var/log/btmp in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /var/log/btmp -p wa -k session
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Check if watch rule for /var/log/btmp already exists in /etc/audit/audit.rules
      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'
      tags:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Add watch rule for /var/log/btmp in /etc/audit/audit.rules
      lineinfile:
        line: -w /var/log/btmp -p wa -k session
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
          == 0
      tags:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Check if watch rule for /var/log/wtmp already exists in /etc/audit/rules.d/
      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'
      tags:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key session
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)session$
        patterns: '*.rules'
      register: find_watch_key
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Use /etc/audit/rules.d/session.rules as the recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/session.rules
      when:
        - '"audit" 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:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" 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:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Add watch rule for /var/log/wtmp in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /var/log/wtmp -p wa -k session
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
          == 0
      tags:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Check if watch rule for /var/log/wtmp already exists in /etc/audit/audit.rules
      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'
      tags:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Add watch rule for /var/log/wtmp in /etc/audit/audit.rules
      lineinfile:
        line: -w /var/log/wtmp -p wa -k session
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
        - '"audit" in ansible_facts.packages'
        - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
          == 0
      tags:
        - CCE-83713-8
        - CJIS-5.4.1.1
        - 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.2.3
        - audit_rules_session_events
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
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:,{{ %0A-w%20/var/run/utmp%20-p%20wa%20-k%20session%0A-w%20/var/log/btmp%20-p%20wa%20-k%20session%0A-w%20/var/log/wtmp%20-p%20wa%20-k%20session%0A }}
            mode: 0600
            path: /etc/audit/rules.d/75-audit-session-events.rules
            overwrite: true
#### Rule Ensure auditd Collects System Administrator Actions [ref]
At a minimum, the audit system should collect administrator actions 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`:
    -w /etc/sudoers -p wa -k actions
    -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 line to `/etc/audit/audit.rules` file:
    -w /etc/sudoers -p wa -k actions
    -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
Identifiers and References | Identifiers: CCE-83729-4 References: 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000126, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, 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), 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, 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, 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, AC-2(7)(b), AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.2, Req-10.2.5.b, 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-000462-VMM-001840, SRG-OS-000471-VMM-001910
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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 0640 "$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
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83729-4
        - 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
        - audit_rules_sysadmin_actions
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Search /etc/audit/rules.d for audit rule entries for sysadmin actions
      find:
        paths: /etc/audit/rules.d
        recurse: false
        contains: ^.*/etc/sudoers.*$
        patterns: '*.rules'
      register: find_audit_sysadmin_actions
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83729-4
        - 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
        - audit_rules_sysadmin_actions
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Use /etc/audit/rules.d/actions.rules as the recipient for the rule
      set_fact:
        all_sysadmin_actions_files:
          - /etc/audit/rules.d/actions.rules
      when:
        - '"audit" in ansible_facts.packages'
        - find_audit_sysadmin_actions.matched is defined and find_audit_sysadmin_actions.matched
          == 0
      tags:
        - CCE-83729-4
        - 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
        - audit_rules_sysadmin_actions
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_sysadmin_actions_files:
          - '{{ find_audit_sysadmin_actions.files | map(attribute=''path'') | list | first
            }}'
      when:
        - '"audit" in ansible_facts.packages'
        - find_audit_sysadmin_actions.matched is defined and find_audit_sysadmin_actions.matched
          > 0
      tags:
        - CCE-83729-4
        - 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
        - audit_rules_sysadmin_actions
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Inserts/replaces audit rule for /etc/sudoers rule in rules.d
      lineinfile:
        path: '{{ all_sysadmin_actions_files[0] }}'
        line: -w /etc/sudoers -p wa -k actions
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83729-4
        - 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
        - audit_rules_sysadmin_actions
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Inserts/replaces audit rule for /etc/sudoers.d rule in rules.d
      lineinfile:
        path: '{{ all_sysadmin_actions_files[0] }}'
        line: -w /etc/sudoers.d/ -p wa -k actions
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83729-4
        - 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
        - audit_rules_sysadmin_actions
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Inserts/replaces audit rule for /etc/sudoers in audit.rules
      lineinfile:
        path: /etc/audit/audit.rules
        line: -w /etc/sudoers -p wa -k actions
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83729-4
        - 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
        - audit_rules_sysadmin_actions
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Inserts/replaces audit rule for /etc/sudoers.d in audit.rules
      lineinfile:
        path: /etc/audit/audit.rules
        line: -w /etc/sudoers.d/ -p wa -k actions
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83729-4
        - 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
        - audit_rules_sysadmin_actions
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
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
#### Rule Shutdown System When Auditing Failures Occur [ref]
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`:
    -f 2
If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to the top of the `/etc/audit/audit.rules` file:
    -f 2
Rationale: | It is critical for the appropriate personnel to be aware if a system is at risk of failing to process audit logs as required. Without this notification, the security personnel may be unaware of an impending failure of the audit capability, and system operation may be adversely affected. Audit processing failures include software/hardware errors, failures in the audit capturing mechanisms, and audit storage capacity being reached or exceeded.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_system_shutdown
Identifiers and References | Identifiers: CCE-83709-6 References: 1, 14, 15, 16, 3, 5, 6, APO11.04, BAI03.05, DSS05.04, DSS05.07, MEA02.01, 3.3.1, 3.3.4, CCI-000139, CCI-000140, 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), 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, AU-5(b), SC-24, CM-6(a), PR.PT-1, SRG-OS-000046-GPOS-00022, SRG-OS-000047-GPOS-00023, SRG-OS-000047-VMM-000220
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    # Traverse all of:
    #
    # /etc/audit/audit.rules,			(for auditctl case)
    # /etc/audit/rules.d/*.rules			(for augenrules case)
    find /etc/audit /etc/audit/rules.d -maxdepth 1 -type f -name '*.rules' -exec sed -i '/-f[[:space:]]\+.*/d' {} ';'

    for AUDIT_FILE in "/etc/audit/audit.rules" "/etc/audit/rules.d/immutable.rules"
    do
    	echo '' >> $AUDIT_FILE
    	echo '# Set the audit.rules configuration to halt system upon audit failure per security requirements' >> $AUDIT_FILE
    	echo '-f 2' >> $AUDIT_FILE
    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:
        - CCE-83709-6
        - NIST-800-171-3.3.1
        - NIST-800-171-3.3.4
        - NIST-800-53-AU-5(b)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SC-24
        - audit_rules_system_shutdown
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Collect all files from /etc/audit/rules.d with .rules extension
      find:
        paths: /etc/audit/rules.d/
        patterns: '*.rules'
      register: find_rules_d
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83709-6
        - NIST-800-171-3.3.1
        - NIST-800-171-3.3.4
        - NIST-800-53-AU-5(b)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SC-24
        - audit_rules_system_shutdown
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Remove the -f option from all Audit config files
      lineinfile:
        path: '{{ item }}'
        regexp: ^\s*(?:-f)\s+.*$
        state: absent
      loop: '{{ find_rules_d.files | map(attribute=''path'') | list + [''/etc/audit/audit.rules'']
        }}'
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83709-6
        - NIST-800-171-3.3.1
        - NIST-800-171-3.3.4
        - NIST-800-53-AU-5(b)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SC-24
        - audit_rules_system_shutdown
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Add Audit -f option into /etc/audit/rules.d/immutable.rules and /etc/audit/audit.rules
      lineinfile:
        path: '{{ item }}'
        create: true
        line: -f 2
      loop:
        - /etc/audit/audit.rules
        - /etc/audit/rules.d/immutable.rules
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83709-6
        - NIST-800-171-3.3.1
        - NIST-800-171-3.3.4
        - NIST-800-53-AU-5(b)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SC-24
        - audit_rules_system_shutdown
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - 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`, in order to capture events that modify account changes:
    -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` file, in order to capture events that modify account changes:
    -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
Identifiers and References | Identifiers: CCE-83722-9 References: 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000018, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-001405, CCI-001683, CCI-001684, CCI-001685, CCI-001686, CCI-002130, CCI-002132, CCI-002884, 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), 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, 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, 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, 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, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.5, 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-000004-VMM-000040, SRG-OS-000239-VMM-000810, SRG-OS-000240-VMM-000820, SRG-OS-000241-VMM-000830, SRG-OS-000274-VMM-000960, SRG-OS-000275-VMM-000970, SRG-OS-000276-VMM-000980, SRG-OS-000277-VMM-000990, SRG-OS-000303-VMM-001090, SRG-OS-000304-VMM-001100, SRG-OS-000476-VMM-001960
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83722-9
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_group
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit group tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83722-9
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_group
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other user/group modification audit rules
      find:
        paths: /etc/audit/rules.d
        recurse: false
        contains: -k audit_rules_usergroup_modification$
        patterns: '*.rules'
      register: find_group
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83722-9
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_group
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: If existing user/group modification ruleset not found, use /etc/audit/rules.d/privileged.rules
        as the recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/privileged.rules
      when:
        - '"audit" in ansible_facts.packages'
        - find_group.matched is defined and find_group.matched == 0
      tags:
        - CCE-83722-9
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_group
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_group.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" in ansible_facts.packages'
        - find_group.matched is defined and find_group.matched > 0
      tags:
        - CCE-83722-9
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_group
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Inserts/replaces the group rule in rules.d when on x86
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/group -p wa -k audit_rules_usergroup_modification
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83722-9
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_group
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Inserts/replaces the group rule in /etc/audit/audit.rules
      lineinfile:
        line: -w /etc/group -p wa -k audit_rules_usergroup_modification
        state: present
        dest: /etc/audit/audit.rules
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83722-9
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_group
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - 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`, in order to capture events that modify account changes:
    -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` file, in order to capture events that modify account changes:
    -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
Identifiers and References | Identifiers: CCE-83723-7 References: 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000018, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-001405, CCI-001683, CCI-001684, CCI-001685, CCI-001686, CCI-002130, CCI-002132, 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), 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, 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, 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, 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, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.5, 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-000004-VMM-000040, SRG-OS-000239-VMM-000810, SRG-OS-000240-VMM-000820, SRG-OS-000241-VMM-000830, SRG-OS-000274-VMM-000960, SRG-OS-000275-VMM-000970, SRG-OS-000276-VMM-000980, SRG-OS-000277-VMM-000990, SRG-OS-000303-VMM-001090, SRG-OS-000304-VMM-001100, SRG-OS-000476-VMM-001960
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83723-7
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_gshadow
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit gshadow tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83723-7
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_gshadow
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other user/group modification audit rules
      find:
        paths: /etc/audit/rules.d
        recurse: false
        contains: -k audit_rules_usergroup_modification$
        patterns: '*.rules'
      register: find_gshadow
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83723-7
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_gshadow
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: If existing user/group modification ruleset not found, use /etc/audit/rules.d/privileged.rules
        as the recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/privileged.rules
      when:
        - '"audit" in ansible_facts.packages'
        - find_gshadow.matched is defined and find_gshadow.matched == 0
      tags:
        - CCE-83723-7
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_gshadow
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_gshadow.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" in ansible_facts.packages'
        - find_gshadow.matched is defined and find_gshadow.matched > 0
      tags:
        - CCE-83723-7
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_gshadow
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Inserts/replaces the gshadow rule in rules.d when on x86
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/gshadow -p wa -k audit_rules_usergroup_modification
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83723-7
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_gshadow
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Inserts/replaces the gshadow rule in /etc/audit/audit.rules
      lineinfile:
        line: -w /etc/gshadow -p wa -k audit_rules_usergroup_modification
        state: present
        dest: /etc/audit/audit.rules
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83723-7
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_gshadow
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - 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`, in order to capture events that modify account changes:
    -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` file, in order to capture events that modify account changes:
    -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
Identifiers and References | Identifiers: CCE-83712-0 References: 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000018, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-001405, CCI-001683, CCI-001684, CCI-001685, CCI-001686, CCI-002130, CCI-002132, 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), 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, 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, 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, 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, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.5, 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-000476-GPOS-00221, SRG-OS-000463-GPOS-00207, SRG-OS-000004-VMM-000040, SRG-OS-000239-VMM-000810, SRG-OS-000240-VMM-000820, SRG-OS-000241-VMM-000830, SRG-OS-000274-VMM-000960, SRG-OS-000275-VMM-000970, SRG-OS-000276-VMM-000980, SRG-OS-000277-VMM-000990, SRG-OS-000303-VMM-001090, SRG-OS-000304-VMM-001100, SRG-OS-000476-VMM-001960
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83712-0
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_opasswd
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit opasswd tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83712-0
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_opasswd
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other user/group modification audit rules
      find:
        paths: /etc/audit/rules.d
        recurse: false
        contains: -k audit_rules_usergroup_modification$
        patterns: '*.rules'
      register: find_opasswd
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83712-0
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_opasswd
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: If existing user/group modification ruleset not found, use /etc/audit/rules.d/privileged.rules
        as the recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/privileged.rules
      when:
        - '"audit" in ansible_facts.packages'
        - find_opasswd.matched is defined and find_opasswd.matched == 0
      tags:
        - CCE-83712-0
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_opasswd
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_opasswd.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" in ansible_facts.packages'
        - find_opasswd.matched is defined and find_opasswd.matched > 0
      tags:
        - CCE-83712-0
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_opasswd
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Inserts/replaces the opasswd rule in rules.d when on x86
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83712-0
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_opasswd
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Inserts/replaces the opasswd rule in /etc/audit/audit.rules
      lineinfile:
        line: -w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification
        state: present
        dest: /etc/audit/audit.rules
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83712-0
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_opasswd
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - 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`, in order to capture events that modify account changes:
    -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` file, in order to capture events that modify account changes:
    -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
Identifiers and References | Identifiers: CCE-83714-6 References: 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000018, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-001405, CCI-001683, CCI-001684, CCI-001685, CCI-001686, CCI-002130, CCI-002132, 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), 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, 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, 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, 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, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.5, 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, SRG-OS-000004-VMM-000040, SRG-OS-000239-VMM-000810, SRG-OS-000240-VMM-000820, SRG-OS-000241-VMM-000830, SRG-OS-000274-VMM-000960, SRG-OS-000275-VMM-000970, SRG-OS-000276-VMM-000980, SRG-OS-000277-VMM-000990, SRG-OS-000303-VMM-001090, SRG-OS-000304-VMM-001100, SRG-OS-000476-VMM-001960
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83714-6
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_passwd
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit passwd tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83714-6
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_passwd
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other user/group modification audit rules
      find:
        paths: /etc/audit/rules.d
        recurse: false
        contains: -k audit_rules_usergroup_modification$
        patterns: '*.rules'
      register: find_passwd
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83714-6
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_passwd
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: If existing user/group modification ruleset not found, use /etc/audit/rules.d/privileged.rules
        as the recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/privileged.rules
      when:
        - '"audit" in ansible_facts.packages'
        - find_passwd.matched is defined and find_passwd.matched == 0
      tags:
        - CCE-83714-6
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_passwd
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_passwd.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" in ansible_facts.packages'
        - find_passwd.matched is defined and find_passwd.matched > 0
      tags:
        - CCE-83714-6
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_passwd
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Inserts/replaces the passwd rule in rules.d when on x86
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/passwd -p wa -k audit_rules_usergroup_modification
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83714-6
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_passwd
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Inserts/replaces the passwd rule in /etc/audit/audit.rules
      lineinfile:
        line: -w /etc/passwd -p wa -k audit_rules_usergroup_modification
        state: present
        dest: /etc/audit/audit.rules
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83714-6
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_passwd
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - 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`, in order to capture events that modify account changes:
    -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` file, in order to capture events that modify account changes:
    -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
Identifiers and References | Identifiers: CCE-83725-2 References: 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.1.7, CCI-000018, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-001405, CCI-001683, CCI-001684, CCI-001685, CCI-001686, CCI-002130, CCI-002132, 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), 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, 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, 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, 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, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), 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, FAU_GEN.1.1.c, Req-10.2.5, 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-000004-VMM-000040, SRG-OS-000239-VMM-000810, SRG-OS-000240-VMM-000820, SRG-OS-000241-VMM-000830, SRG-OS-000274-VMM-000960, SRG-OS-000275-VMM-000970, SRG-OS-000276-VMM-000980, SRG-OS-000277-VMM-000990, SRG-OS-000303-VMM-001090, SRG-OS-000304-VMM-001100, SRG-OS-000476-VMM-001960
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; 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 0640 "$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: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83725-2
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_shadow
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Set architecture for audit shadow tasks
      set_fact:
        audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83725-2
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_shadow
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Search /etc/audit/rules.d for other user/group modification audit rules
      find:
        paths: /etc/audit/rules.d
        recurse: false
        contains: -k audit_rules_usergroup_modification$
        patterns: '*.rules'
      register: find_shadow
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83725-2
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_shadow
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: If existing user/group modification ruleset not found, use /etc/audit/rules.d/privileged.rules
        as the recipient for the rule
      set_fact:
        all_files:
          - /etc/audit/rules.d/privileged.rules
      when:
        - '"audit" in ansible_facts.packages'
        - find_shadow.matched is defined and find_shadow.matched == 0
      tags:
        - CCE-83725-2
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_shadow
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
          - '{{ find_shadow.files | map(attribute=''path'') | list | first }}'
      when:
        - '"audit" in ansible_facts.packages'
        - find_shadow.matched is defined and find_shadow.matched > 0
      tags:
        - CCE-83725-2
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_shadow
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Inserts/replaces the shadow rule in rules.d when on x86
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/shadow -p wa -k audit_rules_usergroup_modification
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83725-2
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_shadow
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Inserts/replaces the shadow rule in /etc/audit/audit.rules
      lineinfile:
        line: -w /etc/shadow -p wa -k audit_rules_usergroup_modification
        state: present
        dest: /etc/audit/audit.rules
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83725-2
        - CJIS-5.4.1.1
        - 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
        - audit_rules_usergroup_modification_shadow
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
Group Configure auditd Data Retention Group contains 2 rules
[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 to use audispd's syslog plugin [ref]
To configure the `auditd` service to use the `syslog` plug-in of the `audispd` audit event multiplexor, set the `active` line in `/etc/audit/plugins.d/syslog.conf` to `yes`. Restart the `auditd` service:
    $ sudo service auditd restart
Rationale: | The auditd service does not include the ability to send audit records to a centralized server for management directly. It does, however, include a plug-in for audit event multiplexor (audispd) to pass audit records to the local syslog server
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_auditd_audispd_syslog_plugin_activated
Identifiers and References | Identifiers: CCE-83695-7 References: 1, 11, 12, 13, 14, 15, 16, 19, 3, 4, 5, 6, 7, 8, 5.4.1.1, APO11.04, APO12.06, BAI03.05, BAI08.02, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.04, DSS05.07, MEA02.01, 3.3.1, CCI-000136, 164.308(a)(1)(ii)(D), 164.308(a)(5)(ii)(B), 164.308(a)(5)(ii)(C), 164.308(a)(6)(ii), 164.308(a)(8), 164.310(d)(2)(iii), 164.312(b), 164.314(a)(2)(i)(C), 164.314(a)(2)(iii), 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, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, 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, AU-4(1), CM-6(a), DE.AE-3, DE.AE-5, PR.PT-1, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.5.3, SRG-OS-000479-GPOS-00224, SRG-OS-000342-GPOS-00133, SRG-OS-000051-VMM-000230, SRG-OS-000058-VMM-000270, SRG-OS-000059-VMM-000280, SRG-OS-000479-VMM-001990, SRG-OS-000479-VMM-001990
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    var_syslog_active="yes"

    AUDISP_SYSLOGCONFIG=/etc/audit/plugins.d/syslog.conf

    # Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
    # Otherwise, regular sed command will do.
    sed_command=('sed' '-i')
    if test -L "$AUDISP_SYSLOGCONFIG"; then
        sed_command+=('--follow-symlinks')
    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' <<< "^active")

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

    # 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 "^active\\>" "$AUDISP_SYSLOGCONFIG"; then
        "${sed_command[@]}" "s/^active\\>.*/$formatted_output/gi" "$AUDISP_SYSLOGCONFIG"
    else
        # \n is precaution for case where file ends without trailing newline
        cce="CCE-83695-7"
        printf '\n# Per %s: Set %s in %s\n' "$cce" "$formatted_output" "$AUDISP_SYSLOGCONFIG" >> "$AUDISP_SYSLOGCONFIG"
        printf '%s\n' "$formatted_output" >> "$AUDISP_SYSLOGCONFIG"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | configure
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83695-7
        - CJIS-5.4.1.1
        - NIST-800-171-3.3.1
        - NIST-800-53-AU-4(1)
        - NIST-800-53-CM-6(a)
        - PCI-DSS-Req-10.5.3
        - auditd_audispd_syslog_plugin_activated
        - configure_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed

    - name: enable syslog plugin
      lineinfile:
        dest: /etc/audit/plugins.d/syslog.conf
        regexp: ^active
        line: active = yes
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83695-7
        - CJIS-5.4.1.1
        - NIST-800-171-3.3.1
        - NIST-800-53-AU-4(1)
        - NIST-800-53-CM-6(a)
        - PCI-DSS-Req-10.5.3
        - auditd_audispd_syslog_plugin_activated
        - configure_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
#### Rule Configure auditd flush priority [ref]
The `auditd` service can be configured to synchronously write audit event data to disk. Add or correct the following line in `/etc/audit/auditd.conf` to ensure that audit event data is fully synchronized with the log files on the disk:
    flush = data
Rationale: | Audit data should be synchronously written to disk to ensure log integrity. These parameters assure that all audit event data is fully synchronized with the log files on the disk.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_auditd_data_retention_flush
Identifiers and References | Identifiers: CCE-83685-8 References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.3.1, CCI-001576, 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), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, CIP-004-6 R2.2.3, CIP-004-6 R3.3, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3, CIP-007-3 R6.5, AU-11, CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, SRG-OS-000480-GPOS-00227
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    var_auditd_flush='data'

    AUDITCONFIG=/etc/audit/auditd.conf

    # if flush is present, flush param edited to var_auditd_flush
    # else flush param is defined by var_auditd_flush
    #
    # the freq param is only used for values 'incremental' and 'incremental_async' and will be
    # commented out if flush != incremental or flush != incremental_async
    #
    # if flush == incremental or flush == incremental_async && freq param is not defined, it
    # will be defined as the package-default value of 20

    grep -q ^flush $AUDITCONFIG && \
      sed -i 's/^flush.*/flush = '"$var_auditd_flush"'/g' $AUDITCONFIG
    if ! [ $? -eq 0 ]; then
      echo "flush = $var_auditd_flush" >> $AUDITCONFIG
    fi

    if ! [ "$var_auditd_flush" == "incremental" ] && ! [ "$var_auditd_flush" == "incremental_async" ]; then
      sed -i 's/^freq/##freq/g' $AUDITCONFIG
    elif [ "$var_auditd_flush" == "incremental" ] || [ "$var_auditd_flush" == "incremental_async" ]; then
      grep -q freq $AUDITCONFIG && \
        sed -i 's/^#\+freq/freq/g' $AUDITCONFIG
      if ! [ $? -eq 0 ]; then
        echo "freq = 20" >> $AUDITCONFIG
      fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83685-8
        - NIST-800-171-3.3.1
        - NIST-800-53-AU-11
        - NIST-800-53-CM-6(a)
        - auditd_data_retention_flush
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
    - name: XCCDF Value var_auditd_flush # promote to variable
      set_fact:
        var_auditd_flush: !!str data
      tags:
        - always

    - name: Configure auditd Flush Priority
      lineinfile:
        dest: /etc/audit/auditd.conf
        regexp: ^\s*flush\s*=\s*.*$
        line: flush = {{ var_auditd_flush }}
        state: present
        create: true
      when: '"audit" in ansible_facts.packages'
      tags:
        - CCE-83685-8
        - NIST-800-171-3.3.1
        - NIST-800-53-AU-11
        - NIST-800-53-CM-6(a)
        - auditd_data_retention_flush
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
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
#### Rule Enable auditd Service [ref]
The `auditd` service is an essential userspace component of the Linux Auditing System, as it is responsible for writing audit records to disk. The `auditd` service can be enabled with the following command:
    $ sudo systemctl enable auditd.service
Rationale: | Without establishing what type of events occurred, it would be difficult to establish, correlate, and investigate the events leading up to an outage or attack. Ensuring the `auditd` service is active ensures audit records generated by the kernel are appropriately recorded. Additionally, a properly configured audit subsystem ensures that actions of individual system users can be uniquely traced to those users so they can be held accountable for their actions.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_auditd_enabled
Identifiers and References | Identifiers: CCE-90829-3 References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, 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, 3.3.1, 3.3.2, 3.3.6, CCI-000126, CCI-000130, CCI-000131, CCI-000132, CCI-000133, CCI-000134, CCI-000135, CCI-000154, CCI-000158, CCI-000366, CCI-001464, CCI-001487, CCI-001814, CCI-001876, CCI-002884, CCI-000169, 164.308(a)(1)(ii)(D), 164.308(a)(5)(ii)(C), 164.310(a)(2)(iv), 164.310(d)(2)(iii), 164.312(b), 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, 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, 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, CIP-004-6 R3.3, CIP-007-3 R6.5, AC-2(g), AU-3, AU-10, AU-2(d), AU-12(c), AU-14(1), AC-6(9), CM-6(a), SI-4(23), 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, FAU_GEN.1, Req-10.1, SRG-OS-000037-GPOS-00015, SRG-OS-000038-GPOS-00016, SRG-OS-000039-GPOS-00017, SRG-OS-000040-GPOS-00018, SRG-OS-000041-GPOS-00019, SRG-OS-000042-GPOS-00021, SRG-OS-000051-GPOS-00024, SRG-OS-000054-GPOS-00025, SRG-OS-000122-GPOS-00063, SRG-OS-000254-GPOS-00095, SRG-OS-000255-GPOS-00096, SRG-OS-000365-GPOS-00152, SRG-OS-000392-GPOS-00172, SRG-OS-000480-GPOS-00227, SRG-OS-000062-GPOS-00031, SRG-OS-000037-VMM-000150, SRG-OS-000063-VMM-000310, SRG-OS-000038-VMM-000160, SRG-OS-000039-VMM-000170, SRG-OS-000040-VMM-000180, SRG-OS-000041-VMM-000190
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && { rpm --quiet -q audit; }; then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" unmask 'auditd.service'
    "$SYSTEMCTL_EXEC" start 'auditd.service'
    "$SYSTEMCTL_EXEC" enable 'auditd.service'

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-90829-3
        - CJIS-5.4.1.1
        - NIST-800-171-3.3.1
        - NIST-800-171-3.3.2
        - NIST-800-171-3.3.6
        - NIST-800-53-AC-2(g)
        - NIST-800-53-AC-6(9)
        - NIST-800-53-AU-10
        - NIST-800-53-AU-12(c)
        - NIST-800-53-AU-14(1)
        - NIST-800-53-AU-2(d)
        - NIST-800-53-AU-3
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SI-4(23)
        - PCI-DSS-Req-10.1
        - enable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_auditd_enabled

    - name: Enable service auditd
      block:

        - name: Gather the package facts
          package_facts:
            manager: auto

        - name: Enable service auditd
          service:
            name: auditd
            enabled: 'yes'
            state: started
            masked: 'no'
          when:
            - '"audit" in ansible_facts.packages'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - '"audit" in ansible_facts.packages'
      tags:
        - CCE-90829-3
        - CJIS-5.4.1.1
        - NIST-800-171-3.3.1
        - NIST-800-171-3.3.2
        - NIST-800-171-3.3.6
        - NIST-800-53-AC-2(g)
        - NIST-800-53-AC-6(9)
        - NIST-800-53-AU-10
        - NIST-800-53-AU-12(c)
        - NIST-800-53-AU-14(1)
        - NIST-800-53-AU-2(d)
        - NIST-800-53-AU-3
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SI-4(23)
        - PCI-DSS-Req-10.1
        - enable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_auditd_enabled
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    include enable_auditd

    class enable_auditd {
      service {'auditd':
        enable => true,
        ensure => 'running',
      }
    }
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        systemd:
          units:
          - name: auditd.service
            enabled: true
Remediation script ⇲
    [customizations.services]
    enabled = ["auditd"]
#### Rule Enable Auditing for Processes Which Start Prior to the Audit Daemon [ref]
To ensure all processes can be audited, even those which start prior to the audit daemon, add the argument `audit=1` to the default GRUB 2 command line for the Linux operating system. To ensure that `audit=1` is added as a kernel command line argument to newly installed kernels, ad `audit=1` to the default Grub2 command line for Linux operating systems. Modify the line within `/etc/default/grub` as shown below:
    GRUB_CMDLINE_LINUX="... audit=1 ..."
Run the following command to update command line for already installed kernels:
    # grubby --update-kernel=ALL --args="audit=1"
Rationale: | Each process on the system carries an "auditable" flag which indicates whether its activities can be audited. Although `auditd` takes care of enabling this for all processes which launch after it does, adding the kernel argument ensures it is set for every process during boot.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_grub2_audit_argument
Identifiers and References | Identifiers: CCE-83651-0 References: 1, 11, 12, 13, 14, 15, 16, 19, 3, 4, 5, 6, 7, 8, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS05.02, DSS05.03, DSS05.04, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.3.1, CCI-001464, CCI-000130, CCI-000169, 164.308(a)(1)(ii)(D), 164.308(a)(5)(ii)(C), 164.310(a)(2)(iv), 164.310(d)(2)(iii), 164.312(b), 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, 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 7.1, SR 7.6, 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.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, AC-17(1), AU-14(1), AU-10, CM-6(a), IR-5(1), DE.AE-3, DE.AE-5, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1, Req-10.3, 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-000254-GPOS-00095, SRG-OS-000254-VMM-000880
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && { rpm --quiet -q grub2-common; }; then

    grubby --update-kernel=ALL --args=audit=1

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | medium
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CCE-83651-0
        - CJIS-5.4.1.1
        - NIST-800-171-3.3.1
        - NIST-800-53-AC-17(1)
        - NIST-800-53-AU-10
        - NIST-800-53-AU-14(1)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-IR-5(1)
        - PCI-DSS-Req-10.3
        - grub2_audit_argument
        - low_disruption
        - medium_complexity
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Update grub defaults and the bootloader menu
      command: /sbin/grubby --update-kernel=ALL --args="audit=1"
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - '"grub2-common" in ansible_facts.packages'
      tags:
        - CCE-83651-0
        - CJIS-5.4.1.1
        - NIST-800-171-3.3.1
        - NIST-800-53-AC-17(1)
        - NIST-800-53-AU-10
        - NIST-800-53-AU-14(1)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-IR-5(1)
        - PCI-DSS-Req-10.3
        - grub2_audit_argument
        - low_disruption
        - medium_complexity
        - medium_severity
        - reboot_required
        - restrict_strategy
Remediation script ⇲
    [customizations.kernel]
    append = "audit=1"
Group GRUB2 bootloader configuration Group contains 2 groups and 5 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 4 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
Identifiers and References | Identifiers: CCE-83848-2 References: 12, 13, 14, 15, 16, 18, 3, 5, 5.5.2.2, APO01.06, DSS05.04, DSS05.07, DSS06.02, 3.4.5, CCI-000225, 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), 4.3.3.7.3, SR 2.1, SR 5.2, 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, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, Req-7.1
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | configure
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    chgrp 0 /boot/grub2/grub.cfg

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | configure
    - name: Test for existence /boot/grub2/grub.cfg
      stat:
        path: /boot/grub2/grub.cfg
      register: file_exists
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83848-2
        - 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
        - configure_strategy
        - file_groupowner_grub2_cfg
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed

    - name: Ensure group owner 0 on /boot/grub2/grub.cfg
      file:
        path: /boot/grub2/grub.cfg
        group: '0'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - file_exists.stat is defined and file_exists.stat.exists
      tags:
        - CCE-83848-2
        - 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
        - configure_strategy
        - file_groupowner_grub2_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
Identifiers and References | Identifiers: CCE-83845-8 References: 12, 13, 14, 15, 16, 18, 3, 5, 5.5.2.2, APO01.06, DSS05.04, DSS05.07, DSS06.02, 3.4.5, CCI-000225, 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), 4.3.3.7.3, SR 2.1, SR 5.2, 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, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, Req-7.1
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | configure
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    chown 0 /boot/grub2/grub.cfg

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | configure
    - name: Test for existence /boot/grub2/grub.cfg
      stat:
        path: /boot/grub2/grub.cfg
      register: file_exists
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83845-8
        - 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
        - configure_strategy
        - file_owner_grub2_cfg
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed

    - name: Ensure owner 0 on /boot/grub2/grub.cfg
      file:
        path: /boot/grub2/grub.cfg
        owner: '0'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - file_exists.stat is defined and file_exists.stat.exists
      tags:
        - CCE-83845-8
        - 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
        - configure_strategy
        - file_owner_grub2_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
Identifiers and References | Identifiers: CCE-83846-6 References: 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 3.4.5, CCI-000225, 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), 4.3.3.7.3, SR 2.1, SR 5.2, 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, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | configure
    # Remediation is applicable only in certain platforms
    if [ ! -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
Strategy: | configure
    - name: Test for existence /boot/grub2/grub.cfg
      stat:
        path: /boot/grub2/grub.cfg
      register: file_exists
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83846-6
        - NIST-800-171-3.4.5
        - NIST-800-53-AC-6(1)
        - NIST-800-53-CM-6(a)
        - 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
      file:
        path: /boot/grub2/grub.cfg
        mode: u-xs,g-xwrs,o-xwrt
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - file_exists.stat is defined and file_exists.stat.exists
      tags:
        - CCE-83846-6
        - NIST-800-171-3.4.5
        - NIST-800-53-AC-6(1)
        - NIST-800-53-CM-6(a)
        - configure_strategy
        - file_permissions_grub2_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
Identifiers and References | Identifiers: CCE-83849-0 References: BP28(R17), 1, 11, 12, 14, 15, 16, 18, 3, 5, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.06, DSS06.10, 3.4.5, CCI-000213, 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), 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, 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, 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, CM-6(a), PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.PT-3, FIA_UAU.1, SRG-OS-000080-GPOS-00048
Group UEFI GRUB2 bootloader configuration Group contains 1 rule
[ref] UEFI GRUB2 bootloader configuration
#### Rule Set the UEFI Boot Loader Password [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_uefi_password
Identifiers and References | Identifiers: CCE-88654-9 References: BP28(R17), 11, 12, 14, 15, 16, 18, 3, 5, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.03, DSS06.06, 3.4.5, CCI-000213, 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), 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, 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, 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, CM-6(a), PR.AC-4, PR.AC-6, PR.PT-3, FIA_UAU.1, SRG-OS-000080-GPOS-00048
Group Configure Syslog Group contains 1 group and 1 rule
[ref] The syslog service has been the default Unix logging mechanism for many years. It has a number of downsides, including inconsistent log format, lack of authentication for received messages, and lack of authentication, encryption, or reliable transport for messages sent over a network. However, due to its long history, syslog is a de facto standard which is supported by almost all Unix applications. In Red Hat Enterprise Linux 9, rsyslog has replaced ksyslogd as the syslog daemon of choice, and it includes some additional security features such as reliable, connection-oriented (i.e. TCP) transmission of logs, the option to log to database formats, and the encryption of log data en route to a central logging server. This section discusses how to configure rsyslog for best effect, and how to use tools provided with the system to maintain and monitor logs.
Group Rsyslog Logs Sent To Remote Host Group contains 1 rule
[ref] If system logs are to be useful in detecting malicious activities, it is necessary to send logs to a remote server. An intruder who has compromised the root account on a system may delete the log entries which indicate that the system was attacked before they are seen by an administrator. However, it is recommended that logs be stored on the local host in addition to being sent to the loghost, especially if `rsyslog` has been configured to use the UDP protocol to send messages over a network. UDP does not guarantee reliable delivery, and moderately busy sites will lose log messages occasionally, especially in periods of high traffic which may be the result of an attack. In addition, remote `rsyslog` messages are not authenticated in any way by default, so it is easy for an attacker to introduce spurious messages to the central log server. Also, some problems cause loss of network connectivity, which will prevent the sending of messages to the central server. For all of these reasons, it is better to store log messages both centrally and on each host, so that they can be correlated if necessary.
#### Rule Ensure Logs Sent To Remote Host [ref]
To configure rsyslog to send logs to a remote log server, open `/etc/rsyslog.conf` and read and understand the last section of the file, which describes the multiple directives necessary to activate remote logging. Along with these other directives, the system can be configured to forward its logs to a particular log server by adding or correcting one of the following lines, substituting `logcollector` appropriately. The choice of protocol depends on the environment of the system; although TCP and RELP provide more reliable message delivery, they may not be supported in all environments. To use UDP for log message delivery:
    *.* @logcollector
To use TCP for log message delivery:
    *.* @@logcollector
To use RELP for log message delivery:
    *.* :omrelp:logcollector
There must be a resolvable DNS CNAME or Alias record set to "logcollector" for logs to be sent correctly to the centralized logging utility.
Rationale: | A log server (loghost) receives syslog messages from one or more systems. This data can be used as an additional log source in the event a system is compromised and its local logs are suspect. Forwarding log messages to a remote loghost also provides system administrators with a centralized place to view the status of multiple hosts within the enterprise.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_rsyslog_remote_loghost
Identifiers and References | Identifiers: CCE-83990-2 References: BP28(R7), NT28(R43), NT12(R5), 1, 13, 14, 15, 16, 2, 3, 5, 6, APO11.04, APO13.01, BAI03.05, BAI04.04, DSS05.04, DSS05.07, MEA02.01, CCI-000366, CCI-001348, CCI-000136, CCI-001851, 164.308(a)(1)(ii)(D), 164.308(a)(5)(ii)(B), 164.308(a)(5)(ii)(C), 164.308(a)(6)(ii), 164.308(a)(8), 164.310(d)(2)(iii), 164.312(b), 164.314(a)(2)(i)(C), 164.314(a)(2)(iii), 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 7.1, SR 7.2, 0988, 1405, 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.17.2.1, CIP-003-8 R5.2, CIP-004-6 R3.3, CM-6(a), AU-4(1), AU-9(2), PR.DS-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000479-GPOS-00224, SRG-OS-000480-GPOS-00227, SRG-OS-000342-GPOS-00133, SRG-OS-000032-VMM-000130
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    rsyslog_remote_loghost_address='logcollector'

    # Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
    # Otherwise, regular sed command will do.
    sed_command=('sed' '-i')
    if test -L "/etc/rsyslog.conf"; then
        sed_command+=('--follow-symlinks')
    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' <<< "^\*\.\*")

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

    # 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 "^\*\.\*\\>" "/etc/rsyslog.conf"; then
        "${sed_command[@]}" "s/^\*\.\*\\>.*/$formatted_output/gi" "/etc/rsyslog.conf"
    else
        # \n is precaution for case where file ends without trailing newline
        cce="CCE-83990-2"
        printf '\n# Per %s: Set %s in %s\n' "$cce" "$formatted_output" "/etc/rsyslog.conf" >> "/etc/rsyslog.conf"
        printf '%s\n' "$formatted_output" >> "/etc/rsyslog.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: XCCDF Value rsyslog_remote_loghost_address # promote to variable
      set_fact:
        rsyslog_remote_loghost_address: !!str logcollector
      tags:
        - always

    - name: Set rsyslog remote loghost
      lineinfile:
        dest: /etc/rsyslog.conf
        regexp: ^\*\.\*
        line: '*.* @@{{ rsyslog_remote_loghost_address }}'
        create: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83990-2
        - NIST-800-53-AU-4(1)
        - NIST-800-53-AU-9(2)
        - NIST-800-53-CM-6(a)
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
        - rsyslog_remote_loghost
Group Network Configuration and Firewalls Group contains 1 group and 1 rule
[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 IPSec Support Group contains 1 rule
[ref] Support for Internet Protocol Security (IPsec) is provided with Libreswan.
#### Rule Verify Any Configured IPSec Tunnel Connections [ref]
Libreswan provides an implementation of IPsec and IKE, which permits the creation of secure tunnels over untrusted networks. As such, IPsec can be used to circumvent certain network requirements such as filtering. Verify that if any IPsec connection (`conn`) configured in `/etc/ipsec.conf` and `/etc/ipsec.d` exists is an approved organizational connection.
Rationale: | IP tunneling mechanisms can be used to bypass network filtering.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_libreswan_approved_tunnels
Identifiers and References | Identifiers: CCE-90319-5 References: 1, 12, 13, 14, 15, 16, 18, 4, 6, 8, 9, APO01.06, APO13.01, DSS01.05, DSS03.01, DSS05.02, DSS05.04, DSS05.07, DSS06.02, CCI-000336, 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), 4.2.3.4, 4.3.3.4, 4.4.3.3, 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, 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, AC-17(a), MA-4(6), CM-6(a), AC-4, SC-8, DE.AE-1, ID.AM-3, PR.AC-5, PR.DS-5, PR.PT-4, SRG-OS-000480-GPOS-00227
Group File Permissions and Masks Group contains 4 groups and 6 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 Dynamic Mounting and Unmounting of Filesystems Group contains 2 rules
[ref] Linux includes a number of facilities for the automated addition and removal of filesystems on a running system. These facilities may be necessary in many environments, but this capability also carries some risk -- whether direct risk from allowing users to introduce arbitrary filesystems, or risk that software flaws in the automated mount facility itself could allow an attacker to compromise the system. This command can be used to list the types of filesystems that are available to the currently executing kernel:
    $ find /lib/modules/`uname -r`/kernel/fs -type f -name '*.ko'
If these filesystems are not required then they can be explicitly disabled in a configuratio file in `/etc/modprobe.d`.
#### Rule Disable the Automounter [ref]
The `autofs` daemon mounts and unmounts filesystems, such as user home directories shared via NFS, on demand. In addition, autofs can be used to handle removable media, and the default configuration provides the cdrom device as `/misc/cd`. However, this method of providing access to removable media is not common, so autofs can almost always be disabled if NFS is not in use. Even if NFS is required, it may be possible to configure filesystem mounts statically by editing `/etc/fstab` rather than relying on the automounter. The `autofs` service can be disabled with the following command:
    $ sudo systemctl mask --now autofs.service
Rationale: | Disabling the automounter permits the administrator to statically control filesystem mounting through `/etc/fstab`. Additionally, automatically mounting filesystems permits easy introduction of unknown devices, thereby facilitating malicious activity.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_autofs_disabled
Identifiers and References | Identifiers: CCE-83850-8 References: 1, 12, 15, 16, 5, APO13.01, DSS01.04, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.4.6, CCI-000366, CCI-000778, CCI-001958, 164.308(a)(3)(i), 164.308(a)(3)(ii)(A), 164.310(d)(1), 164.310(d)(2), 164.312(a)(1), 164.312(a)(2)(iv), 164.312(b), 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, SR 1.1, SR 1.10, 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.6, A.11.2.6, A.13.1.1, A.13.2.1, A.18.1.4, A.6.2.1, A.6.2.2, 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, CM-7(a), CM-7(b), CM-6(a), MP-7, PR.AC-1, PR.AC-3, PR.AC-6, PR.AC-7, SRG-OS-000114-GPOS-00059, SRG-OS-000378-GPOS-00163, SRG-OS-000480-GPOS-00227
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" stop 'autofs.service'
    "$SYSTEMCTL_EXEC" disable 'autofs.service'
    "$SYSTEMCTL_EXEC" mask 'autofs.service'
    # Disable socket activation if we have a unit file for it
    if "$SYSTEMCTL_EXEC" list-unit-files | grep -q '^autofs.socket'; then
        "$SYSTEMCTL_EXEC" stop 'autofs.socket'
        "$SYSTEMCTL_EXEC" mask 'autofs.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 'autofs.service' || true

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    - name: Disable service autofs
      block:

        - name: Disable service autofs
          systemd:
            name: autofs.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
          ignore_errors: 'yes'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83850-8
        - NIST-800-171-3.4.6
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-MP-7
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_autofs_disabled

    - name: Unit Socket Exists - autofs.socket
      command: systemctl list-unit-files autofs.socket
      args:
        warn: false
      register: socket_file_exists
      changed_when: false
      ignore_errors: true
      check_mode: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83850-8
        - NIST-800-171-3.4.6
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-MP-7
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_autofs_disabled

    - name: Disable socket autofs
      systemd:
        name: autofs.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - '"autofs.socket" in socket_file_exists.stdout_lines[1]'
      tags:
        - CCE-83850-8
        - NIST-800-171-3.4.6
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-MP-7
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_autofs_disabled
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    include disable_autofs

    class disable_autofs {
      service {'autofs':
        enable => false,
        ensure => 'stopped',
      }
    }
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        systemd:
          units:
          - enabled: false
            name: autofs.service
Remediation script ⇲
    [customizations.services]
    disabled = ["autofs"]
#### Rule Disable Modprobe Loading of USB Storage Driver [ref]
To prevent USB storage devices from being used, configure the kernel module loading system to prevent automatic loading of the USB storage driver. To configure the system to prevent the `usb-storage` kernel module from being loaded, add the following line to a file in the directory `/etc/modprobe.d`:
    install usb-storage /bin/true
This will prevent the `modprobe` program from loading the `usb-storage` module, but will not prevent an administrator (or another program) from using the `insmod` program to load the module manually.
Rationale: | USB storage devices such as thumb drives can be used to introduce malicious software.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_kernel_module_usb-storage_disabled
Identifiers and References | Identifiers: CCE-83851-6 References: 1, 12, 15, 16, 5, APO13.01, DSS01.04, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.1.21, CCI-000366, CCI-000778, CCI-001958, 164.308(a)(3)(i), 164.308(a)(3)(ii)(A), 164.310(d)(1), 164.310(d)(2), 164.312(a)(1), 164.312(a)(2)(iv), 164.312(b), 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, SR 1.1, SR 1.10, 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.6, A.11.2.6, A.13.1.1, A.13.2.1, A.18.1.4, A.6.2.1, A.6.2.2, 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, CM-7(a), CM-7(b), CM-6(a), MP-7, PR.AC-1, PR.AC-3, PR.AC-6, PR.AC-7, SRG-OS-000114-GPOS-00059, SRG-OS-000378-GPOS-00163, SRG-OS-000480-GPOS-00227
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if LC_ALL=C grep -q -m 1 "^install usb-storage" /etc/modprobe.d/usb-storage.conf ; then

    	sed -i 's#^install usb-storage.*#install usb-storage /bin/true#g' /etc/modprobe.d/usb-storage.conf
    else
    	echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/usb-storage.conf
    	echo "install usb-storage /bin/true" >> /etc/modprobe.d/usb-storage.conf
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: Ensure kernel module 'usb-storage' is disabled
      lineinfile:
        create: true
        dest: /etc/modprobe.d/usb-storage.conf
        regexp: usb-storage
        line: install usb-storage /bin/true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83851-6
        - NIST-800-171-3.1.21
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-MP-7
        - disable_strategy
        - kernel_module_usb-storage_disabled
        - low_complexity
        - medium_disruption
        - medium_severity
        - reboot_required
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,install%20usb-storage%20/bin/true%0A
            mode: 0644
            path: /etc/modprobe.d/75-kernel_module_usb-storage_disabled.conf
            overwrite: true
Group Restrict Programs from Dangerous Execution Patterns Group contains 2 groups and 4 rules
[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
Identifiers and References | Identifiers: CCE-83981-1 References: BP28(R23), 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), SI-11(a), SI-11(b)
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

    #
    # Set runtime for fs.suid_dumpable
    #
    /sbin/sysctl -q -n -w fs.suid_dumpable="0"

    #
    # If fs.suid_dumpable present in /etc/sysctl.conf, change value to "0"
    #	else, add "fs.suid_dumpable = 0" to /etc/sysctl.conf
    #
    # Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
    # Otherwise, regular sed command will do.
    sed_command=('sed' '-i')
    if test -L "/etc/sysctl.conf"; then
        sed_command+=('--follow-symlinks')
    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' <<< "^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\\>" "/etc/sysctl.conf"; then
        "${sed_command[@]}" "s/^fs.suid_dumpable\\>.*/$formatted_output/gi" "/etc/sysctl.conf"
    else
        # \n is precaution for case where file ends without trailing newline
        cce="CCE-83981-1"
        printf '\n# Per %s: Set %s in %s\n' "$cce" "$formatted_output" "/etc/sysctl.conf" >> "/etc/sysctl.conf"
        printf '%s\n' "$formatted_output" >> "/etc/sysctl.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: List /etc/sysctl.d/*.conf files
      find:
        paths: /etc/sysctl.d/
        contains: ^[\s]*fs.suid_dumpable.*$
        patterns: '*.conf'
      register: find_sysctl_d
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83981-1
        - NIST-800-53-SI-11(a)
        - NIST-800-53-SI-11(b)
        - disable_strategy
        - low_complexity
        - medium_disruption
        - medium_severity
        - reboot_required
        - sysctl_fs_suid_dumpable

    - name: Comment out any occurrences of fs.suid_dumpable from /etc/sysctl.d/*.conf
        files
      replace:
        path: '{{ item }}'
        regexp: ^[\s]*fs.suid_dumpable
        replace: '#fs.suid_dumpable'
      loop: '{{ find_sysctl_d.files }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83981-1
        - NIST-800-53-SI-11(a)
        - NIST-800-53-SI-11(b)
        - disable_strategy
        - low_complexity
        - medium_disruption
        - medium_severity
        - reboot_required
        - sysctl_fs_suid_dumpable

    - name: Ensure sysctl fs.suid_dumpable is set to 0
      sysctl:
        name: fs.suid_dumpable
        value: '0'
        state: present
        reload: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83981-1
        - NIST-800-53-SI-11(a)
        - NIST-800-53-SI-11(b)
        - disable_strategy
        - low_complexity
        - medium_disruption
        - medium_severity
        - reboot_required
        - sysctl_fs_suid_dumpable
Group Enable ExecShield Group contains 2 rules
[ref] ExecShield describes kernel features that provide protection against exploitation of memory corruption errors such as buffer overflows. These features include random placement of the stack and other memory regions, prevention of execution in memory that should only hold data, and special handling of text buffers. These protections are enabled by default on 32-bit systems and controlled through `sysctl` variables `kernel.exec-shield` and `kernel.randomize_va_space`. On the latest 64-bit systems, `kernel.exec-shield` cannot be enabled or disabled with `sysctl`.
#### Rule Enable ExecShield via sysctl [ref]
By default on Red Hat Enterprise Linux 7 64-bit systems, ExecShield is enabled and can only be disabled if the hardware does not support ExecShield or is disabled in `/etc/default/grub`. For Red Hat Enterprise Linux 7 32-bit systems, `sysctl` can be used to enable ExecShield.
Rationale: | ExecShield uses the segmentation feature on all x86 systems to prevent execution in memory higher than a certain address. It writes an address as a limit in the code segment descriptor, to control where code can be executed, on a per-process basis. When the kernel places a process's memory regions such as the stack and heap higher than this address, the hardware prevents execution in that address range. This is enabled by default on the latest Red Hat and Fedora systems if supported by the hardware.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_kernel_exec_shield
Identifiers and References | Identifiers: CCE-83970-4 References: BP28(R9), 12, 15, 8, APO13.01, DSS05.02, 3.1.7, CCI-002530, 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), 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, A.13.1.1, A.13.2.1, A.14.1.3, SC-39, CM-6(a), PR.PT-4, SRG-OS-000433-GPOS-00192
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if [ "$(getconf LONG_BIT)" = "32" ] ; then
      #
      # Set runtime for kernel.exec-shield
      #
      sysctl -q -n -w kernel.exec-shield=1

      #
      # If kernel.exec-shield present in /etc/sysctl.conf, change value to "1"
      #	else, add "kernel.exec-shield = 1" to /etc/sysctl.conf
      #
      # Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
    # Otherwise, regular sed command will do.
    sed_command=('sed' '-i')
    if test -L "/etc/sysctl.conf"; then
        sed_command+=('--follow-symlinks')
    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' <<< "^kernel.exec-shield")

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

    # 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 "^kernel.exec-shield\\>" "/etc/sysctl.conf"; then
        "${sed_command[@]}" "s/^kernel.exec-shield\\>.*/$formatted_output/gi" "/etc/sysctl.conf"
    else
        # \n is precaution for case where file ends without trailing newline
        cce="CCE-83970-4"
        printf '\n# Per %s: Set %s in %s\n' "$cce" "$formatted_output" "/etc/sysctl.conf" >> "/etc/sysctl.conf"
        printf '%s\n' "$formatted_output" >> "/etc/sysctl.conf"
    fi
    fi

    if [ "$(getconf LONG_BIT)" = "64" ] ; then
      if grep --silent noexec /boot/grub2/grub*.cfg ; then
            sed -i "s/noexec.*//g" /etc/default/grub
            sed -i "s/noexec.*//g" /etc/grub.d/*
            grub2-mkconfig -o "/boot/grub2"/grub*.cfg
      fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
#### Rule Enable Randomized Layout of Virtual Address Space [ref]
To set the runtime status of the `kernel.randomize_va_space` kernel parameter, run the following command:
    $ sudo sysctl -w kernel.randomize_va_space=2
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    kernel.randomize_va_space = 2
Rationale: | Address space layout randomization (ASLR) makes it more difficult for an attacker to predict the location of attack code they have introduced into a process's address space during an attempt at exploitation. Additionally, ASLR makes it more difficult for an attacker to know the location of existing code in order to re-purpose it using return oriented programming (ROP) techniques.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_kernel_randomize_va_space
Identifiers and References | Identifiers: CCE-83971-2 References: BP28(R23), 3.1.7, CCI-000366, CCI-002824, 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), CIP-002-5 R1.1, CIP-002-5 R1.2, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 4.1, CIP-004-6 4.2, CIP-004-6 R2.2.3, CIP-004-6 R2.2.4, CIP-004-6 R2.3, CIP-004-6 R4, CIP-005-6 R1, CIP-005-6 R1.1, CIP-005-6 R1.2, CIP-007-3 R3, CIP-007-3 R3.1, CIP-007-3 R5.1, CIP-007-3 R5.1.2, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, CIP-007-3 R8.4, CIP-009-6 R.1.1, CIP-009-6 R4, SC-30, SC-30(2), CM-6(a), SRG-OS-000433-GPOS-00193, SRG-OS-000480-GPOS-00227
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

    #
    # Set runtime for kernel.randomize_va_space
    #
    /sbin/sysctl -q -n -w kernel.randomize_va_space="2"

    #
    # If kernel.randomize_va_space present in /etc/sysctl.conf, change value to "2"
    #	else, add "kernel.randomize_va_space = 2" to /etc/sysctl.conf
    #
    # Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
    # Otherwise, regular sed command will do.
    sed_command=('sed' '-i')
    if test -L "/etc/sysctl.conf"; then
        sed_command+=('--follow-symlinks')
    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' <<< "^kernel.randomize_va_space")

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

    # 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 "^kernel.randomize_va_space\\>" "/etc/sysctl.conf"; then
        "${sed_command[@]}" "s/^kernel.randomize_va_space\\>.*/$formatted_output/gi" "/etc/sysctl.conf"
    else
        # \n is precaution for case where file ends without trailing newline
        cce="CCE-83971-2"
        printf '\n# Per %s: Set %s in %s\n' "$cce" "$formatted_output" "/etc/sysctl.conf" >> "/etc/sysctl.conf"
        printf '%s\n' "$formatted_output" >> "/etc/sysctl.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: List /etc/sysctl.d/*.conf files
      find:
        paths: /etc/sysctl.d/
        contains: ^[\s]*kernel.randomize_va_space.*$
        patterns: '*.conf'
      register: find_sysctl_d
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83971-2
        - NIST-800-171-3.1.7
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SC-30
        - NIST-800-53-SC-30(2)
        - disable_strategy
        - low_complexity
        - medium_disruption
        - medium_severity
        - reboot_required
        - sysctl_kernel_randomize_va_space

    - name: Comment out any occurrences of kernel.randomize_va_space from /etc/sysctl.d/*.conf
        files
      replace:
        path: '{{ item }}'
        regexp: ^[\s]*kernel.randomize_va_space
        replace: '#kernel.randomize_va_space'
      loop: '{{ find_sysctl_d.files }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83971-2
        - NIST-800-171-3.1.7
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SC-30
        - NIST-800-53-SC-30(2)
        - disable_strategy
        - low_complexity
        - medium_disruption
        - medium_severity
        - reboot_required
        - sysctl_kernel_randomize_va_space

    - name: Ensure sysctl kernel.randomize_va_space is set to 2
      sysctl:
        name: kernel.randomize_va_space
        value: '2'
        state: present
        reload: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83971-2
        - NIST-800-171-3.1.7
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SC-30
        - NIST-800-53-SC-30(2)
        - disable_strategy
        - low_complexity
        - medium_disruption
        - medium_severity
        - reboot_required
        - sysctl_kernel_randomize_va_space
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,kernel.randomize_va_space%3D2%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_kernel_randomize_va_space.conf
            overwrite: true
#### Rule Restrict Access to Kernel Message Buffer [ref]
To set the runtime status of the `kernel.dmesg_restrict` kernel parameter, run the following command:
    $ sudo sysctl -w kernel.dmesg_restrict=1
To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:
    kernel.dmesg_restrict = 1
Rationale: | Unprivileged access to the kernel syslog can expose sensitive kernel address information.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_kernel_dmesg_restrict
Identifiers and References | Identifiers: CCE-83952-2 References: BP28(R23), 3.1.5, CCI-001314, 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), SI-11(a), SI-11(b), SRG-OS-000132-GPOS-00067, SRG-OS-000138-GPOS-00069
Remediation Shell script ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

    #
    # Set runtime for kernel.dmesg_restrict
    #
    /sbin/sysctl -q -n -w kernel.dmesg_restrict="1"

    #
    # If kernel.dmesg_restrict present in /etc/sysctl.conf, change value to "1"
    #	else, add "kernel.dmesg_restrict = 1" to /etc/sysctl.conf
    #
    # Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
    # Otherwise, regular sed command will do.
    sed_command=('sed' '-i')
    if test -L "/etc/sysctl.conf"; then
        sed_command+=('--follow-symlinks')
    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' <<< "^kernel.dmesg_restrict")

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

    # 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 "^kernel.dmesg_restrict\\>" "/etc/sysctl.conf"; then
        "${sed_command[@]}" "s/^kernel.dmesg_restrict\\>.*/$formatted_output/gi" "/etc/sysctl.conf"
    else
        # \n is precaution for case where file ends without trailing newline
        cce="CCE-83952-2"
        printf '\n# Per %s: Set %s in %s\n' "$cce" "$formatted_output" "/etc/sysctl.conf" >> "/etc/sysctl.conf"
        printf '%s\n' "$formatted_output" >> "/etc/sysctl.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable
    - name: List /etc/sysctl.d/*.conf files
      find:
        paths: /etc/sysctl.d/
        contains: ^[\s]*kernel.dmesg_restrict.*$
        patterns: '*.conf'
      register: find_sysctl_d
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83952-2
        - NIST-800-171-3.1.5
        - NIST-800-53-SI-11(a)
        - NIST-800-53-SI-11(b)
        - disable_strategy
        - low_complexity
        - low_severity
        - medium_disruption
        - reboot_required
        - sysctl_kernel_dmesg_restrict

    - name: Comment out any occurrences of kernel.dmesg_restrict from /etc/sysctl.d/*.conf
        files
      replace:
        path: '{{ item }}'
        regexp: ^[\s]*kernel.dmesg_restrict
        replace: '#kernel.dmesg_restrict'
      loop: '{{ find_sysctl_d.files }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83952-2
        - NIST-800-171-3.1.5
        - NIST-800-53-SI-11(a)
        - NIST-800-53-SI-11(b)
        - disable_strategy
        - low_complexity
        - low_severity
        - medium_disruption
        - reboot_required
        - sysctl_kernel_dmesg_restrict

    - name: Ensure sysctl kernel.dmesg_restrict is set to 1
      sysctl:
        name: kernel.dmesg_restrict
        value: '1'
        state: present
        reload: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-83952-2
        - NIST-800-171-3.1.5
        - NIST-800-53-SI-11(a)
        - NIST-800-53-SI-11(b)
        - disable_strategy
        - low_complexity
        - low_severity
        - medium_disruption
        - reboot_required
        - sysctl_kernel_dmesg_restrict
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,kernel.dmesg_restrict%3D1%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_kernel_dmesg_restrict.conf
            overwrite: true
Group SELinux Group contains 1 group and 7 rules
[ref] SELinux is a feature of the Linux kernel which can be used to guard against misconfigured or compromised programs. SELinux enforces the idea that programs should be limited in what files they can access and what actions they can take. The default SELinux policy, as configured on Red Hat Enterprise Linux 9, has been sufficiently developed and debugged that it should be usable on almost any system with minimal configuration and a small amount of system administrator training. This policy prevents system services - including most of the common network-visible services such as mail servers, FTP servers, and DNS servers - from accessing files which those services have no valid reason to access. This action alone prevents a huge amount of possible damage from network attacks against services, from trojaned software, and so forth. This guide recommends that SELinux be enabled using the default (targeted) policy on every Red Hat Enterprise Linux 9 system, unless that system has unusual requirements which make a stronger policy appropriate.
Group SELinux - Booleans Group contains 3 rules
[ref] Enable or Disable runtime customization of SELinux system policies without having to reload or recompile the SELinux policy.
#### Rule Disable the selinuxuser_execheap SELinux Boolean [ref]
By default, the SELinux boolean `selinuxuser_execheap` is disabled. When enabled this boolean is enabled it allows selinuxusers to execute code from the heap. If this setting is enabled, it should be disabled. To disable the `selinuxuser_execheap` SELinux boolean, run the following command:
    $ sudo setsebool -P selinuxuser_execheap off
Rationale: | Disabling code execution from the heap blocks buffer overflow attacks.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sebool_selinuxuser_execheap
Identifiers and References | Identifiers: CCE-84084-3 References: BP28(R67), 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)
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    var_selinuxuser_execheap='false'

    setsebool -P selinuxuser_execheap $var_selinuxuser_execheap

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    - name: XCCDF Value var_selinuxuser_execheap # promote to variable
      set_fact:
        var_selinuxuser_execheap: !!str false
      tags:
        - always

    - name: Ensure python3-libsemanage installed
      package:
        name: python3-libsemanage
        state: present
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84084-3
        - enable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - sebool_selinuxuser_execheap

    - name: Set SELinux boolean selinuxuser_execheap accordingly
      seboolean:
        name: selinuxuser_execheap
        state: '{{ var_selinuxuser_execheap }}'
        persistent: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84084-3
        - enable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - sebool_selinuxuser_execheap
#### Rule Enable the selinuxuser_execmod SELinux Boolean [ref]
By default, the SELinux boolean `selinuxuser_execmod` is enabled. If this setting is disabled, it should be enabled. To enable the `selinuxuser_execmod` SELinux boolean, run the following command:
    $ sudo setsebool -P selinuxuser_execmod on
Rationale: |
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sebool_selinuxuser_execmod
Identifiers and References | Identifiers: CCE-84086-8 References: 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)
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    var_selinuxuser_execmod='true'

    setsebool -P selinuxuser_execmod $var_selinuxuser_execmod

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    - name: XCCDF Value var_selinuxuser_execmod # promote to variable
      set_fact:
        var_selinuxuser_execmod: !!str true
      tags:
        - always

    - name: Ensure python3-libsemanage installed
      package:
        name: python3-libsemanage
        state: present
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84086-8
        - enable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - sebool_selinuxuser_execmod

    - name: Set SELinux boolean selinuxuser_execmod accordingly
      seboolean:
        name: selinuxuser_execmod
        state: '{{ var_selinuxuser_execmod }}'
        persistent: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84086-8
        - enable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - sebool_selinuxuser_execmod
#### Rule disable the selinuxuser_execstack SELinux Boolean [ref]
By default, the SELinux boolean `selinuxuser_execstack` is enabled. This setting should be disabled as unconfined executables should not be able to make their stack executable. To disable the `selinuxuser_execstack` SELinux boolean, run the following command:
    $ sudo setsebool -P selinuxuser_execstack off
Rationale: | Disabling code execution from the stack blocks buffer overflow attacks.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sebool_selinuxuser_execstack
Identifiers and References | Identifiers: CCE-84089-2 References: BP28(R67), 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)
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    var_selinuxuser_execstack='false'

    setsebool -P selinuxuser_execstack $var_selinuxuser_execstack

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    - name: XCCDF Value var_selinuxuser_execstack # promote to variable
      set_fact:
        var_selinuxuser_execstack: !!str false
      tags:
        - always

    - name: Ensure python3-libsemanage installed
      package:
        name: python3-libsemanage
        state: present
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84089-2
        - enable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - sebool_selinuxuser_execstack

    - name: Set SELinux boolean selinuxuser_execstack accordingly
      seboolean:
        name: selinuxuser_execstack
        state: '{{ var_selinuxuser_execstack }}'
        persistent: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84089-2
        - enable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - sebool_selinuxuser_execstack
#### Rule Ensure SELinux Not Disabled in /etc/default/grub [ref]
SELinux can be disabled at boot time by an argument in `/etc/default/grub`. Remove any instances of `selinux=0` from the kernel arguments in that file to prevent SELinux from being disabled at boot.
Rationale: | Disabling a major host protection feature, such as SELinux, at boot time prevents it from confining system services at boot time. Further, it increases the chances that it will remain off during system operation.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_grub2_enable_selinux
Identifiers and References | Identifiers: CCE-84078-5 References: 1, 11, 12, 13, 14, 15, 16, 18, 3, 4, 5, 6, 8, 9, APO01.06, APO11.04, APO13.01, BAI03.05, DSS01.05, DSS03.01, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.03, DSS06.06, MEA02.01, 3.1.2, 3.7.2, CCI-000022, CCI-000032, 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), 4.2.3.4, 4.3.3.2.2, 4.3.3.3.9, 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.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, 4.4.3.3, 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.10, SR 2.11, SR 2.12, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, 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 7.1, SR 7.6, 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.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, 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.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, 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, AC-3, AC-3(3)(a), DE.AE-1, ID.AM-3, PR.AC-4, PR.AC-5, PR.AC-6, PR.DS-5, PR.PT-1, PR.PT-3, PR.PT-4, SRG-OS-000445-VMM-001780
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    sed -i --follow-symlinks "s/selinux=0//gI" /etc/default/grub /etc/grub2.cfg /etc/grub.d/*
    sed -i --follow-symlinks "s/enforcing=0//gI" /etc/default/grub /etc/grub2.cfg /etc/grub.d/*

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Find /etc/grub.d/ files
      find:
        paths:
          - /etc/grub.d/
        follow: true
      register: grub
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84078-5
        - NIST-800-171-3.1.2
        - NIST-800-171-3.7.2
        - NIST-800-53-AC-3
        - NIST-800-53-AC-3(3)(a)
        - grub2_enable_selinux
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Ensure SELinux Not Disabled in grub files
      replace:
        dest: '{{ item.path }}'
        regexp: (selinux|enforcing)=0
      with_items:
        - '{{ grub.files }}'
        - path: /etc/grub2.cfg
        - path: /etc/default/grub
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84078-5
        - NIST-800-171-3.1.2
        - NIST-800-171-3.7.2
        - NIST-800-53-AC-3
        - NIST-800-53-AC-3(3)(a)
        - grub2_enable_selinux
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure No Daemons are Unconfined by SELinux [ref]
Daemons for which the SELinux policy does not contain rules will inherit the context of the parent process. Because daemons are launched during startup and descend from the `init` process, they inherit the `unconfined_service_t` context. To check for unconfined daemons, run the following command:
    $ sudo ps -eZ | grep "unconfined_service_t"
It should produce no output in a well-configured system. Warning: Automatic remediation of this control is not available. Remediation can be achieved by amending SELinux policy or stopping the unconfined daemons as outlined above.
Rationale: | Daemons which run with the `unconfined_service_t` context may cause AVC denials, or allow privileges that the daemon does not require.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_selinux_confinement_of_daemons
Identifiers and References | Identifiers: CCE-84075-1 References: 1, 11, 12, 13, 14, 15, 16, 18, 3, 5, 6, 9, APO01.06, APO11.04, BAI03.05, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06, MEA02.01, 3.1.2, 3.1.5, 3.7.2, 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), 4.3.3.3.9, 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.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, 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.10, SR 2.11, SR 2.12, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 2.8, SR 2.9, SR 5.2, SR 7.6, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.5.1, A.12.6.2, A.12.7.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.14.2.2, A.14.2.3, A.14.2.4, 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, 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, CM-7(a), CM-7(b), CM-6(a), AC-3(3)(a), AC-6, PR.AC-4, PR.DS-5, PR.IP-1, PR.PT-1, PR.PT-3
#### Rule Configure SELinux Policy [ref]
The SELinux `targeted` policy is appropriate for general-purpose desktops and servers, as well as systems in many other roles. To configure the system to use this policy, add or correct the following line in `/etc/selinux/config`:
    SELINUXTYPE=targeted
Other policies, such as `mls`, provide additional security labeling and greater confinement but are not compatible with many general-purpose use cases.
Rationale: | Setting the SELinux policy to `targeted` or a more specialized policy ensures the system will confine processes that are likely to be targeted for exploitation, such as network or system services. Note: During the development or debugging of SELinux modules, it is common to temporarily place non-production systems in `permissive` mode. In such temporary cases, SELinux policies should be developed, and once work is completed, the system should be reconfigured to `targeted`.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_selinux_policytype
Identifiers and References | Identifiers: CCE-84074-4 References: BP28(R66), 1, 11, 12, 13, 14, 15, 16, 18, 3, 4, 5, 6, 8, 9, APO01.06, APO11.04, APO13.01, BAI03.05, DSS01.05, DSS03.01, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.03, DSS06.06, MEA02.01, 3.1.2, 3.7.2, CCI-002165, CCI-002696, 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), 4.2.3.4, 4.3.3.2.2, 4.3.3.3.9, 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.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, 4.4.3.3, 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.10, SR 2.11, SR 2.12, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, 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 7.1, SR 7.6, 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.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, 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.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.2, CIP-003-8 R5.3, CIP-004-6 R2.2.3, CIP-004-6 R2.3, CIP-004-6 R3.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, CIP-007-3 R6.5, AC-3, AC-3(3)(a), AU-9, SC-7(21), DE.AE-1, ID.AM-3, PR.AC-4, PR.AC-5, PR.AC-6, PR.DS-5, PR.PT-1, PR.PT-3, PR.PT-4, SRG-OS-000445-GPOS-00199, SRG-OS-000445-VMM-001780
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    var_selinux_policy_name='targeted'

    if [ -e "/etc/selinux/config" ] ; then

        LC_ALL=C sed -i "/^SELINUXTYPE=/Id" "/etc/selinux/config"
    else
        touch "/etc/selinux/config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/selinux/config"

    cp "/etc/selinux/config" "/etc/selinux/config.bak"
    # Insert at the end of the file
    printf '%s\n' "SELINUXTYPE=$var_selinux_policy_name" >> "/etc/selinux/config"
    # Clean up after ourselves.
    rm "/etc/selinux/config.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: XCCDF Value var_selinux_policy_name # promote to variable
      set_fact:
        var_selinux_policy_name: !!str targeted
      tags:
        - always

    - name: Configure SELinux Policy
      block:

        - name: Check for duplicate values
          lineinfile:
            path: /etc/selinux/config
            create: false
            regexp: ^SELINUXTYPE=
            state: absent
          check_mode: true
          changed_when: false
          register: dupes

        - name: Deduplicate values from /etc/selinux/config
          lineinfile:
            path: /etc/selinux/config
            create: false
            regexp: ^SELINUXTYPE=
            state: absent
          when: dupes.found is defined and dupes.found > 1

        - name: Insert correct line to /etc/selinux/config
          lineinfile:
            path: /etc/selinux/config
            create: true
            regexp: ^SELINUXTYPE=
            line: SELINUXTYPE={{ var_selinux_policy_name }}
            state: present
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84074-4
        - NIST-800-171-3.1.2
        - NIST-800-171-3.7.2
        - NIST-800-53-AC-3
        - NIST-800-53-AC-3(3)(a)
        - NIST-800-53-AU-9
        - NIST-800-53-SC-7(21)
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
        - selinux_policytype
#### Rule Ensure SELinux State is Enforcing [ref]
The SELinux state should be set to `enforcing` at system boot time. In the file `/etc/selinux/config`, add or correct the following line to configure the system to boot into enforcing mode:
    SELINUX=enforcing
Rationale: | Setting the SELinux state to enforcing ensures SELinux is able to confine potentially compromised processes to the security policy, which is designed to prevent them from causing damage to the system or further elevating their privileges.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_selinux_state
Identifiers and References | Identifiers: CCE-84079-3 References: BP28(R4), BP28(R66), 1, 11, 12, 13, 14, 15, 16, 18, 3, 4, 5, 6, 8, 9, APO01.06, APO11.04, APO13.01, BAI03.05, DSS01.05, DSS03.01, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.03, DSS06.06, MEA02.01, 3.1.2, 3.7.2, CCI-002165, CCI-002696, 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), 4.2.3.4, 4.3.3.2.2, 4.3.3.3.9, 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.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, 4.4.3.3, 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.10, SR 2.11, SR 2.12, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, 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 7.1, SR 7.6, 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.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, 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.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.2, CIP-003-8 R5.3, CIP-004-6 R2.2.3, CIP-004-6 R2.3, CIP-004-6 R3.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, CIP-007-3 R6.5, AC-3, AC-3(3)(a), AU-9, SC-7(21), DE.AE-1, ID.AM-3, PR.AC-4, PR.AC-5, PR.AC-6, PR.DS-5, PR.PT-1, PR.PT-3, PR.PT-4, SRG-OS-000445-GPOS-00199, SRG-OS-000445-VMM-001780
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    var_selinux_state='enforcing'

    if [ -e "/etc/selinux/config" ] ; then

        LC_ALL=C sed -i "/^SELINUX=/Id" "/etc/selinux/config"
    else
        touch "/etc/selinux/config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/selinux/config"

    cp "/etc/selinux/config" "/etc/selinux/config.bak"
    # Insert at the end of the file
    printf '%s\n' "SELINUX=$var_selinux_state" >> "/etc/selinux/config"
    # Clean up after ourselves.
    rm "/etc/selinux/config.bak"

    fixfiles onboot
    fixfiles -f relabel

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: XCCDF Value var_selinux_state # promote to variable
      set_fact:
        var_selinux_state: !!str enforcing
      tags:
        - always

    - name: Ensure SELinux State is Enforcing
      block:

        - name: Check for duplicate values
          lineinfile:
            path: /etc/selinux/config
            create: false
            regexp: ^SELINUX=
            state: absent
          check_mode: true
          changed_when: false
          register: dupes

        - name: Deduplicate values from /etc/selinux/config
          lineinfile:
            path: /etc/selinux/config
            create: false
            regexp: ^SELINUX=
            state: absent
          when: dupes.found is defined and dupes.found > 1

        - name: Insert correct line to /etc/selinux/config
          lineinfile:
            path: /etc/selinux/config
            create: true
            regexp: ^SELINUX=
            line: SELINUX={{ var_selinux_state }}
            state: present
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84079-3
        - NIST-800-171-3.1.2
        - NIST-800-171-3.7.2
        - NIST-800-53-AC-3
        - NIST-800-53-AC-3(3)(a)
        - NIST-800-53-AU-9
        - NIST-800-53-SC-7(21)
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
        - selinux_state
Group Services Group contains 11 groups and 24 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 Base Services Group contains 1 rule
[ref] This section addresses the base services that are installed on a Red Hat Enterprise Linux 9 default installation which are not covered in other sections. Some of these services listen on the network and should be treated with particular discretion. Other services are local system utilities that may or may not be extraneous. In general, system services should be disabled if not required.
#### Rule Disable KDump Kernel Crash Analyzer (kdump) [ref]
The `kdump` service provides a kernel crash dump analyzer. It uses the `kexec` system call to boot a secondary kernel ("capture" kernel) following a system crash, which can load information from the crashed kernel for analysis. The `kdump` service can be disabled with the following command:
    $ sudo systemctl mask --now kdump.service
Rationale: | Kernel core dumps may contain the full contents of system memory at the time of the crash. Kernel core dumps consume a considerable amount of disk space and may result in denial of service by exhausting the available space on the target file system partition. Unless the system is used for kernel development or testing, there is little need to run the kdump service.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_kdump_disabled
Identifiers and References | Identifiers: CCE-84232-8 References: 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000366, 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), 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, 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 7.1, SR 7.6, A.11.2.6, A.12.1.2, 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.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, FMT_SMF_EXT.1.1, SRG-OS-000480-GPOS-00227
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" stop 'kdump.service'
    "$SYSTEMCTL_EXEC" disable 'kdump.service'
    "$SYSTEMCTL_EXEC" mask 'kdump.service'
    # Disable socket activation if we have a unit file for it
    if "$SYSTEMCTL_EXEC" list-unit-files | grep -q '^kdump.socket'; then
        "$SYSTEMCTL_EXEC" stop 'kdump.socket'
        "$SYSTEMCTL_EXEC" mask 'kdump.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 'kdump.service' || true

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    - name: Disable service kdump
      block:

        - name: Disable service kdump
          systemd:
            name: kdump.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
          ignore_errors: 'yes'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84232-8
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_kdump_disabled

    - name: Unit Socket Exists - kdump.socket
      command: systemctl list-unit-files kdump.socket
      args:
        warn: false
      register: socket_file_exists
      changed_when: false
      ignore_errors: true
      check_mode: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84232-8
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_kdump_disabled

    - name: Disable socket kdump
      systemd:
        name: kdump.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - '"kdump.socket" in socket_file_exists.stdout_lines[1]'
      tags:
        - CCE-84232-8
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_kdump_disabled
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    include disable_kdump

    class disable_kdump {
      service {'kdump':
        enable => false,
        ensure => 'stopped',
      }
    }
Remediation Anaconda snippet ⇲
    kdump --disable
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: kdump.service
            enabled: false
            mask: true
          - name: kdump.socket
            enabled: false
            mask: true
Remediation script ⇲
    [customizations.services]
    disabled = ["kdump"]
Group Cron and At Daemons Group contains 1 rule
[ref] The cron and at services are used to allow commands to be executed at a later time. The cron service is required by almost all systems to perform necessary maintenance tasks, while at may or may not be required on a given system. Both daemons should be configured defensively.
#### Rule Enable cron Service [ref]
The `crond` service is used to execute commands at preconfigured times. It is required by almost all systems to perform necessary maintenance tasks, such as notifying root of system activity. The `crond` service can be enabled with the following command:
    $ sudo systemctl enable crond.service
Rationale: | Due to its usage for maintenance and security-supporting tasks, enabling the cron daemon is essential.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_crond_enabled
Identifiers and References | Identifiers: CCE-84163-5 References: 11, 14, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, 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), 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, 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, 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, CM-6(a), PR.IP-1, PR.PT-3
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" unmask 'crond.service'
    "$SYSTEMCTL_EXEC" start 'crond.service'
    "$SYSTEMCTL_EXEC" enable 'crond.service'

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    - name: Enable service crond
      block:

        - name: Gather the package facts
          package_facts:
            manager: auto

        - name: Enable service crond
          service:
            name: crond
            enabled: 'yes'
            state: started
            masked: 'no'
          when:
            - '"cronie" in ansible_facts.packages'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84163-5
        - NIST-800-53-CM-6(a)
        - enable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_crond_enabled
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    include enable_crond

    class enable_crond {
      service {'crond':
        enable => true,
        ensure => 'running',
      }
    }
Remediation script ⇲
    [customizations.services]
    enabled = ["crond"]
Group NFS and RPC Group contains 1 group and 1 rule
[ref] The Network File System is a popular distributed filesystem for the Unix environment, and is very widely deployed. This section discusses the circumstances under which it is possible to disable NFS and its dependencies, and then details steps which should be taken to secure NFS's configuration. This section is relevant to systems operating as NFS clients, as well as to those operating as NFS servers.
Group Configure NFS Servers Group contains 1 rule
[ref] The steps in this section are appropriate for systems which operate as NFS servers.
#### Rule Use Kerberos Security on All Exports [ref]
Using Kerberos on all exported mounts prevents a malicious client or user from impersonating a system user. To cryptography authenticate users to the NFS server, add `sec=krb5:krb5i:krb5p` to each export in `/etc/exports`.
Rationale: | When an NFS server is configured to use AUTH_SYS a selected userid and groupid are used to handle requests from the remote user. The userid and groupid could mistakenly or maliciously be set incorrectly. The AUTH_GSS method of authentication uses certificates on the server and client systems to more securely authenticate the remote mount request.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_use_kerberos_security_all_exports
Identifiers and References | Identifiers: CCE-89947-6 References: 1, 12, 14, 15, 16, 18, 3, 5, DSS05.04, DSS05.10, DSS06.10, CCI-000366, 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), 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.3, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, A.18.1.4, A.6.1.2, A.9.1.2, A.9.2.1, A.9.2.3, A.9.2.4, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, CM-7(a), CM-7(b), CM-6(a), IA-2, IA-2(8), IA-2(9), AC-17(a), PR.AC-4, PR.AC-7, SRG-OS-000480-GPOS-00227
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | configure
    nfs_exports=()
    readarray -t nfs_exports < <(grep -E "^/.*[[:space:]]+ .*\(.*\)[[:space:]]*$" /etc/exports | awk '{print $2}')

    for nfs_export in "${nfs_exports[@]}"
    do
        correct_export=""
        if [ "$(grep -c "sec=" <<<"$nfs_export")" -eq 0 ]; then
            correct_export="$(echo $nfs_export|sed  -e 's/).*$/,sec=krb5\:krb5i\:krb5p)/')"
        else
            correct_export="$(echo $nfs_export|sed  -e 's/sec=[^\,\)]*/sec=krb5\:krb5i\:krb5p/')"
        fi
        sed -i "s|$nfs_export|$correct_export|g" /etc/exports
    done
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | configure
    - name: Drop any security clause for every export
      replace:
        path: /etc/exports
        regexp: ^(/.*\w+.*\(.*),sec=[^,]*(.*\)\w*$)
        replace: \1\2
      ignore_errors: true
      tags:
        - CCE-89947-6
        - NIST-800-53-AC-17(a)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-IA-2
        - NIST-800-53-IA-2(8)
        - NIST-800-53-IA-2(9)
        - configure_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - use_kerberos_security_all_exports

    - name: Add kerberos security when no security is defined for an export
      replace:
        path: /etc/exports
        regexp: ^(/.*\w+.*\(.*)(\)\w*$)
        replace: \1,sec=krb5:krb5i:krb5p\2
      tags:
        - CCE-89947-6
        - NIST-800-53-AC-17(a)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-IA-2
        - NIST-800-53-IA-2(8)
        - NIST-800-53-IA-2(9)
        - configure_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - use_kerberos_security_all_exports
Group Obsolete Services Group contains 4 groups and 10 rules
[ref] This section discusses a number of network-visible services which have historically caused problems for system security, and for which disabling or severely limiting the service has been the best available guidance for some time. As a result of this, many of these services are not installed as part of Red Hat Enterprise Linux 9 by default. Organizations which are running these services should switch to more secure equivalents as soon as possible. If it remains absolutely necessary to run one of these services for legacy reasons, care should be taken to restrict the service as much as possible, for instance by configuring host firewall software such as `iptables` to restrict access to the vulnerable service to only those remote hosts which have a known need to use it.
Group Xinetd Group contains 2 rules
[ref] The `xinetd` service acts as a dedicated listener for some network services (mostly, obsolete ones) and can be used to provide access controls and perform some logging. It has been largely obsoleted by other features, and it is not installed by default. The older Inetd service is not even available as part of Red Hat Enterprise Linux 9.
#### Rule Uninstall xinetd Package [ref]
The `xinetd` package can be removed with the following command:
    $ sudo dnf erase xinetd
Rationale: | Removing the `xinetd` package decreases the risk of the xinetd service's accidental (or intentional) activation.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_package_xinetd_removed
Identifiers and References | Identifiers: CCE-84155-1 References: BP28(R1), 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000305, 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), 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, 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 7.1, SR 7.6, A.11.2.6, A.12.1.2, 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.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    # CAUTION: This remediation script will remove xinetd
    #	   from the system, and may remove any packages
    #	   that depend on xinetd. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "xinetd" ; then

        dnf remove -y "xinetd"

    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    - name: Ensure xinetd is removed
      package:
        name: xinetd
        state: absent
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84155-1
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - disable_strategy
        - low_complexity
        - low_disruption
        - low_severity
        - no_reboot_needed
        - package_xinetd_removed
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    include remove_xinetd

    class remove_xinetd {
      package { 'xinetd':
        ensure => 'purged',
      }
    }
Remediation Anaconda snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    package --remove=xinetd
#### Rule Disable xinetd Service [ref]
The `xinetd` service can be disabled with the following command:
    $ sudo systemctl mask --now xinetd.service
Rationale: | The xinetd service provides a dedicated listener service for some programs, which is no longer necessary for commonly-used network services. Disabling it ensures that these uncommon services are not running, and also prevents attacks against xinetd itself.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_xinetd_disabled
Identifiers and References | Identifiers: CCE-84156-9 References: 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, 3.4.7, CCI-000305, 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), 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, 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 7.1, SR 7.6, A.11.2.6, A.12.1.2, 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.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" stop 'xinetd.service'
    "$SYSTEMCTL_EXEC" disable 'xinetd.service'
    "$SYSTEMCTL_EXEC" mask 'xinetd.service'
    # Disable socket activation if we have a unit file for it
    if "$SYSTEMCTL_EXEC" list-unit-files | grep -q '^xinetd.socket'; then
        "$SYSTEMCTL_EXEC" stop 'xinetd.socket'
        "$SYSTEMCTL_EXEC" mask 'xinetd.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 'xinetd.service' || true

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    - name: Disable service xinetd
      block:

        - name: Disable service xinetd
          systemd:
            name: xinetd.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
          ignore_errors: 'yes'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84156-9
        - NIST-800-171-3.4.7
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_xinetd_disabled

    - name: Unit Socket Exists - xinetd.socket
      command: systemctl list-unit-files xinetd.socket
      args:
        warn: false
      register: socket_file_exists
      changed_when: false
      ignore_errors: true
      check_mode: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84156-9
        - NIST-800-171-3.4.7
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_xinetd_disabled

    - name: Disable socket xinetd
      systemd:
        name: xinetd.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - '"xinetd.socket" in socket_file_exists.stdout_lines[1]'
      tags:
        - CCE-84156-9
        - NIST-800-171-3.4.7
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_xinetd_disabled
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    include disable_xinetd

    class disable_xinetd {
      service {'xinetd':
        enable => false,
        ensure => 'stopped',
      }
    }
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: xinetd.service
            enabled: false
            mask: true
          - name: xinetd.socket
            enabled: false
            mask: true
Remediation script ⇲
    [customizations.services]
    disabled = ["xinetd"]
Group Rlogin, Rsh, and Rexec Group contains 3 rules
[ref] The Berkeley r-commands are legacy services which allow cleartext remote access and have an insecure trust model.
#### Rule Uninstall rsh-server Package [ref]
The `rsh-server` package can be removed with the following command:
    $ sudo dnf erase rsh-server
Rationale: | The `rsh-server` service provides unencrypted remote access service which does not provide for the confidentiality and integrity of user passwords or the remote session and has very weak authentication. If a privileged user were to login using this service, the privileged user password could be compromised. The `rsh-server` package provides several obsolete and insecure network services. Removing it decreases the risk of those services' accidental (or intentional) activation.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_package_rsh-server_removed
Identifiers and References | Identifiers: CCE-84143-7 References: BP28(R1), 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000381, 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), 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, 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 7.1, SR 7.6, A.11.2.6, A.12.1.2, 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.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), IA-5(1)(c), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, SRG-OS-000095-GPOS-00049
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    # CAUTION: This remediation script will remove rsh-server
    #	   from the system, and may remove any packages
    #	   that depend on rsh-server. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "rsh-server" ; then

        dnf remove -y "rsh-server"

    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    - name: Ensure rsh-server is removed
      package:
        name: rsh-server
        state: absent
      tags:
        - CCE-84143-7
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-IA-5(1)(c)
        - disable_strategy
        - high_severity
        - low_complexity
        - low_disruption
        - no_reboot_needed
        - package_rsh-server_removed
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    include remove_rsh-server

    class remove_rsh-server {
      package { 'rsh-server':
        ensure => 'purged',
      }
    }
Remediation Anaconda snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    package --remove=rsh-server
#### Rule Disable rlogin Service [ref]
The `rlogin` service, which is available with the `rsh-server` package and runs as a service through xinetd or separately as a systemd socket, should be disabled. If using xinetd, set `disable` to `yes` in `/etc/xinetd.d/rlogin`. The `rlogin` socket can be disabled with the following command:
    $ sudo systemctl mask --now rlogin.socket
Rationale: | The rlogin service uses unencrypted network communications, which means that data from the login session, including passwords and all other information transmitted during the session, can be stolen by eavesdroppers on the network.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_service_rlogin_disabled
Identifiers and References | Identifiers: CCE-88395-9 References: 1, 11, 12, 14, 15, 16, 3, 5, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.06, DSS06.10, 3.1.13, 3.4.7, CCI-001436, 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), 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, 4.3.4.3.2, 4.3.4.3.3, 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 7.1, SR 7.6, A.11.2.6, A.12.1.2, 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.18.1.4, 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.2, A.9.4.3, CM-7(a), CM-7(b), CM-6(a), IA-5(1)(c), PR.AC-1, PR.AC-3, PR.AC-6, PR.AC-7, PR.IP-1, PR.PT-3, PR.PT-4
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" stop 'rlogin.service'
    "$SYSTEMCTL_EXEC" disable 'rlogin.service'
    "$SYSTEMCTL_EXEC" mask 'rlogin.service'
    # Disable socket activation if we have a unit file for it
    if "$SYSTEMCTL_EXEC" list-unit-files | grep -q '^rlogin.socket'; then
        "$SYSTEMCTL_EXEC" stop 'rlogin.socket'
        "$SYSTEMCTL_EXEC" mask 'rlogin.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 'rlogin.service' || true

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    - name: Disable service rlogin
      block:

        - name: Disable service rlogin
          systemd:
            name: rlogin.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
          ignore_errors: 'yes'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-88395-9
        - NIST-800-171-3.1.13
        - NIST-800-171-3.4.7
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-IA-5(1)(c)
        - disable_strategy
        - high_severity
        - low_complexity
        - low_disruption
        - no_reboot_needed
        - service_rlogin_disabled

    - name: Unit Socket Exists - rlogin.socket
      command: systemctl list-unit-files rlogin.socket
      args:
        warn: false
      register: socket_file_exists
      changed_when: false
      ignore_errors: true
      check_mode: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-88395-9
        - NIST-800-171-3.1.13
        - NIST-800-171-3.4.7
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-IA-5(1)(c)
        - disable_strategy
        - high_severity
        - low_complexity
        - low_disruption
        - no_reboot_needed
        - service_rlogin_disabled

    - name: Disable socket rlogin
      systemd:
        name: rlogin.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - '"rlogin.socket" in socket_file_exists.stdout_lines[1]'
      tags:
        - CCE-88395-9
        - NIST-800-171-3.1.13
        - NIST-800-171-3.4.7
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-IA-5(1)(c)
        - disable_strategy
        - high_severity
        - low_complexity
        - low_disruption
        - no_reboot_needed
        - service_rlogin_disabled
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    include disable_rlogin

    class disable_rlogin {
      service {'rlogin':
        enable => false,
        ensure => 'stopped',
      }
    }
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: rlogin.service
            enabled: false
            mask: true
          - name: rlogin.socket
            enabled: false
            mask: true
Remediation script ⇲
    [customizations.services]
    disabled = ["rlogin"]
#### Rule Remove Rsh Trust Files [ref]
The files `/etc/hosts.equiv` and `~/.rhosts` (in each user's home directory) list remote hosts and users that are trusted by the local system when using the rshd daemon. To remove these files, run the following command to delete them from any location:
    $ sudo rm /etc/hosts.equiv
    $ rm ~/.rhosts
Rationale: | This action is only meaningful if `.rhosts` support is permitted through PAM. Trust files are convenient, but when used in conjunction with the R-services, they can allow unauthenticated access to a system.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_no_rsh_trust_files
Identifiers and References | Identifiers: CCE-84145-2 References: 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-001436, 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), 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, 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 7.1, SR 7.6, A.11.2.6, A.12.1.2, 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.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4
Remediation Shell script ⇲
    find /root -xdev -type f -name ".rhosts" -exec rm -f {} \;
    find /home -maxdepth 2 -xdev -type f -name ".rhosts" -exec rm -f {} \;
    rm -f /etc/hosts.equiv
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - block:

        - name: Detect .rhosts files in users home directories
          find:
            paths:
              - /root
              - /home
            recurse: true
            patterns: .rhosts
            hidden: true
            file_type: file
          check_mode: false
          register: rhosts_locations

        - name: Remove .rhosts files
          file:
            path: '{{ item }}'
            state: absent
          with_items: '{{ rhosts_locations.files | map(attribute=''path'') | list }}'
          when: rhosts_locations is success

        - name: Remove /etc/hosts.equiv file
          file:
            path: /etc/hosts.equiv
            state: absent
      tags:
        - CCE-84145-2
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - high_severity
        - low_complexity
        - low_disruption
        - no_reboot_needed
        - no_rsh_trust_files
        - restrict_strategy
Group Chat/Messaging Services Group contains 2 rules
[ref] The talk software makes it possible for users to send and receive messages across systems through a terminal session.
#### Rule Uninstall talk-server Package [ref]
The `talk-server` package can be removed with the following command:
     $ sudo dnf erase talk-server
Rationale: | The talk software presents a security risk as it uses unencrypted protocols for communications. Removing the `talk-server` package decreases the risk of the accidental (or intentional) activation of talk services.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_talk-server_removed
Identifiers and References | Identifiers: CCE-84158-5 References: BP28(R1), 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)
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    # CAUTION: This remediation script will remove talk-server
    #	   from the system, and may remove any packages
    #	   that depend on talk-server. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "talk-server" ; then

        dnf remove -y "talk-server"

    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    - name: Ensure talk-server is removed
      package:
        name: talk-server
        state: absent
      tags:
        - CCE-84158-5
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - package_talk-server_removed
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    include remove_talk-server

    class remove_talk-server {
      package { 'talk-server':
        ensure => 'purged',
      }
    }
Remediation Anaconda snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    package --remove=talk-server
#### Rule Uninstall talk Package [ref]
The `talk` package contains the client program for the Internet talk protocol, which allows the user to chat with other users on different systems. Talk is a communication program which copies lines from one terminal to the terminal of another user. The `talk` package can be removed with the following command:
    $ sudo dnf erase talk
Rationale: | The talk software presents a security risk as it uses unencrypted protocols for communications. Removing the `talk` package decreases the risk of the accidental (or intentional) activation of talk client program.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_talk_removed
Identifiers and References | Identifiers: CCE-84157-7 References: BP28(R1), 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)
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    # CAUTION: This remediation script will remove talk
    #	   from the system, and may remove any packages
    #	   that depend on talk. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "talk" ; then

        dnf remove -y "talk"

    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    - name: Ensure talk is removed
      package:
        name: talk
        state: absent
      tags:
        - CCE-84157-7
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - package_talk_removed
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    include remove_talk

    class remove_talk {
      package { 'talk':
        ensure => 'purged',
      }
    }
Remediation Anaconda snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    package --remove=talk
Group Telnet Group contains 3 rules
[ref] The telnet protocol does not provide confidentiality or integrity for information transmitted on the network. This includes authentication information such as passwords. Organizations which use telnet should be actively working to migrate to a more secure protocol.
#### Rule Uninstall telnet-server Package [ref]
The `telnet-server` package can be removed with the following command:
    $ sudo dnf erase telnet-server
Rationale: | It is detrimental for operating systems to provide, or install by default, functionality exceeding requirements or mission objectives. These unnecessary capabilities are often overlooked and therefore may remain unsecure. They increase the risk to the platform by providing additional attack vectors. The telnet service provides an unencrypted remote access service which does not provide for the confidentiality and integrity of user passwords or the remote session. If a privileged user were to login using this service, the privileged user password could be compromised. Removing the `telnet-server` package decreases the risk of the telnet service's accidental (or intentional) activation.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_package_telnet-server_removed
Identifiers and References | Identifiers: CCE-84149-4 References: BP28(R1), 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000381, 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), 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, 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 7.1, SR 7.6, A.11.2.6, A.12.1.2, 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.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, SRG-OS-000095-GPOS-00049
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    # CAUTION: This remediation script will remove telnet-server
    #	   from the system, and may remove any packages
    #	   that depend on telnet-server. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "telnet-server" ; then

        dnf remove -y "telnet-server"

    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    - name: Ensure telnet-server is removed
      package:
        name: telnet-server
        state: absent
      tags:
        - CCE-84149-4
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - disable_strategy
        - high_severity
        - low_complexity
        - low_disruption
        - no_reboot_needed
        - package_telnet-server_removed
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    include remove_telnet-server

    class remove_telnet-server {
      package { 'telnet-server':
        ensure => 'purged',
      }
    }
Remediation Anaconda snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    package --remove=telnet-server
#### Rule Remove telnet Clients [ref]
The telnet client allows users to start connections to other systems via the telnet protocol.
Rationale: | The `telnet` protocol is insecure and unencrypted. The use of an unencrypted transmission medium could allow an unauthorized user to steal credentials. The `ssh` package provides an encrypted session and stronger security and is included in Red Hat Enterprise Linux 9.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_package_telnet_removed
Identifiers and References | Identifiers: CCE-84146-0 References: BP28(R1), 3.1.13, 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), A.8.2.3, A.13.1.1, A.13.2.1, A.13.2.3, A.14.1.2, A.14.1.3
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    # CAUTION: This remediation script will remove telnet
    #	   from the system, and may remove any packages
    #	   that depend on telnet. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "telnet" ; then

        dnf remove -y "telnet"

    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    - name: Ensure telnet is removed
      package:
        name: telnet
        state: absent
      tags:
        - CCE-84146-0
        - NIST-800-171-3.1.13
        - disable_strategy
        - low_complexity
        - low_disruption
        - low_severity
        - no_reboot_needed
        - package_telnet_removed
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    include remove_telnet

    class remove_telnet {
      package { 'telnet':
        ensure => 'purged',
      }
    }
Remediation Anaconda snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    package --remove=telnet
#### Rule Disable telnet Service [ref]
The `telnet` service configuration file `/etc/xinetd.d/telnet` is not created automatically. If it was created manually, check the `/etc/xinetd.d/telnet` file and ensure that `disable = no` is changed to read `disable = yes` as follows below:
    # description: The telnet server serves telnet sessions; it uses \\
    #       unencrypted username/password pairs for authentication.
    service telnet
    {
            flags           = REUSE
            socket_type     = stream

            wait            = no
            user            = root
            server          = /usr/sbin/in.telnetd
            log_on_failure  += USERID
            disable         = yes
    }
If the `/etc/xinetd.d/telnet` file does not exist, make sure that the activation of the `telnet` service on system boot is disabled via the following command: The `rexec` socket can be disabled with the following command:
    $ sudo systemctl mask --now rexec.socket
Rationale: | The telnet protocol uses unencrypted network communication, which means that data from the login session, including passwords and all other information transmitted during the session, can be stolen by eavesdroppers on the network. The telnet protocol is also subject to man-in-the-middle attacks.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_service_telnet_disabled
Identifiers and References | Identifiers: CCE-84150-2 References: 1, 11, 12, 14, 15, 16, 3, 5, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.06, DSS06.10, 3.1.13, 3.4.7, 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), 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, 4.3.4.3.2, 4.3.4.3.3, 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 7.1, SR 7.6, A.11.2.6, A.12.1.2, 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.18.1.4, 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.2, A.9.4.3, CM-7(a), CM-7(b), CM-6(a), IA-5(1)(c), PR.AC-1, PR.AC-3, PR.AC-6, PR.AC-7, PR.IP-1, PR.PT-3, PR.PT-4
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" stop 'telnet.service'
    "$SYSTEMCTL_EXEC" disable 'telnet.service'
    "$SYSTEMCTL_EXEC" mask 'telnet.service'
    # Disable socket activation if we have a unit file for it
    if "$SYSTEMCTL_EXEC" list-unit-files | grep -q '^telnet.socket'; then
        "$SYSTEMCTL_EXEC" stop 'telnet.socket'
        "$SYSTEMCTL_EXEC" mask 'telnet.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 'telnet.service' || true

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | disable
    - name: Disable service telnet
      block:

        - name: Disable service telnet
          systemd:
            name: telnet.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
          ignore_errors: 'yes'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84150-2
        - NIST-800-171-3.1.13
        - NIST-800-171-3.4.7
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-IA-5(1)(c)
        - disable_strategy
        - high_severity
        - low_complexity
        - low_disruption
        - no_reboot_needed
        - service_telnet_disabled

    - name: Unit Socket Exists - telnet.socket
      command: systemctl list-unit-files telnet.socket
      args:
        warn: false
      register: socket_file_exists
      changed_when: false
      ignore_errors: true
      check_mode: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-84150-2
        - NIST-800-171-3.1.13
        - NIST-800-171-3.4.7
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-IA-5(1)(c)
        - disable_strategy
        - high_severity
        - low_complexity
        - low_disruption
        - no_reboot_needed
        - service_telnet_disabled

    - name: Disable socket telnet
      systemd:
        name: telnet.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - '"telnet.socket" in socket_file_exists.stdout_lines[1]'
      tags:
        - CCE-84150-2
        - NIST-800-171-3.1.13
        - NIST-800-171-3.4.7
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-IA-5(1)(c)
        - disable_strategy
        - high_severity
        - low_complexity
        - low_disruption
        - no_reboot_needed
        - service_telnet_disabled
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    include disable_telnet

    class disable_telnet {
      service {'telnet':
        enable => false,
        ensure => 'stopped',
      }
    }
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: telnet.service
            enabled: false
            mask: true
          - name: telnet.socket
            enabled: false
            mask: true
Remediation script ⇲
    [customizations.services]
    disabled = ["telnet"]
Group SSH Server Group contains 1 group and 11 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 11 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 Disable Host-Based Authentication [ref]
SSH's cryptographic host-based authentication is more secure than `.rhosts` authentication. However, it is not recommended that hosts unilaterally trust one another, even within an organization. The default SSH configuration disables host-based authentication. The appropriate configuration is used if no value is set for `HostbasedAuthentication`. To explicitly disable host-based authentication, add or correct the following line in `/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf`:
    HostbasedAuthentication no
Rationale: | SSH trust relationships mean a compromise on one host can allow an attacker to move trivially to other hosts.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_disable_host_auth
Identifiers and References | Identifiers: CCE-90816-0 References: 11, 12, 14, 15, 16, 18, 3, 5, 9, 5.5.6, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.03, DSS06.06, 3.1.12, CCI-000366, 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), 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, 4.3.4.3.2, 4.3.4.3.3, 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, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, 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.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, 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, AC-3, AC-17(a), CM-7(a), CM-7(b), CM-6(a), PR.AC-4, PR.AC-6, PR.IP-1, PR.PT-3, FIA_UAU.1, SRG-OS-000480-GPOS-00229, SRG-OS-000480-VMM-002000
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    mkdir -p /etc/ssh/sshd_config.d
    touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf

    LC_ALL=C sed -i "/^\s*HostbasedAuthentication\s\+/Id" "/etc/ssh/sshd_config"
    LC_ALL=C sed -i "/^\s*HostbasedAuthentication\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*HostbasedAuthentication\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 before the line matching the regex '^Match'.
    line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" | LC_ALL=C sed 's/:.*//g')"
    if [ -z "$line_number" ]; then
        # There was no match of '^Match', insert at
        # the end of the file.
        printf '%s\n' "HostbasedAuthentication no" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    else
        head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
        printf '%s\n' "HostbasedAuthentication no" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
        tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    fi
    # 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
Strategy: | restrict
    - name: Disable Host-Based Authentication
      block:

        - name: Deduplicate values from /etc/ssh/sshd_config
          lineinfile:
            path: /etc/ssh/sshd_config
            create: false
            regexp: ^\s*{{ "HostbasedAuthentication"| regex_escape }}\s+
            state: absent

        - name: Check if /etc/ssh/sshd_config.d exists
          stat:
            path: /etc/ssh/sshd_config.d
          register: _etc_ssh_sshd_config_d_exists

        - name: Check if the parameter HostbasedAuthentication is present in /etc/ssh/sshd_config.d
          find:
            paths: /etc/ssh/sshd_config.d
            recurse: 'yes'
            follow: 'no'
            contains: ^\s*{{ "HostbasedAuthentication"| 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
          lineinfile:
            path: '{{ item.path }}'
            create: false
            regexp: ^\s*{{ "HostbasedAuthentication"| regex_escape }}\s+
            state: absent
          with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}'
          when: _etc_ssh_sshd_config_d_has_parameter.matched

        - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
          lineinfile:
            path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
            create: true
            regexp: ^\s*{{ "HostbasedAuthentication"| regex_escape }}\s+
            line: HostbasedAuthentication no
            state: present
            insertbefore: ^[#\s]*Match
            validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-90816-0
        - CJIS-5.5.6
        - NIST-800-171-3.1.12
        - NIST-800-53-AC-17(a)
        - NIST-800-53-AC-3
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - disable_host_auth
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
Remediation Kubernetes snippet ⇲
    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,%23%09%24OpenBSD%3A%20sshd_config%2Cv%201.103%202018%2F04%2F09%2020%3A41%3A22%20tj%20Exp%20%24%0A%0A%23%20This%20is%20the%20sshd%20server%20system-wide%20configuration%20file.%20%20See%0A%23%20sshd_config%285%29%20for%20more%20information.%0A%0A%23%20This%20sshd%20was%20compiled%20with%20PATH%3D%2Fusr%2Flocal%2Fbin%3A%2Fusr%2Fbin%3A%2Fusr%2Flocal%2Fsbin%3A%2Fusr%2Fsbin%0A%0A%23%20The%20strategy%20used%20for%20options%20in%20the%20default%20sshd_config%20shipped%20with%0A%23%20OpenSSH%20is%20to%20specify%20options%20with%20their%20default%20value%20where%0A%23%20possible%2C%20but%20leave%20them%20commented.%20%20Uncommented%20options%20override%20the%0A%23%20default%20value.%0A%0A%23%20If%20you%20want%20to%20change%20the%20port%20on%20a%20SELinux%20system%2C%20you%20have%20to%20tell%0A%23%20SELinux%20about%20this%20change.%0A%23%20semanage%20port%20-a%20-t%20ssh_port_t%20-p%20tcp%20%23PORTNUMBER%0A%23%0A%23Port%2022%0A%23AddressFamily%20any%0A%23ListenAddress%200.0.0.0%0A%23ListenAddress%20%3A%3A%0A%0AHostKey%20%2Fetc%2Fssh%2Fssh_host_rsa_key%0AHostKey%20%2Fetc%2Fssh%2Fssh_host_ecdsa_key%0AHostKey%20%2Fetc%2Fssh%2Fssh_host_ed25519_key%0A%0A%23%20Ciphers%20and%20keying%0ARekeyLimit%20512M%201h%0A%0A%23%20System-wide%20Crypto%20policy%3A%0A%23%20This%20system%20is%20following%20system-wide%20crypto%20policy.%20The%20changes%20to%0A%23%20Ciphers%2C%20MACs%2C%20KexAlgoritms%20and%20GSSAPIKexAlgorithsm%20will%20not%20have%20any%0A%23%20effect%20here.%20They%20will%20be%20overridden%20by%20command-line%20options%20passed%20on%0A%23%20the%20server%20start%20up.%0A%23%20To%20opt%20out%2C%20uncomment%20a%20line%20with%20redefinition%20of%20%20CRYPTO_POLICY%3D%0A%23%20variable%20in%20%20%2Fetc%2Fsysconfig%2Fsshd%20%20to%20overwrite%20the%20policy.%0A%23%20For%20more%20information%2C%20see%20manual%20page%20for%20update-crypto-policies%288%29.%0A%0A%23%20Logging%0A%23SyslogFacility%20AUTH%0ASyslogFacility%20AUTHPRIV%0A%23LogLevel%20INFO%0A%0A%23%20Authentication%3A%0A%0A%23LoginGraceTime%202m%0APermitRootLogin%20no%0AStrictModes%20yes%0A%23MaxAuthTries%206%0A%23MaxSessions%2010%0A%0APubkeyAuthentication%20yes%0A%0A%23%20The%20default%20is%20to%20check%20both%20.ssh%2Fauthorized_keys%20and%20.ssh%2Fauthorized_keys2%0A%23%20but%20this%20is%20overridden%20so%20installations%20will%20only%20check%20.ssh%2Fauthorized_keys%0AAuthorizedKeysFile%09.ssh%2Fauthorized_keys%0A%0A%23AuthorizedPrincipalsFile%20none%0A%0A%23AuthorizedKeysCommand%20none%0A%23AuthorizedKeysCommandUser%20nobody%0A%0A%23%20For%20this%20to%20work%20you%20will%20also%20need%20host%20keys%20in%20%2Fetc%2Fssh%2Fssh_known_hosts%0AHostbasedAuthentication%20no%0A%23%20Change%20to%20yes%20if%20you%20don%27t%20trust%20~%2F.ssh%2Fknown_hosts%20for%0A%23%20HostbasedAuthentication%0AIgnoreUserKnownHosts%20yes%0A%23%20Don%27t%20read%20the%20user%27s%20~%2F.rhosts%20and%20~%2F.shosts%20files%0AIgnoreRhosts%20yes%0A%0A%23%20To%20disable%20tunneled%20clear%20text%20passwords%2C%20change%20to%20no%20here%21%0A%23PasswordAuthentication%20yes%0APermitEmptyPasswords%20no%0APasswordAuthentication%20no%0A%0A%23%20Change%20to%20no%20to%20disable%20s%2Fkey%20passwords%0A%23ChallengeResponseAuthentication%20yes%0AChallengeResponseAuthentication%20no%0A%0A%23%20Kerberos%20options%0AKerberosAuthentication%20no%0A%23KerberosOrLocalPasswd%20yes%0A%23KerberosTicketCleanup%20yes%0A%23KerberosGetAFSToken%20no%0A%23KerberosUseKuserok%20yes%0A%0A%23%20GSSAPI%20options%0AGSSAPIAuthentication%20no%0AGSSAPICleanupCredentials%20no%0A%23GSSAPIStrictAcceptorCheck%20yes%0A%23GSSAPIKeyExchange%20no%0A%23GSSAPIEnablek5users%20no%0A%0A%23%20Set%20this%20to%20%27yes%27%20to%20enable%20PAM%20authentication%2C%20account%20processing%2C%0A%23%20and%20session%20processing.%20If%20this%20is%20enabled%2C%20PAM%20authentication%20will%0A%23%20be%20allowed%20through%20the%20ChallengeResponseAuthentication%20and%0A%23%20PasswordAuthentication.%20%20Depending%20on%20your%20PAM%20configuration%2C%0A%23%20PAM%20authentication%20via%20ChallengeResponseAuthentication%20may%20bypass%0A%23%20the%20setting%20of%20%22PermitRootLogin%20without-password%22.%0A%23%20If%20you%20just%20want%20the%20PAM%20account%20and%20session%20checks%20to%20run%20without%0A%23%20PAM%20authentication%2C%20then%20enable%20this%20but%20set%20PasswordAuthentication%0A%23%20and%20ChallengeResponseAuthentication%20to%20%27no%27.%0A%23%20WARNING%3A%20%27UsePAM%20no%27%20is%20not%20supported%20in%20Fedora%20and%20may%20cause%20several%0A%23%20problems.%0AUsePAM%20yes%0A%0A%23AllowAgentForwarding%20yes%0A%23AllowTcpForwarding%20yes%0A%23GatewayPorts%20no%0AX11Forwarding%20yes%0A%23X11DisplayOffset%2010%0A%23X11UseLocalhost%20yes%0A%23PermitTTY%20yes%0A%0A%23%20It%20is%20recommended%20to%20use%20pam_motd%20in%20%2Fetc%2Fpam.d%2Fsshd%20instead%20of%20PrintMotd%2C%0A%23%20as%20it%20is%20more%20configurable%20and%20versatile%20than%20the%20built-in%20version.%0APrintMotd%20no%0A%0APrintLastLog%20yes%0A%23TCPKeepAlive%20yes%0APermitUserEnvironment%20no%0ACompression%20no%0AClientAliveInterval%20600%0AClientAliveCountMax%200%0A%23UseDNS%20no%0A%23PidFile%20%2Fvar%2Frun%2Fsshd.pid%0A%23MaxStartups%2010%3A30%3A100%0A%23PermitTunnel%20no%0A%23ChrootDirectory%20none%0A%23VersionAddendum%20none%0A%0A%23%20no%20default%20banner%20path%0ABanner%20%2Fetc%2Fissue%0A%0A%23%20Accept%20locale-related%20environment%20variables%0AAcceptEnv%20LANG%20LC_CTYPE%20LC_NUMERIC%20LC_TIME%20LC_COLLATE%20LC_MONETARY%20LC_MESSAGES%0AAcceptEnv%20LC_PAPER%20LC_NAME%20LC_ADDRESS%20LC_TELEPHONE%20LC_MEASUREMENT%0AAcceptEnv%20LC_IDENTIFICATION%20LC_ALL%20LANGUAGE%0AAcceptEnv%20XMODIFIERS%0A%0A%23%20override%20default%20of%20no%20subsystems%0ASubsystem%09sftp%09%2Fusr%2Flibexec%2Fopenssh%2Fsftp-server%0A%0A%23%20Example%20of%20overriding%20settings%20on%20a%20per-user%20basis%0A%23Match%20User%20anoncvs%0A%23%09X11Forwarding%20no%0A%23%09AllowTcpForwarding%20no%0A%23%09PermitTTY%20no%0A%23%09ForceCommand%20cvs%20server%0A%0AUsePrivilegeSeparation%20sandbox
            mode: 0600
            path: /etc/ssh/sshd_config
            overwrite: true
#### Rule Allow Only SSH Protocol 2 [ref]
Only SSH protocol version 2 connections should be permitted. The default setting in `/etc/ssh/sshd_config` is correct, and can be verified by ensuring that the following line appears:
    Protocol 2
Warning: As of `openssh-server` version `7.4` and above, the only protocol supported is version 2, and line
    Protocol 2
in `/etc/ssh/sshd_config` is not necessary.
Rationale: | SSH protocol version 1 is an insecure implementation of the SSH protocol and has many well-known vulnerability exploits. Exploits of the SSH daemon could provide immediate root access to the system.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_allow_only_protocol2
Identifiers and References | Identifiers: CCE-90812-9 References: NT007(R1), 1, 12, 15, 16, 5, 8, 5.5.6, APO13.01, DSS01.04, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.1.13, 3.5.4, CCI-000197, CCI-000366, 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), 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, SR 1.1, SR 1.10, 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.6, 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, 0487, 1449, 1506, A.11.2.6, A.13.1.1, A.13.2.1, A.14.1.3, A.18.1.4, A.6.2.1, A.6.2.2, 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, CIP-003-8 R4.2, CIP-007-3 R5.1, CIP-007-3 R7.1, CM-6(a), AC-17(a), AC-17(2), IA-5(1)(c), SC-13, MA-4(6), PR.AC-1, PR.AC-3, PR.AC-6, PR.AC-7, PR.PT-4, SRG-OS-000074-GPOS-00042, SRG-OS-000480-GPOS-00227, SRG-OS-000033-VMM-000140
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    # Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
    # Otherwise, regular sed command will do.
    sed_command=('sed' '-i')
    if test -L "/etc/ssh/sshd_config"; then
        sed_command+=('--follow-symlinks')
    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' <<< "^Protocol")

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

    # 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 "^Protocol\\>" "/etc/ssh/sshd_config"; then
        "${sed_command[@]}" "s/^Protocol\\>.*/$formatted_output/gi" "/etc/ssh/sshd_config"
    else
        # \n is precaution for case where file ends without trailing newline
        cce="CCE-90812-9"
        printf '\n# Per %s: Set %s in %s\n' "$cce" "$formatted_output" "/etc/ssh/sshd_config" >> "/etc/ssh/sshd_config"
        printf '%s\n' "$formatted_output" >> "/etc/ssh/sshd_config"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Allow Only SSH Protocol 2
      block:

        - name: Check for duplicate values
          lineinfile:
            path: /etc/ssh/sshd_config
            create: false
            regexp: (?i)^\s*Protocol\s+
            state: absent
          check_mode: true
          changed_when: false
          register: dupes

        - name: Deduplicate values from /etc/ssh/sshd_config
          lineinfile:
            path: /etc/ssh/sshd_config
            create: false
            regexp: (?i)^\s*Protocol\s+
            state: absent
          when: dupes.found is defined and dupes.found > 1

        - name: Insert correct line to /etc/ssh/sshd_config
          lineinfile:
            path: /etc/ssh/sshd_config
            create: true
            regexp: (?i)^\s*Protocol\s+
            line: Protocol 2
            state: present
            insertbefore: ^[#\s]*Match
            validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-90812-9
        - CJIS-5.5.6
        - NIST-800-171-3.1.13
        - NIST-800-171-3.5.4
        - NIST-800-53-AC-17(2)
        - NIST-800-53-AC-17(a)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-IA-5(1)(c)
        - NIST-800-53-MA-4(6)
        - NIST-800-53-SC-13
        - high_severity
        - low_complexity
        - low_disruption
        - no_reboot_needed
        - restrict_strategy
        - sshd_allow_only_protocol2
#### Rule Disable Compression Or Set Compression to delayed [ref]
Compression is useful for slow network connections over long distances but can cause performance issues on local LANs. If use of compression is required, it should be enabled only after a user has authenticated; otherwise, it should be disabled. To disable compression or delay compression until after a user has successfully authenticated, add or correct the following line in the `/etc/ssh/sshd_config` file:
    Compression no
Rationale: | If compression is allowed in an SSH connection prior to authentication, vulnerabilities in the compression software could result in compromise of the system from an unauthenticated connection, potentially with root privileges.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_disable_compression
Identifiers and References | Identifiers: CCE-90801-2 References: 11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.1.12, CCI-000366, 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), 4.3.4.3.2, 4.3.4.3.3, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, AC-17(a), CM-7(a), CM-7(b), CM-6(a), PR.IP-1, SRG-OS-000480-GPOS-00227, SRG-OS-000480-VMM-002000
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    var_sshd_disable_compression='no'

    # Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
    # Otherwise, regular sed command will do.
    sed_command=('sed' '-i')
    if test -L "/etc/ssh/sshd_config"; then
        sed_command+=('--follow-symlinks')
    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' <<< "^Compression")

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

    # 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 "^Compression\\>" "/etc/ssh/sshd_config"; then
        "${sed_command[@]}" "s/^Compression\\>.*/$formatted_output/gi" "/etc/ssh/sshd_config"
    else
        # \n is precaution for case where file ends without trailing newline
        cce="CCE-90801-2"
        printf '\n# Per %s: Set %s in %s\n' "$cce" "$formatted_output" "/etc/ssh/sshd_config" >> "/etc/ssh/sshd_config"
        printf '%s\n' "$formatted_output" >> "/etc/ssh/sshd_config"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: XCCDF Value var_sshd_disable_compression # promote to variable
      set_fact:
        var_sshd_disable_compression: !!str no
      tags:
        - always

    - name: Disable Compression Or Set Compression to delayed
      block:

        - name: Check for duplicate values
          lineinfile:
            path: /etc/ssh/sshd_config
            create: false
            regexp: (?i)^\s*Compression\s+
            state: absent
          check_mode: true
          changed_when: false
          register: dupes

        - name: Deduplicate values from /etc/ssh/sshd_config
          lineinfile:
            path: /etc/ssh/sshd_config
            create: false
            regexp: (?i)^\s*Compression\s+
            state: absent
          when: dupes.found is defined and dupes.found > 1

        - name: Insert correct line to /etc/ssh/sshd_config
          lineinfile:
            path: /etc/ssh/sshd_config
            create: true
            regexp: (?i)^\s*Compression\s+
            line: Compression {{ var_sshd_disable_compression }}
            state: present
            insertbefore: ^[#\s]*Match
            validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-90801-2
        - NIST-800-171-3.1.12
        - NIST-800-53-AC-17(a)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
        - sshd_disable_compression
#### Rule Disable SSH Access via Empty Passwords [ref]
Disallow SSH login with empty passwords. The default SSH configuration disables logins with empty passwords. The appropriate configuration is used if no value is set for `PermitEmptyPasswords`. To explicitly disallow SSH login from accounts with empty passwords, add or correct the following line in `/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf`:
    PermitEmptyPasswords no
Any accounts with empty passwords should be disabled immediately, and PAM configuration should prevent users from being able to assign themselves empty passwords.
Rationale: | Configuring this setting for the SSH daemon provides additional assurance that remote login via SSH will require a password, even in the event of misconfiguration elsewhere.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_disable_empty_passwords
Identifiers and References | Identifiers: CCE-90799-8 References: NT007(R17), 11, 12, 13, 14, 15, 16, 18, 3, 5, 9, 5.5.6, APO01.06, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.03, DSS06.06, 3.1.1, 3.1.5, CCI-000366, CCI-000766, 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), 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, 4.3.4.3.2, 4.3.4.3.3, 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 5.2, SR 7.6, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.2, A.12.5.1, A.12.6.2, 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.14.2.2, A.14.2.3, A.14.2.4, 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.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, AC-17(a), CM-7(a), CM-7(b), CM-6(a), PR.AC-4, PR.AC-6, PR.DS-5, PR.IP-1, PR.PT-3, FIA_UAU.1, SRG-OS-000106-GPOS-00053, SRG-OS-000480-GPOS-00229, SRG-OS-000480-GPOS-00227, SRG-OS-000480-VMM-002000
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    mkdir -p /etc/ssh/sshd_config.d
    touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf

    LC_ALL=C sed -i "/^\s*PermitEmptyPasswords\s\+/Id" "/etc/ssh/sshd_config"
    LC_ALL=C sed -i "/^\s*PermitEmptyPasswords\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*PermitEmptyPasswords\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 before the line matching the regex '^Match'.
    line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" | LC_ALL=C sed 's/:.*//g')"
    if [ -z "$line_number" ]; then
        # There was no match of '^Match', insert at
        # the end of the file.
        printf '%s\n' "PermitEmptyPasswords no" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    else
        head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
        printf '%s\n' "PermitEmptyPasswords no" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
        tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    fi
    # 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
Strategy: | restrict
    - name: Disable SSH Access via Empty Passwords
      block:

        - name: Deduplicate values from /etc/ssh/sshd_config
          lineinfile:
            path: /etc/ssh/sshd_config
            create: false
            regexp: ^\s*{{ "PermitEmptyPasswords"| regex_escape }}\s+
            state: absent

        - name: Check if /etc/ssh/sshd_config.d exists
          stat:
            path: /etc/ssh/sshd_config.d
          register: _etc_ssh_sshd_config_d_exists

        - name: Check if the parameter PermitEmptyPasswords is present in /etc/ssh/sshd_config.d
          find:
            paths: /etc/ssh/sshd_config.d
            recurse: 'yes'
            follow: 'no'
            contains: ^\s*{{ "PermitEmptyPasswords"| 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
          lineinfile:
            path: '{{ item.path }}'
            create: false
            regexp: ^\s*{{ "PermitEmptyPasswords"| regex_escape }}\s+
            state: absent
          with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}'
          when: _etc_ssh_sshd_config_d_has_parameter.matched

        - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
          lineinfile:
            path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
            create: true
            regexp: ^\s*{{ "PermitEmptyPasswords"| regex_escape }}\s+
            line: PermitEmptyPasswords no
            state: present
            insertbefore: ^[#\s]*Match
            validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-90799-8
        - CJIS-5.5.6
        - NIST-800-171-3.1.1
        - NIST-800-171-3.1.5
        - NIST-800-53-AC-17(a)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - high_severity
        - low_complexity
        - low_disruption
        - no_reboot_needed
        - restrict_strategy
        - sshd_disable_empty_passwords
#### Rule Disable GSSAPI Authentication [ref]
Unless needed, SSH should not permit extraneous or unnecessary authentication mechanisms like GSSAPI. The default SSH configuration disallows authentications based on GSSAPI. The appropriate configuration is used if no value is set for `GSSAPIAuthentication`. To explicitly disable GSSAPI authentication, add or correct the following line in `/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf`:
    GSSAPIAuthentication no
Rationale: | GSSAPI authentication is used to provide additional authentication mechanisms to applications. Allowing GSSAPI authentication through SSH exposes the system's GSSAPI to remote hosts, increasing the attack surface of the system.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_disable_gssapi_auth
Identifiers and References | Identifiers: CCE-90808-7 References: 11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.1.12, CCI-000318, CCI-000368, CCI-001812, CCI-001813, CCI-001814, CCI-000366, 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), 4.3.4.3.2, 4.3.4.3.3, SR 7.6, 0418, 1055, 1402, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, CM-7(a), CM-7(b), CM-6(a), AC-17(a), PR.IP-1, FTP_ITC_EXT.1, SRG-OS-000364-GPOS-00151, SRG-OS-000480-GPOS-00227, SRG-OS-000480-VMM-002000
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    mkdir -p /etc/ssh/sshd_config.d
    touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf

    LC_ALL=C sed -i "/^\s*GSSAPIAuthentication\s\+/Id" "/etc/ssh/sshd_config"
    LC_ALL=C sed -i "/^\s*GSSAPIAuthentication\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*GSSAPIAuthentication\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 before the line matching the regex '^Match'.
    line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" | LC_ALL=C sed 's/:.*//g')"
    if [ -z "$line_number" ]; then
        # There was no match of '^Match', insert at
        # the end of the file.
        printf '%s\n' "GSSAPIAuthentication no" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    else
        head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
        printf '%s\n' "GSSAPIAuthentication no" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
        tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    fi
    # 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
Strategy: | restrict
    - name: Disable GSSAPI Authentication
      block:

        - name: Deduplicate values from /etc/ssh/sshd_config
          lineinfile:
            path: /etc/ssh/sshd_config
            create: false
            regexp: ^\s*{{ "GSSAPIAuthentication"| regex_escape }}\s+
            state: absent

        - name: Check if /etc/ssh/sshd_config.d exists
          stat:
            path: /etc/ssh/sshd_config.d
          register: _etc_ssh_sshd_config_d_exists

        - name: Check if the parameter GSSAPIAuthentication is present in /etc/ssh/sshd_config.d
          find:
            paths: /etc/ssh/sshd_config.d
            recurse: 'yes'
            follow: 'no'
            contains: ^\s*{{ "GSSAPIAuthentication"| 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
          lineinfile:
            path: '{{ item.path }}'
            create: false
            regexp: ^\s*{{ "GSSAPIAuthentication"| regex_escape }}\s+
            state: absent
          with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}'
          when: _etc_ssh_sshd_config_d_has_parameter.matched

        - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
          lineinfile:
            path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
            create: true
            regexp: ^\s*{{ "GSSAPIAuthentication"| regex_escape }}\s+
            line: GSSAPIAuthentication no
            state: present
            insertbefore: ^[#\s]*Match
            validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-90808-7
        - NIST-800-171-3.1.12
        - NIST-800-53-AC-17(a)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
        - sshd_disable_gssapi_auth
#### Rule Disable Kerberos Authentication [ref]
Unless needed, SSH should not permit extraneous or unnecessary authentication mechanisms like Kerberos. The default SSH configuration disallows authentication validation through Kerberos. The appropriate configuration is used if no value is set for `KerberosAuthentication`. To explicitly disable Kerberos authentication, add or correct the following line in `/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf`:
    KerberosAuthentication no
Rationale: | Kerberos authentication for SSH is often implemented using GSSAPI. If Kerberos is enabled through SSH, the SSH daemon provides a means of access to the system's Kerberos implementation. Vulnerabilities in the system's Kerberos implementations may be subject to exploitation.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_disable_kerb_auth
Identifiers and References | Identifiers: CCE-90802-0 References: 11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.1.12, CCI-000318, CCI-000368, CCI-001812, CCI-001813, CCI-001814, CCI-000366, 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), 4.3.4.3.2, 4.3.4.3.3, SR 7.6, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, AC-17(a), CM-7(a), CM-7(b), CM-6(a), PR.IP-1, FTP_ITC_EXT.1, SRG-OS-000364-GPOS-00151, SRG-OS-000480-GPOS-00227, SRG-OS-000480-VMM-002000
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    mkdir -p /etc/ssh/sshd_config.d
    touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf

    LC_ALL=C sed -i "/^\s*KerberosAuthentication\s\+/Id" "/etc/ssh/sshd_config"
    LC_ALL=C sed -i "/^\s*KerberosAuthentication\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*KerberosAuthentication\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 before the line matching the regex '^Match'.
    line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" | LC_ALL=C sed 's/:.*//g')"
    if [ -z "$line_number" ]; then
        # There was no match of '^Match', insert at
        # the end of the file.
        printf '%s\n' "KerberosAuthentication no" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    else
        head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
        printf '%s\n' "KerberosAuthentication no" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
        tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    fi
    # 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
Strategy: | restrict
    - name: Disable Kerberos Authentication
      block:

        - name: Deduplicate values from /etc/ssh/sshd_config
          lineinfile:
            path: /etc/ssh/sshd_config
            create: false
            regexp: ^\s*{{ "KerberosAuthentication"| regex_escape }}\s+
            state: absent

        - name: Check if /etc/ssh/sshd_config.d exists
          stat:
            path: /etc/ssh/sshd_config.d
          register: _etc_ssh_sshd_config_d_exists

        - name: Check if the parameter KerberosAuthentication is present in /etc/ssh/sshd_config.d
          find:
            paths: /etc/ssh/sshd_config.d
            recurse: 'yes'
            follow: 'no'
            contains: ^\s*{{ "KerberosAuthentication"| 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
          lineinfile:
            path: '{{ item.path }}'
            create: false
            regexp: ^\s*{{ "KerberosAuthentication"| regex_escape }}\s+
            state: absent
          with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}'
          when: _etc_ssh_sshd_config_d_has_parameter.matched

        - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
          lineinfile:
            path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
            create: true
            regexp: ^\s*{{ "KerberosAuthentication"| regex_escape }}\s+
            line: KerberosAuthentication no
            state: present
            insertbefore: ^[#\s]*Match
            validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-90802-0
        - NIST-800-171-3.1.12
        - NIST-800-53-AC-17(a)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
        - sshd_disable_kerb_auth
#### Rule Disable SSH Root Login [ref]
The root user should never be allowed to login to a system directly over a network. To disable root login via SSH, add or correct the following line in `/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf`:
    PermitRootLogin no
Rationale: | Even though the communications channel may be encrypted, an additional layer of security is gained by extending the policy of not logging directly on as root. In addition, logging in with a user-specific account provides individual accountability of actions performed on the system and also helps to minimize direct attack attempts on root's password.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_disable_root_login
Identifiers and References | Identifiers: CCE-90800-4 References: BP28(R19), NT007(R21), 1, 11, 12, 13, 14, 15, 16, 18, 3, 5, 5.5.6, APO01.06, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.02, DSS06.03, DSS06.06, DSS06.10, 3.1.1, 3.1.5, CCI-000366, CCI-000770, 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), 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, 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 5.2, 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.18.1.4, 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.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, 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 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, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3, AC-6(2), AC-17(a), IA-2, IA-2(5), CM-7(a), CM-7(b), CM-6(a), PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.DS-5, PR.PT-3, FIA_UAU.1, SRG-OS-000109-GPOS-00056, SRG-OS-000480-GPOS-00227, SRG-OS-000480-VMM-002000
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    mkdir -p /etc/ssh/sshd_config.d
    touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf

    LC_ALL=C sed -i "/^\s*PermitRootLogin\s\+/Id" "/etc/ssh/sshd_config"
    LC_ALL=C sed -i "/^\s*PermitRootLogin\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*PermitRootLogin\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 before the line matching the regex '^Match'.
    line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" | LC_ALL=C sed 's/:.*//g')"
    if [ -z "$line_number" ]; then
        # There was no match of '^Match', insert at
        # the end of the file.
        printf '%s\n' "PermitRootLogin no" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    else
        head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
        printf '%s\n' "PermitRootLogin no" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
        tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    fi
    # 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
Strategy: | restrict
    - name: Disable SSH Root Login
      block:

        - name: Deduplicate values from /etc/ssh/sshd_config
          lineinfile:
            path: /etc/ssh/sshd_config
            create: false
            regexp: ^\s*{{ "PermitRootLogin"| regex_escape }}\s+
            state: absent

        - name: Check if /etc/ssh/sshd_config.d exists
          stat:
            path: /etc/ssh/sshd_config.d
          register: _etc_ssh_sshd_config_d_exists

        - name: Check if the parameter PermitRootLogin is present in /etc/ssh/sshd_config.d
          find:
            paths: /etc/ssh/sshd_config.d
            recurse: 'yes'
            follow: 'no'
            contains: ^\s*{{ "PermitRootLogin"| 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
          lineinfile:
            path: '{{ item.path }}'
            create: false
            regexp: ^\s*{{ "PermitRootLogin"| regex_escape }}\s+
            state: absent
          with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}'
          when: _etc_ssh_sshd_config_d_has_parameter.matched

        - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
          lineinfile:
            path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
            create: true
            regexp: ^\s*{{ "PermitRootLogin"| regex_escape }}\s+
            line: PermitRootLogin no
            state: present
            insertbefore: ^[#\s]*Match
            validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-90800-4
        - CJIS-5.5.6
        - NIST-800-171-3.1.1
        - NIST-800-171-3.1.5
        - NIST-800-53-AC-17(a)
        - NIST-800-53-AC-6(2)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-IA-2
        - NIST-800-53-IA-2(5)
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
        - sshd_disable_root_login
#### Rule Do Not Allow SSH Environment Options [ref]
Ensure that users are not able to override environment variables of the SSH daemon. The default SSH configuration disables environment processing. The appropriate configuration is used if no value is set for `PermitUserEnvironment`. To explicitly disable Environment options, add or correct the following `/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf`:
    PermitUserEnvironment no
Rationale: | SSH environment options potentially allow users to bypass access restriction in some configurations.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_do_not_permit_user_env
Identifiers and References | Identifiers: CCE-90803-8 References: 11, 3, 9, 5.5.6, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.1.12, CCI-000366, 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), 4.3.4.3.2, 4.3.4.3.3, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, AC-17(a), CM-7(a), CM-7(b), CM-6(a), PR.IP-1, SRG-OS-000480-GPOS-00229, SRG-OS-000480-VMM-002000
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    mkdir -p /etc/ssh/sshd_config.d
    touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf

    LC_ALL=C sed -i "/^\s*PermitUserEnvironment\s\+/Id" "/etc/ssh/sshd_config"
    LC_ALL=C sed -i "/^\s*PermitUserEnvironment\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*PermitUserEnvironment\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 before the line matching the regex '^Match'.
    line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" | LC_ALL=C sed 's/:.*//g')"
    if [ -z "$line_number" ]; then
        # There was no match of '^Match', insert at
        # the end of the file.
        printf '%s\n' "PermitUserEnvironment no" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    else
        head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
        printf '%s\n' "PermitUserEnvironment no" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
        tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    fi
    # 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
Strategy: | restrict
    - name: Do Not Allow SSH Environment Options
      block:

        - name: Deduplicate values from /etc/ssh/sshd_config
          lineinfile:
            path: /etc/ssh/sshd_config
            create: false
            regexp: ^\s*{{ "PermitUserEnvironment"| regex_escape }}\s+
            state: absent

        - name: Check if /etc/ssh/sshd_config.d exists
          stat:
            path: /etc/ssh/sshd_config.d
          register: _etc_ssh_sshd_config_d_exists

        - name: Check if the parameter PermitUserEnvironment is present in /etc/ssh/sshd_config.d
          find:
            paths: /etc/ssh/sshd_config.d
            recurse: 'yes'
            follow: 'no'
            contains: ^\s*{{ "PermitUserEnvironment"| 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
          lineinfile:
            path: '{{ item.path }}'
            create: false
            regexp: ^\s*{{ "PermitUserEnvironment"| regex_escape }}\s+
            state: absent
          with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}'
          when: _etc_ssh_sshd_config_d_has_parameter.matched

        - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
          lineinfile:
            path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
            create: true
            regexp: ^\s*{{ "PermitUserEnvironment"| regex_escape }}\s+
            line: PermitUserEnvironment no
            state: present
            insertbefore: ^[#\s]*Match
            validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-90803-8
        - CJIS-5.5.6
        - NIST-800-171-3.1.12
        - NIST-800-53-AC-17(a)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
        - sshd_do_not_permit_user_env
#### Rule Enable Use of Strict Mode Checking [ref]
SSHs `StrictModes` option checks file and ownership permissions in the user's home directory `.ssh` folder before accepting login. If world- writable permissions are found, logon is rejected. The default SSH configuration has `StrictModes` enabled. The appropriate configuration is used if no value is set for `StrictModes`. To explicitly enable `StrictModes` in SSH, add or correct the following line in `/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf`:
    StrictModes yes
Rationale: | If other users have access to modify user-specific SSH configuration files, they may be able to log into the system as another user.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_enable_strictmodes
Identifiers and References | Identifiers: CCE-90809-5 References: 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 3.1.12, CCI-000366, 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), 4.3.3.7.3, SR 2.1, SR 5.2, 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, 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, AC-6, AC-17(a), CM-6(a), PR.AC-4, PR.DS-5, SRG-OS-000480-GPOS-00227, SRG-OS-000480-VMM-002000
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    mkdir -p /etc/ssh/sshd_config.d
    touch /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf

    LC_ALL=C sed -i "/^\s*StrictModes\s\+/Id" "/etc/ssh/sshd_config"
    LC_ALL=C sed -i "/^\s*StrictModes\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*StrictModes\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 before the line matching the regex '^Match'.
    line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" | LC_ALL=C sed 's/:.*//g')"
    if [ -z "$line_number" ]; then
        # There was no match of '^Match', insert at
        # the end of the file.
        printf '%s\n' "StrictModes yes" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    else
        head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
        printf '%s\n' "StrictModes yes" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
        tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    fi
    # 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
Strategy: | restrict
    - name: Enable Use of Strict Mode Checking
      block:

        - name: Deduplicate values from /etc/ssh/sshd_config
          lineinfile:
            path: /etc/ssh/sshd_config
            create: false
            regexp: ^\s*{{ "StrictModes"| regex_escape }}\s+
            state: absent

        - name: Check if /etc/ssh/sshd_config.d exists
          stat:
            path: /etc/ssh/sshd_config.d
          register: _etc_ssh_sshd_config_d_exists

        - name: Check if the parameter StrictModes is present in /etc/ssh/sshd_config.d
          find:
            paths: /etc/ssh/sshd_config.d
            recurse: 'yes'
            follow: 'no'
            contains: ^\s*{{ "StrictModes"| 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
          lineinfile:
            path: '{{ item.path }}'
            create: false
            regexp: ^\s*{{ "StrictModes"| regex_escape }}\s+
            state: absent
          with_items: '{{ _etc_ssh_sshd_config_d_has_parameter.files }}'
          when: _etc_ssh_sshd_config_d_has_parameter.matched

        - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
          lineinfile:
            path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
            create: true
            regexp: ^\s*{{ "StrictModes"| regex_escape }}\s+
            line: StrictModes yes
            state: present
            insertbefore: ^[#\s]*Match
            validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-90809-5
        - NIST-800-171-3.1.12
        - NIST-800-53-AC-17(a)
        - NIST-800-53-AC-6
        - NIST-800-53-CM-6(a)
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
        - sshd_enable_strictmodes
#### 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
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
Identifiers and References | Identifiers: CCE-90807-9 References: 1, 12, 15, 16, 5.5.6, DSS05.04, DSS05.10, DSS06.10, 3.1.9, CCI-000048, CCI-000050, CCI-001384, CCI-001385, CCI-001386, CCI-001387, CCI-001388, 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), 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, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, AC-8(a), AC-8(c), AC-17(a), CM-6(a), PR.AC-7, FTA_TAB.1, SRG-OS-000023-GPOS-00006, SRG-OS-000024-GPOS-00007, SRG-OS-000228-GPOS-00088, SRG-OS-000023-VMM-000060, SRG-OS-000024-VMM-000070
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    mkdir -p /etc/ssh/sshd_config.d
    touch /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 before the line matching the regex '^Match'.
    line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" | LC_ALL=C sed 's/:.*//g')"
    if [ -z "$line_number" ]; then
        # There was no match of '^Match', insert at
        # the end of the file.
        printf '%s\n' "Banner /etc/issue" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    else
        head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" > "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
        printf '%s\n' "Banner /etc/issue" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
        tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf.bak" >> "/etc/ssh/sshd_config.d/00-complianceascode-hardening.conf"
    fi
    # 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
Strategy: | restrict
    - name: Enable SSH Warning Banner
      block:

        - name: Deduplicate values from /etc/ssh/sshd_config
          lineinfile:
            path: /etc/ssh/sshd_config
            create: false
            regexp: ^\s*{{ "Banner"| regex_escape }}\s+
            state: absent

        - name: Check if /etc/ssh/sshd_config.d exists
          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
          find:
            paths: /etc/ssh/sshd_config.d
            recurse: 'yes'
            follow: 'no'
            contains: ^\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
          lineinfile:
            path: '{{ item.path }}'
            create: false
            regexp: ^\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

        - name: Insert correct line to /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
          lineinfile:
            path: /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf
            create: true
            regexp: ^\s*{{ "Banner"| regex_escape }}\s+
            line: Banner /etc/issue
            state: present
            insertbefore: ^[#\s]*Match
            validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - CCE-90807-9
        - 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
#### Rule Distribute the SSH Server configuration to multiple files in a config directory. [ref]
Make sure to have the `Include /etc/ssh/sshd_config.d/*.conf` line in the `/etc/ssh/sshd_config` file. Ideally, don't have any active configuration directives in that file, and distribute the service configuration to several files in the `/etc/ssh/sshd_config.d` directory.
Rationale: | This form of distributed configuration is considered as a good practice, and as other sshd rules assume that directives in files in the `/etc/ssh/sshd_config.d` config directory are effective, there has to be a rule that ensures this. Aside from that, having multiple configuration files makes the SSH Server configuration changes easier to partition according to the reason that they were introduced, and therefore it should help to perform merges of hardening updates.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_use_directory_configuration
Identifiers and References | Identifiers: CCE-87681-3
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if test -f /etc/ssh/sshd_config.d/sshd_config_original.conf; then
    	printf '%s\n' "Remediation probably already happened, '/etc/ssh/sshd_config.d/sshd_config_original.conf' already exists, not doing anything." >&2
    false 1
    elif grep -Eq '^\s*Include\s+/etc/ssh/sshd_config\.d/\*\.conf' /etc/ssh/sshd_config && ! grep -Eq '^\s*Match\s' /etc/ssh/sshd_config; then
    	printf '%s\n' "Remediation probably already happened, '/etc/ssh/sshd_config' already contains the include directive." >&2
    false 1
    else
    	mkdir -p /etc/ssh/sshd_config.d
    	mv /etc/ssh/sshd_config /etc/ssh/sshd_config.d/sshd_config_original.conf
    cat > /etc/ssh/sshd_config << EOF
    # To modify the system-wide sshd configuration, create a  *.conf  file under
    #  /etc/ssh/sshd_config.d/  which will be automatically included below

    Include /etc/ssh/sshd_config.d/*.conf
    EOF
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

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.