## Guide to the Secure Configuration of Oracle Linux 7

> with profile Standard System Security Profile for Oracle Linux 7
> This profile contains rules to ensure standard security baseline of Oracle Linux 7 system. Regardless of your system's workload all of these checks should pass.

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 Oracle Linux 7. 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 | Standard System Security Profile for Oracle Linux 7
Profile ID | xccdf_org.ssgproject.content_profile_standard

#### CPE Platforms

- cpe:/o:oracle:linux:7

## 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. Configure Syslog
   5. File Permissions and Masks
2. Services
   1. Base Services
   2. Cron and At Daemons

## Checklist

Group Guide to the Secure Configuration of Oracle Linux 7 Group contains 28 groups and 72 rules
Group System Settings Group contains 24 groups and 66 rules
[ref] Contains rules that check correct system settings.
Group Installing and Maintaining Software Group contains 5 groups and 7 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 2 groups and 2 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 yum repository using the command:
    $ sudo yum 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 | 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, OL07-00-010020, SV-221653r603260_rule
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' ' ')"

    yum reinstall -y $packages_to_reinstall
Remediation Ansible snippet ⇲
Complexity: | high
Disruption: | medium
Strategy: | restrict
    - name: 'Set fact: Package manager reinstall command (dnf)'
      set_fact:
        package_manager_reinstall_cmd: dnf reinstall -y
      when: ansible_distribution == "Fedora"
      tags:
        - CJIS-5.10.4.1
        - DISA-STIG-OL07-00-010020
        - NIST-800-171-3.3.8
        - NIST-800-171-3.4.1
        - NIST-800-53-AU-9(3)
        - 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_hashes

    - name: 'Set fact: Package manager reinstall command (yum)'
      set_fact:
        package_manager_reinstall_cmd: yum reinstall -y
      when: (ansible_distribution == "RedHat" or ansible_distribution == "OracleLinux")
      tags:
        - CJIS-5.10.4.1
        - DISA-STIG-OL07-00-010020
        - NIST-800-171-3.3.8
        - NIST-800-171-3.4.1
        - NIST-800-53-AU-9(3)
        - 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_hashes

    - name: Read files with incorrect hash
      command: rpm -Va --nodeps --nosize --nomtime --nordev --nocaps --nolinkto --nouser
        --nogroup --nomode --noghost --noconfig
      args:
        warn: false
      register: files_with_incorrect_hash
      changed_when: false
      failed_when: files_with_incorrect_hash.rc > 1
      check_mode: false
      when: (package_manager_reinstall_cmd is defined)
      tags:
        - CJIS-5.10.4.1
        - DISA-STIG-OL07-00-010020
        - NIST-800-171-3.3.8
        - NIST-800-171-3.4.1
        - NIST-800-53-AU-9(3)
        - 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_hashes

    - name: Create list of packages
      command: rpm -qf "{{ item }}"
      args:
        warn: false
      with_items: '{{ files_with_incorrect_hash.stdout_lines | map(''regex_findall'',
        ''^[.]+[5]+.* (\/.*)'', ''\1'') | map(''join'') | select(''match'', ''(\/.*)'')
        | list | unique }}'
      register: list_of_packages
      changed_when: false
      check_mode: false
      when:
        - files_with_incorrect_hash.stdout_lines is defined
        - (files_with_incorrect_hash.stdout_lines | length > 0)
      tags:
        - CJIS-5.10.4.1
        - DISA-STIG-OL07-00-010020
        - NIST-800-171-3.3.8
        - NIST-800-171-3.4.1
        - NIST-800-53-AU-9(3)
        - 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_hashes

    - name: Reinstall packages of files with incorrect hash
      command: '{{ package_manager_reinstall_cmd }} ''{{ item }}'''
      args:
        warn: false
      with_items: '{{ list_of_packages.results | map(attribute=''stdout_lines'') | list
        | unique }}'
      when:
        - files_with_incorrect_hash.stdout_lines is defined
        - (package_manager_reinstall_cmd is defined and (files_with_incorrect_hash.stdout_lines
          | length > 0))
      tags:
        - CJIS-5.10.4.1
        - DISA-STIG-OL07-00-010020
        - NIST-800-171-3.3.8
        - NIST-800-171-3.4.1
        - NIST-800-53-AU-9(3)
        - 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_hashes
#### 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 | 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, OL07-00-010010, SV-221652r646955_rule
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:
        - CJIS-5.10.4.1
        - DISA-STIG-OL07-00-010010
        - 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:
        - CJIS-5.10.4.1
        - DISA-STIG-OL07-00-010010
        - 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:
        - CJIS-5.10.4.1
        - DISA-STIG-OL07-00-010010
        - 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 Disk Partitioning Group contains 2 rules
[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 Ensure /var/log Located On Separate Partition [ref]
System logs are stored in the `/var/log` directory. Ensure that `/var/log` has its own partition or logical volume at installation time, or migrate it using LVM.
Rationale: | Placing `/var/log` in its own partition enables better separation between log files and other files in `/var/`.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_partition_for_var_log
Identifiers and References | References: BP28(R12), BP28(R47), 1, 12, 14, 15, 16, 3, 5, 6, 8, APO11.04, APO13.01, BAI03.05, DSS05.02, DSS05.04, DSS05.07, MEA02.01, CCI-000366, 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 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.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, CIP-007-3 R6.5, CM-6(a), AU-4, SC-5(2), PR.PT-1, PR.PT-4, SRG-OS-000480-GPOS-00227
Remediation Anaconda snippet ⇲
Complexity: | low
Disruption: | high
Strategy: | enable
    part /var/log
#### Rule Ensure /var/log/audit Located On Separate Partition [ref]
Audit logs are stored in the `/var/log/audit` directory. Ensure that `/var/log/audit` has its own partition or logical volume at installation time, or migrate it using LVM. Make absolutely certain that it is large enough to store all audit logs that will be created by the auditing daemon.
Rationale: | Placing `/var/log/audit` in its own partition enables better separation between audit files and other files, and helps ensure that auditing cannot be halted due to the partition running out of space.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_partition_for_var_log_audit
Identifiers and References | References: BP28(R43), 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 8, APO11.04, APO13.01, BAI03.05, BAI04.04, DSS05.02, DSS05.04, DSS05.07, MEA02.01, CCI-000366, CCI-001849, 164.312(a)(2)(ii), 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 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.2, SR 7.6, 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.13.1.1, A.13.2.1, A.14.1.3, A.17.2.1, CIP-007-3 R6.5, CM-6(a), AU-4, SC-5(2), PR.DS-4, PR.PT-1, PR.PT-4, SRG-OS-000341-GPOS-00132, SRG-OS-000480-GPOS-00227, SRG-OS-000341-VMM-001220, OL07-00-021330, SV-221756r603260_rule
Remediation Anaconda snippet ⇲
Complexity: | low
Disruption: | high
Strategy: | enable
    part /var/log/audit
Group Updating Software Group contains 3 rules
[ref] The `yum` 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. Oracle Linux 7 systems contain an installed software catalog called the RPM database, which records metadata of installed packages. Consistently using `yum` 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 yum Configuration [ref]
The `gpgcheck` option controls whether RPM packages' signatures are always checked prior to installation. To configure yum to check package signatures before installing them, ensure the following line appears in `/etc/yum.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 | 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, OL07-00-020050, SV-221710r603260_rule
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/yum.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/yum.conf"; then
        "${sed_command[@]}" "s/^gpgcheck\\>.*/$formatted_output/gi" "/etc/yum.conf"
    else
        # \n is precaution for case where file ends without trailing newline

        printf '%s\n' "$formatted_output" >> "/etc/yum.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:
        - CJIS-5.10.4.1
        - DISA-STIG-OL07-00-020050
        - 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/yum.conf
        section: main
        option: gpgcheck
        value: 1
        no_extra_spaces: true
        create: false
      when: '"yum" in ansible_facts.packages'
      tags:
        - CJIS-5.10.4.1
        - DISA-STIG-OL07-00-020050
        - 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 Oracle Linux GPG Key Installed [ref]
To ensure the system can cryptographically verify base software packages come from Oracle (and to connect to the Unbreakable Linux Network to receive them), the Oracle GPG key must properly be installed. To install the Oracle GPG key, run:
    $ sudo uln_register
If the system is not connected to the Internet, then install the Oracle GPG key from trusted media such as the Oracle 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-oracle
Alternatively, the key may be pre-loaded during the Oracle installation. In such cases, the key can be installed by running the following command:
    sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-oracle
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 Oracle GPG key is necessary to cryptographically verify packages are from Oracle.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_ensure_oracle_gpgkey_installed
Identifiers and References | References: 11, 2, 3, 9, APO01.06, BAI03.05, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS06.02, CCI-001749, 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), CM-11(a), CM-11(b), PR.DS-6, PR.DS-8, PR.IP-1, Req-6.2, 1.2.2
Remediation Shell script ⇲
    # OL fingerprints below retrieved from Oracle Linux Yum Server "Frequently Asked Questions"
    # https://yum.oracle.com/faq.html#a10
    readonly OL_RELEASE_FINGERPRINT="42144123FECFC55B9086313D72F97B74EC551F03"
    readonly OL_AUXILIARY_FINGERPRINT=""

    FINGERPRINTS_REGEX="${OL_RELEASE_FINGERPRINT}"

    if [[ -n "$OL_AUXILIARY_FINGERPRINT" ]]; then
        FINGERPRINTS_REGEX+="|${OL_AUXILIARY_FINGERPRINT}"
    fi

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

    RPM_GPG_DIR_PERMS=$(stat -c %a "$(dirname "$OL_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 "$OL_RELEASE_KEY" | grep "^fpr" | cut -d ":" -f 10)
      GPG_RESULT=$?
      # No CRC error, safe to proceed
      if [ "${GPG_RESULT}" -eq "0" ]
      then
        # Filter just hexadecimal fingerprints from gpg's output from
        # processing of a key file
        echo "${GPG_OUT[*]}" | grep -vE "$FINGERPRINTS_REGEX" || {
          # If $ OL_RELEASE_KEY file doesn't contain any keys with unknown fingerprint, import it
          rpm --import "${OL_RELEASE_KEY}"
        }
      fi
    fi
#### Rule Ensure Software Patches Installed [ref]
If the system is joined to the ULN or a yum server, run the following command to install updates:
    $ sudo yum update
If the system is not configured to use one of these sources, updates (in the form of RPM packages) can be manually downloaded from the ULN and installed using `rpm`. NOTE: U.S. Defense systems are required to be patched within 30 days or sooner as local policy dictates. Warning: The OVAL feed of Oracle Linux 7 is not a XML file, which may not be understood by all scanners.
Rationale: | Installing software updates is a fundamental mitigation against the exploitation of publicly-known vulnerabilities. If the most recent security patches and updates are not installed, unauthorized users may take advantage of weaknesses in the unpatched software. The lack of prompt attention to patching could result in a system compromise.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_security_patches_up_to_date
Identifiers and References | References: BP28(R08), 18, 20, 4, 5.10.4.1, APO12.01, APO12.02, APO12.03, APO12.04, BAI03.10, DSS05.01, DSS05.02, CCI-000366, CCI-001227, 4.2.3, 4.2.3.12, 4.2.3.7, 4.2.3.9, A.12.6.1, A.14.2.3, A.16.1.3, A.18.2.2, A.18.2.3, SI-2(5), SI-2(c), CM-6(a), ID.RA-1, PR.IP-12, FMT_MOF_EXT.1, Req-6.2, SRG-OS-000480-GPOS-00227, SRG-OS-000480-VMM-002000, OL07-00-020260, SV-221720r603260_rule
Remediation Shell script ⇲
Complexity: | low
Disruption: | high
Reboot: | true
Strategy: | patch
    yum -y update
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | high
Reboot: | true
Strategy: | patch
    - name: Security patches are up to date
      package:
        name: '*'
        state: latest
      tags:
        - CJIS-5.10.4.1
        - DISA-STIG-OL07-00-020260
        - NIST-800-53-CM-6(a)
        - NIST-800-53-SI-2(5)
        - NIST-800-53-SI-2(c)
        - PCI-DSS-Req-6.2
        - high_disruption
        - low_complexity
        - medium_severity
        - patch_strategy
        - reboot_required
        - security_patches_up_to_date
        - skip_ansible_lint
Group Account and Access Control Group contains 4 groups and 4 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 Oracle Linux 7.
Group Protect Accounts by Restricting Password-Based Login Group contains 1 group and 2 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 2 rules
[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 Verify All Account Password Hashes are Shadowed [ref]
If any password hashes are stored in `/etc/passwd` (in the second field, instead of an `x` or `*`), the cause of this misconfiguration should be investigated. The account should have its password reset and the hash should be properly stored, or the account should be deleted entirely.
Rationale: | The hashes for all user account passwords should be stored in the file `/etc/shadow` and never in `/etc/passwd`, which is readable by all users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_all_shadowed
Identifiers and References | References: 1, 12, 15, 16, 5, 5.5.2, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.5.10, 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, 1410, 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, IA-5(h), CM-6(a), PR.AC-1, PR.AC-6, PR.AC-7, Req-8.2.1
#### 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 | 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, OL07-00-010290, SV-221687r809141_rule
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:
        - CJIS-5.5.2
        - DISA-STIG-OL07-00-010290
        - 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:
        - CJIS-5.5.2
        - DISA-STIG-OL07-00-010290
        - 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:
        - CJIS-5.5.2
        - DISA-STIG-OL07-00-010290
        - 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:
        - CJIS-5.5.2
        - DISA-STIG-OL07-00-010290
        - 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:
        - CJIS-5.5.2
        - DISA-STIG-OL07-00-010290
        - 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:
        - CJIS-5.5.2
        - DISA-STIG-OL07-00-010290
        - 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 Secure Session Configuration Files for Login Accounts Group contains 1 group and 2 rules
[ref] When a user logs into a Unix account, the system configures the user's session by reading a number of files. Many of these files are located in the user's home directory, and may have weak permissions as a result of user error or misconfiguration. If an attacker can modify or even read certain types of account configuration information, they can often gain full access to the affected user's account. Therefore, it is important to test and correct configuration file permissions for interactive accounts, particularly those of privileged users such as root or system administrators.
Group Ensure that No Dangerous Directories Exist in Root's Path Group contains 2 rules
[ref] The active path of the root account can be obtained by starting a new root shell and running:
    # echo $PATH
This will produce a colon-separated list of directories in the path. Certain path elements could be considered dangerous, as they could lead to root executing unknown or untrusted programs, which could contain malicious code. Since root may sometimes work inside untrusted directories, the `.` character, which represents the current directory, should never be in the root path, nor should any directory which can be written to by an unprivileged or semi-privileged (system) user. It is a good practice for administrators to always execute privileged commands by typing the full path to the command.
#### Rule Ensure that Root's Path Does Not Include World or Group-Writable Directories [ref]
For each element in root's path, run:
    # ls -ld DIR
and ensure that write permissions are disabled for group and other.
Rationale: | Such entries increase the risk that root could execute code provided by unprivileged users, and potentially malicious code.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_root_path_dirs_no_write
Identifiers and References | References: 11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, CCI-000366, 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-6(a), CM-6(a), PR.IP-1
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | medium
Strategy: | restrict
    - name: Print error message if user is not root
      fail:
        msg: Root account required to read root $PATH
      when: ansible_env.USER != "root"
      ignore_errors: true
      tags:
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-6(a)
        - accounts_root_path_dirs_no_write
        - low_complexity
        - medium_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Get root paths which are not symbolic links
      stat:
        path: '{{ item }}'
      changed_when: false
      failed_when: false
      register: root_paths
      with_items: '{{ ansible_env.PATH.split('':'') }}'
      when: ansible_env.USER == "root"
      tags:
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-6(a)
        - accounts_root_path_dirs_no_write
        - low_complexity
        - medium_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Disable writability to root directories
      file:
        path: '{{ item.item }}'
        mode: g-w,o-w
      with_items: '{{ root_paths.results }}'
      when:
        - root_paths.results is defined
        - item.stat.exists
        - not item.stat.islnk
      tags:
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-6(a)
        - accounts_root_path_dirs_no_write
        - low_complexity
        - medium_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure that Root's Path Does Not Include Relative Paths or Null Directories [ref]
Ensure that none of the directories in root's path is equal to a single `.` character, or that it contains any instances that lead to relative path traversal, such as `..` or beginning a path without the slash (`/`) character. Also ensure that there are no "empty" elements in the path, such as in these examples:
    PATH=:/bin
    PATH=/bin:
    PATH=/bin::/sbin
These empty elements have the same effect as a single `.` character.
Rationale: | Including these entries increases the risk that root could execute code from an untrusted location.
Severity: | unknown
Rule ID: | xccdf_org.ssgproject.content_rule_root_path_no_dot
Identifiers and References | References: 11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, CCI-000366, 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-6(a), CM-6(a), PR.IP-1
Group System Accounting with auditd Group contains 7 groups and 46 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 6 groups and 46 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 | 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, OL07-00-030410, SV-221782r810497_rule
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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030410
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030410
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030410
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030410
        - 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 | 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, OL07-00-030370, SV-221778r810481_rule
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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030370
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030370
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030370
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030370
        - 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 | 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, OL07-00-030410, SV-221782r810497_rule
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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030410
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030410
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030410
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030410
        - 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 | 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, OL07-00-030410, SV-221782r810497_rule
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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030410
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030410
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030410
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030410
        - 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 | 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, OL07-00-030370, SV-221778r810481_rule
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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030370
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030370
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030370
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030370
        - 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 | 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, OL07-00-030370, SV-221778r810481_rule
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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030370
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030370
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030370
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030370
        - 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
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
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
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
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 | 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, OL07-00-030440, SV-221785r810486_rule
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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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
      when: '"audit" in ansible_facts.packages'
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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
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
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
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
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 | 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, OL07-00-030440, SV-221785r810486_rule
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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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
      when: '"audit" in ansible_facts.packages'
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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 | 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, OL07-00-030370, SV-221778r810481_rule
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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030370
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030370
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030370
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030370
        - 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 | 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, OL07-00-030440, SV-221785r810486_rule
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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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
      when: '"audit" in ansible_facts.packages'
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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
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
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
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
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 | 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, OL07-00-030440, SV-221785r810486_rule
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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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
      when: '"audit" in ansible_facts.packages'
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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
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
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
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
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 | 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, OL07-00-030440, SV-221785r810486_rule
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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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
      when: '"audit" in ansible_facts.packages'
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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
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
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
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
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 | 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, OL07-00-030440, SV-221785r810486_rule
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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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
      when: '"audit" in ansible_facts.packages'
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030440
        - 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 File Deletion Events by User Group contains 1 rule
[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 [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 -S renameat -F auid>=1000 -F auid!=unset -F key=delete
Warning: This rule checks for multiple syscalls related to file deletion; it was written with DISA STIG in mind. Other policies should use a separate rule for each syscall that needs to be checked. For example: `audit_rules_file_deletion_events_rmdir` `audit_rules_file_deletion_events_unlink` `audit_rules_file_deletion_events_unlinkat`
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
Identifiers and References | 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-000366, CCI-000172, CCI-002884, 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.7
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    # Perform the remediation for 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 unlink unlinkat rename renameat"
    	KEY="delete"
    	SYSCALL_GROUPING="rmdir unlink unlinkat rename renameat"
    	# 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
Group Record Unauthorized Access Attempts Events to Files (unsuccessful) Group contains 1 rule
[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 Ensure auditd Collects Unauthorized Access Attempts to Files (unsuccessful) [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,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 the system is 64 bit then also add the following lines:
    -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
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,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 the system is 64 bit then also add the following lines:
    -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
Warning: This rule checks for multiple syscalls related to unsuccessful file modification; it was written with DISA STIG in mind. Other policies should use a separate rule for each syscall that needs to be checked. For example: `audit_rules_unsuccessful_file_modification_open` `audit_rules_unsuccessful_file_modification_ftruncate` `audit_rules_unsuccessful_file_modification_creat`
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
Identifiers and References | 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-000172, CCI-002884, 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, Req-10.2.4, Req-10.2.1
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if rpm --quiet -q audit; then

    # 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

    	# First fix the -EACCES requirement
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS="-F exit=-EACCES"
    	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    	SYSCALL="creat open openat open_by_handle_at truncate ftruncate"
    	KEY="access"
    	SYSCALL_GROUPING="creat open openat open_by_handle_at truncate ftruncate"
    	# 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

    	# Then fix the -EPERM requirement
    	# No need to change content of $GROUP variable - it's the same as for -EACCES case above
    	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
    	OTHER_FILTERS="-F exit=-EPERM"
    	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    	SYSCALL="creat open openat open_by_handle_at truncate ftruncate"
    	KEY="access"
    	SYSCALL_GROUPING="creat open openat open_by_handle_at truncate ftruncate"
    	# 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
Group Record Information on Kernel Modules Loading and Unloading Group contains 1 rule
[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 Loading and Unloading [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,finit_module,delete_module -F key=modules
The 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`.
Rationale: | The addition/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
Identifiers and References | 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-000172, 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
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 finit_module delete_module"
            KEY="modules"
            SYSCALL_GROUPING="init_module finit_module 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
Reboot: | true
Strategy: | restrict
    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
        - CJIS-5.4.1.1
        - NIST-800-171-3.1.7
        - NIST-800-53-AC-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
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - 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:
        - 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_kernel_module_loading
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for kernel module loading for x86 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - init_module
              - delete_module
              - finit_module
            syscall_grouping:
              - init_module
              - delete_module
              - finit_module

        - name: Check existence of init_module, delete_module, finit_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/modules.rules
          set_fact: audit_file="/etc/audit/rules.d/modules.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=modules
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - init_module
              - delete_module
              - finit_module
            syscall_grouping:
              - init_module
              - delete_module
              - finit_module

        - name: Check existence of init_module, delete_module, finit_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=modules
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when: '"audit" in ansible_facts.packages'
      tags:
        - 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_kernel_module_loading
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy

    - name: Perform remediation of Audit rules for kernel module loading for x86_64 platform
      block:

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - init_module
              - delete_module
              - finit_module
            syscall_grouping:
              - init_module
              - delete_module
              - finit_module

        - name: Check existence of init_module, delete_module, finit_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/modules.rules
          set_fact: audit_file="/etc/audit/rules.d/modules.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=modules
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0

        - name: Declare list of syscalls
          set_fact:
            syscalls:
              - init_module
              - delete_module
              - finit_module
            syscall_grouping:
              - init_module
              - delete_module
              - finit_module

        - name: Check existence of init_module, delete_module, finit_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=modules
            create: true
            mode: o-rwx
            state: present
          when: syscalls_found | length == 0
      when:
        - '"audit" in ansible_facts.packages'
        - audit_arch == "b64"
      tags:
        - 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_kernel_module_loading
        - low_complexity
        - low_disruption
        - medium_severity
        - reboot_required
        - restrict_strategy
Group Record Information on the Use of Privileged Commands Group contains 20 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 [ref]
The audit system should collect information about usage of privileged commands for all users and root. To find the relevant setuid / setgid programs, run the following command for each local partition PART:
    $ sudo find PART -xdev -type f -perm -4000 -o -type f -perm -2000 2>/dev/null
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` for each setuid / setgid program on the system, replacing the SETUID_PROG_PATH part with the full path of that setuid / setgid program in the list:
    -a always,exit -F path=SETUID_PROG_PATH -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` for each setuid / setgid program on the system, replacing the SETUID_PROG_PATH part with the full path of that setuid / setgid program in the list:
    -a always,exit -F path=SETUID_PROG_PATH -F auid>=1000 -F auid!=unset -F key=privileged
Warning: This rule checks for multiple syscalls related to privileged commands; it was written with DISA STIG in mind. Other policies should use a separate rule for each syscall that needs to be checked. For example: `audit_rules_privileged_commands_su` `audit_rules_privileged_commands_umount` `audit_rules_privileged_commands_passwd`
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
Identifiers and References | References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO08.04, 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.05, 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-002234, 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.5, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.3.4.5.9, 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 3.9, 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.1, A.16.1.2, A.16.1.3, A.16.1.4, A.16.1.5, A.16.1.7, A.6.1.3, A.6.2.1, A.6.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.AE-2, DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, DE.DP-4, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, RS.CO-2, Req-10.2.2, SRG-OS-000327-GPOS-00127, 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'
    files_to_inspect=()

    # If the audit tool is 'auditctl', then:
    # * add '/etc/audit/audit.rules'to the list of files to be inspected,
    # * specify '/etc/audit/audit.rules' as the output audit file, where
    #   missing rules should be inserted
    files_to_inspect=("/etc/audit/audit.rules")
    output_audit_file="/etc/audit/audit.rules"

    # Obtain the list of SUID/SGID binaries on the particular system (split by newline)
    # into privileged_binaries array
    privileged_binaries=()
    readarray -t privileged_binaries < <(find / -not \( -fstype afs -o -fstype ceph -o -fstype cifs -o -fstype smb3 -o -fstype smbfs -o -fstype sshfs -o -fstype ncpfs -o -fstype ncp -o -fstype nfs -o -fstype nfs4 -o -fstype gfs -o -fstype gfs2 -o -fstype glusterfs -o -fstype gpfs -o -fstype pvfs2 -o -fstype ocfs2 -o -fstype lustre -o -fstype davfs -o -fstype fuse.sshfs \) -type f \( -perm -4000 -o -perm -2000 \) 2> /dev/null)

    # Keep list of SUID/SGID binaries that have been already handled within some previous iteration
    sbinaries_to_skip=()

    # For each found sbinary in privileged_binaries list
    for sbinary in "${privileged_binaries[@]}"
    do

        # Check if this sbinary wasn't already handled in some of the previous sbinary iterations
        # Return match only if whole sbinary definition matched (not in the case just prefix matched!!!)
        if [[ $(sed -ne "\|${sbinary}|p" <<< "${sbinaries_to_skip[*]}") ]]
        then
            # If so, don't process it second time & go to process next sbinary
            continue
        fi

        # Reset the counter of inspected files when starting to check
        # presence of existing audit rule for new sbinary
        count_of_inspected_files=0

        # Define expected rule form for this binary
        expected_rule="-a always,exit -F path=${sbinary} -F auid>=1000 -F auid!=unset -F key=privileged"

        # If list of audit rules files to be inspected is empty, just add new rule and move on to next binary
        if [[ ${#files_to_inspect[@]} -eq 0 ]]; then
            echo "$expected_rule" >> "$output_audit_file"
            continue
        fi

        # Replace possible slash '/' character in sbinary definition so we could use it in sed expressions below
        sbinary_esc=${sbinary//$'/'/$'\/'}

        # For each audit rules file from the list of files to be inspected
        for afile in "${files_to_inspect[@]}"
        do
            # Search current audit rules file's content for match. Match criteria:
            # * existing rule is for the same SUID/SGID binary we are currently processing (but
            #   can contain multiple -F path= elements covering multiple SUID/SGID binaries)
            # * existing rule contains all arguments from expected rule form (though can contain
            #   them in arbitrary order)

            base_search=$(sed -e '/-a always,exit/!d' -e '/-F path='"${sbinary_esc}"'[^[:graph:]]/!d'		\
                    -e '/-F path=[^[:space:]]\+/!d'						\
                    -e '/-F auid>='"1000"'/!d' -e '/-F auid!=\(4294967295\|unset\)/!d'	\
                    -e '/-k \|-F key=/!d' "$afile")

            # Increase the count of inspected files for this sbinary
            count_of_inspected_files=$((count_of_inspected_files + 1))

            # Search current audit rules file's content for presence of rule pattern for this sbinary
            if [[ $base_search ]]
            then

                # Current audit rules file already contains rule for this binary =>
                # Store the exact form of found rule for this binary for further processing
                concrete_rule=$base_search

                # Select all other SUID/SGID binaries possibly also present in the found rule

                readarray -t handled_sbinaries < <(grep -o -e "-F path=[^[:space:]]\+" <<< "$concrete_rule")
                handled_sbinaries=("${handled_sbinaries[@]//-F path=/}")

                # Merge the list of such SUID/SGID binaries found in this iteration with global list ignoring duplicates
                readarray -t sbinaries_to_skip < <(for i in "${sbinaries_to_skip[@]}" "${handled_sbinaries[@]}"; do echo "$i"; done | sort -du)

                # if there is a -F perm flag, remove it
                if grep -q '.*-F\s\+perm=[rwxa]\+.*' <<< "$concrete_rule"; then

                    # Separate concrete_rule into three sections using hash '#'
                    # sign as a delimiter around rule's permission section borders
                    # note that the trailing space after perm flag is captured because there would be
                    # two consecutive spaces after joining remaining parts of the rule together
                    concrete_rule="$(echo "$concrete_rule" | sed -n "s/\(.*\)\+\(-F perm=[rwax]\+\ \?\)\+/\1#\2#/p")"

                    # Split concrete_rule into head and tail sections using hash '#' delimiter
                    # The second column contains the permission section, which we don't need to extract
                    rule_head=$(cut -d '#' -f 1 <<< "$concrete_rule")
                    rule_tail=$(cut -d '#' -f 3 <<< "$concrete_rule")

                    # Remove permissions section from existing rule in the file
                    sed -i "s#${rule_head}\(.*\)${rule_tail}#${rule_head}${rule_tail}#" "$afile"
                fi
            # If the required audit rule for particular sbinary wasn't found yet, insert it under following conditions:
            #
            # * in the "auditctl" mode of operation insert particular rule each time
            #   (because in this mode there's only one file -- /etc/audit/audit.rules to be inspected for presence of this rule),
            #
            # * in the "augenrules" mode of operation insert particular rule only once and only in case we have already
            #   searched all of the files from /etc/audit/rules.d/*.rules location (since that audit rule can be defined
            #   in any of those files and if not, we want it to be inserted only once into /etc/audit/rules.d/privileged.rules file)
            #

    	else
                # Check if this sbinary wasn't already handled in some of the previous afile iterations
                # Return match only if whole sbinary definition matched (not in the case just prefix matched!!!)
                if [[ ! $(sed -ne "\|${sbinary}|p" <<< "${sbinaries_to_skip[*]}") ]]
                then
                    # Current audit rules file's content doesn't contain expected rule for this
                    # SUID/SGID binary yet => append it
                    echo "$expected_rule" >> "$output_audit_file"
                fi
                continue
            fi
        done
    done
    files_to_inspect=()
    # If the audit tool is 'augenrules', then:
    # * add '/etc/audit/rules.d/*.rules' to the list of files to be inspected
    #   (split by newline),
    # * specify /etc/audit/rules.d/privileged.rules' as the output file, where
    #   missing rules should be inserted
    readarray -t files_to_inspect < <(find /etc/audit/rules.d -maxdepth 1 -type f -name '*.rules' -print)
    output_audit_file="/etc/audit/rules.d/privileged.rules"

    # Obtain the list of SUID/SGID binaries on the particular system (split by newline)
    # into privileged_binaries array
    privileged_binaries=()
    readarray -t privileged_binaries < <(find / -not \( -fstype afs -o -fstype ceph -o -fstype cifs -o -fstype smb3 -o -fstype smbfs -o -fstype sshfs -o -fstype ncpfs -o -fstype ncp -o -fstype nfs -o -fstype nfs4 -o -fstype gfs -o -fstype gfs2 -o -fstype glusterfs -o -fstype gpfs -o -fstype pvfs2 -o -fstype ocfs2 -o -fstype lustre -o -fstype davfs -o -fstype fuse.sshfs \) -type f \( -perm -4000 -o -perm -2000 \) 2> /dev/null)

    # Keep list of SUID/SGID binaries that have been already handled within some previous iteration
    sbinaries_to_skip=()

    # For each found sbinary in privileged_binaries list
    for sbinary in "${privileged_binaries[@]}"
    do

        # Check if this sbinary wasn't already handled in some of the previous sbinary iterations
        # Return match only if whole sbinary definition matched (not in the case just prefix matched!!!)
        if [[ $(sed -ne "\|${sbinary}|p" <<< "${sbinaries_to_skip[*]}") ]]
        then
            # If so, don't process it second time & go to process next sbinary
            continue
        fi

        # Reset the counter of inspected files when starting to check
        # presence of existing audit rule for new sbinary
        count_of_inspected_files=0

        # Define expected rule form for this binary
        expected_rule="-a always,exit -F path=${sbinary} -F auid>=1000 -F auid!=unset -F key=privileged"

        # If list of audit rules files to be inspected is empty, just add new rule and move on to next binary
        if [[ ${#files_to_inspect[@]} -eq 0 ]]; then
            echo "$expected_rule" >> "$output_audit_file"
            continue
        fi

        # Replace possible slash '/' character in sbinary definition so we could use it in sed expressions below
        sbinary_esc=${sbinary//$'/'/$'\/'}

        # For each audit rules file from the list of files to be inspected
        for afile in "${files_to_inspect[@]}"
        do
            # Search current audit rules file's content for match. Match criteria:
            # * existing rule is for the same SUID/SGID binary we are currently processing (but
            #   can contain multiple -F path= elements covering multiple SUID/SGID binaries)
            # * existing rule contains all arguments from expected rule form (though can contain
            #   them in arbitrary order)

            base_search=$(sed -e '/-a always,exit/!d' -e '/-F path='"${sbinary_esc}"'[^[:graph:]]/!d'		\
                    -e '/-F path=[^[:space:]]\+/!d'						\
                    -e '/-F auid>='"1000"'/!d' -e '/-F auid!=\(4294967295\|unset\)/!d'	\
                    -e '/-k \|-F key=/!d' "$afile")

            # Increase the count of inspected files for this sbinary
            count_of_inspected_files=$((count_of_inspected_files + 1))

            # Search current audit rules file's content for presence of rule pattern for this sbinary
            if [[ $base_search ]]
            then

                # Current audit rules file already contains rule for this binary =>
                # Store the exact form of found rule for this binary for further processing
                concrete_rule=$base_search

                # Select all other SUID/SGID binaries possibly also present in the found rule

                readarray -t handled_sbinaries < <(grep -o -e "-F path=[^[:space:]]\+" <<< "$concrete_rule")
                handled_sbinaries=("${handled_sbinaries[@]//-F path=/}")

                # Merge the list of such SUID/SGID binaries found in this iteration with global list ignoring duplicates
                readarray -t sbinaries_to_skip < <(for i in "${sbinaries_to_skip[@]}" "${handled_sbinaries[@]}"; do echo "$i"; done | sort -du)

                # if there is a -F perm flag, remove it
                if grep -q '.*-F\s\+perm=[rwxa]\+.*' <<< "$concrete_rule"; then

                    # Separate concrete_rule into three sections using hash '#'
                    # sign as a delimiter around rule's permission section borders
                    # note that the trailing space after perm flag is captured because there would be
                    # two consecutive spaces after joining remaining parts of the rule together
                    concrete_rule="$(echo "$concrete_rule" | sed -n "s/\(.*\)\+\(-F perm=[rwax]\+\ \?\)\+/\1#\2#/p")"

                    # Split concrete_rule into head and tail sections using hash '#' delimiter
                    # The second column contains the permission section, which we don't need to extract
                    rule_head=$(cut -d '#' -f 1 <<< "$concrete_rule")
                    rule_tail=$(cut -d '#' -f 3 <<< "$concrete_rule")

                    # Remove permissions section from existing rule in the file
                    sed -i "s#${rule_head}\(.*\)${rule_tail}#${rule_head}${rule_tail}#" "$afile"
                fi
            # If the required audit rule for particular sbinary wasn't found yet, insert it under following conditions:
            #
            # * in the "auditctl" mode of operation insert particular rule each time
            #   (because in this mode there's only one file -- /etc/audit/audit.rules to be inspected for presence of this rule),
            #
            # * in the "augenrules" mode of operation insert particular rule only once and only in case we have already
            #   searched all of the files from /etc/audit/rules.d/*.rules location (since that audit rule can be defined
            #   in any of those files and if not, we want it to be inserted only once into /etc/audit/rules.d/privileged.rules file)
            #
    	elif [[ $count_of_inspected_files -eq "${#files_to_inspect[@]}" ]]
            then

                # Check if this sbinary wasn't already handled in some of the previous afile iterations
                # Return match only if whole sbinary definition matched (not in the case just prefix matched!!!)
                if [[ ! $(sed -ne "\|${sbinary}|p" <<< "${sbinaries_to_skip[*]}") ]]
                then
                    # Current audit rules file's content doesn't contain expected rule for this
                    # SUID/SGID binary yet => append it
                    echo "$expected_rule" >> "$output_audit_file"
                fi
                continue
            fi
        done
    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:
        - 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.2
        - audit_rules_privileged_commands
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Search for privileged commands
      shell: |
        set -o pipefail
        find / -not \( -fstype afs -o -fstype ceph -o -fstype cifs -o -fstype smb3 -o -fstype smbfs -o -fstype sshfs -o -fstype ncpfs -o -fstype ncp -o -fstype nfs -o -fstype nfs4 -o -fstype gfs -o -fstype gfs2 -o -fstype glusterfs -o -fstype gpfs -o -fstype pvfs2 -o -fstype ocfs2 -o -fstype lustre -o -fstype davfs -o -fstype fuse.sshfs \) -type f \( -perm -4000 -o -perm -2000 \) 2> /dev/null
      args:
        warn: false
        executable: /bin/bash
      check_mode: false
      register: find_result
      changed_when: false
      failed_when: false
      when: '"audit" in ansible_facts.packages'
      tags:
        - 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.2
        - audit_rules_privileged_commands
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Search /etc/audit/rules.d for audit rule entries
      find:
        paths: /etc/audit/rules.d
        recurse: false
        contains: ^.*path={{ item }} .*$
        patterns: '*.rules'
      with_items:
        - '{{ find_result.stdout_lines }}'
      register: files_result
      when: '"audit" in ansible_facts.packages'
      tags:
        - 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.2
        - audit_rules_privileged_commands
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Overwrites the rule in rules.d
      lineinfile:
        path: '{{ item.1.path }}'
        line: -a always,exit -F path={{ item.0.item }} -F auid>=1000 -F auid!=unset -F
          key=privileged
        create: false
        regexp: ^.*path={{ item.0.item }} .*$
      with_subelements:
        - '{{ files_result.results }}'
        - files
      when: '"audit" in ansible_facts.packages'
      tags:
        - 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.2
        - audit_rules_privileged_commands
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Adds the rule in rules.d
      lineinfile:
        path: /etc/audit/rules.d/privileged.rules
        line: -a always,exit -F path={{ item.item }} -F auid>=1000 -F auid!=unset -F key=privileged
        create: true
      with_items:
        - '{{ files_result.results }}'
      when:
        - '"audit" in ansible_facts.packages'
        - files_result.results is defined and item.matched == 0
      tags:
        - 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.2
        - audit_rules_privileged_commands
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Inserts/replaces the rule in audit.rules
      lineinfile:
        path: /etc/audit/audit.rules
        line: -a always,exit -F path={{ item.item }} -F auid>=1000 -F auid!=unset -F key=privileged
        create: true
        regexp: ^.*path={{ item.item }} .*$
      with_items:
        - '{{ files_result.results }}'
      when: '"audit" in ansible_facts.packages'
      tags:
        - 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.2
        - audit_rules_privileged_commands
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - at [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/at -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/at -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_at
Identifiers and References | References: CCI-000172, AU-2(d), AU-12(c), AC-6(9), CM-6(a), FAU_GEN.1.1.c, 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/at"
    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:
        - 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_at
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/bin/at
      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/at -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/at -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/at -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/at -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/at -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/at -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:
        - 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_at
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### 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 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 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 | 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, OL07-00-030660, SV-221806r603260_rule
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"
    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:
        - DISA-STIG-OL07-00-030660
        - 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 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 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 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 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 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 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:
        - DISA-STIG-OL07-00-030660
        - 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 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 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 | 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, OL07-00-030720, SV-221812r603260_rule
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"
    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:
        - DISA-STIG-OL07-00-030720
        - 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 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 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 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 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 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 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:
        - DISA-STIG-OL07-00-030720
        - 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 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 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 | 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, OL07-00-030800, SV-221818r603260_rule
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"
    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:
        - DISA-STIG-OL07-00-030800
        - 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 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 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
              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 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 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
              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:
        - DISA-STIG-OL07-00-030800
        - 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 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 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 | 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, OL07-00-030650, SV-221805r603260_rule
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"
    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:
        - DISA-STIG-OL07-00-030650
        - 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 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 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
              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 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 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
              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:
        - DISA-STIG-OL07-00-030650
        - 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 - mount [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/mount -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/mount -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_mount
Identifiers and References | References: CCI-000135, CCI-000172, CCI-002884, AU-2(d), AU-12(c), AC-6(9), CM-6(a), FAU_GEN.1.1.c, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000392-GPOS-00172, SRG-OS-000471-VMM-001910, OL07-00-030740, SV-221813r603260_rule
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/mount"
    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:
        - DISA-STIG-OL07-00-030740
        - 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_mount
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/bin/mount
      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/mount -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/mount -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/mount -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/mount -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/mount -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/mount -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:
        - DISA-STIG-OL07-00-030740
        - 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_mount
        - 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 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 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 | 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, OL07-00-030710, SV-221811r603260_rule
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"
    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:
        - DISA-STIG-OL07-00-030710
        - 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 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 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
              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 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 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
              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:
        - DISA-STIG-OL07-00-030710
        - 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 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 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 | 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, OL07-00-030810, SV-221819r603260_rule
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"
    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:
        - DISA-STIG-OL07-00-030810
        - 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 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 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 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 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 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 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:
        - DISA-STIG-OL07-00-030810
        - 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 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 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 | 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, OL07-00-030630, SV-221803r603260_rule
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"
    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:
        - DISA-STIG-OL07-00-030630
        - 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 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 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
              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 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 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
              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:
        - DISA-STIG-OL07-00-030630
        - 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 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 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 | 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, OL07-00-030760, SV-221815r603260_rule
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"
    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:
        - DISA-STIG-OL07-00-030760
        - 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 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 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 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 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 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 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:
        - DISA-STIG-OL07-00-030760
        - 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 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 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 | 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, OL07-00-030770, SV-221816r603260_rule
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"
    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:
        - DISA-STIG-OL07-00-030770
        - 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 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 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 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 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 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 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:
        - DISA-STIG-OL07-00-030770
        - 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 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 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 | 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, OL07-00-030780, SV-221817r603260_rule
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"
    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:
        - DISA-STIG-OL07-00-030780
        - 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 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 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 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 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 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 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:
        - DISA-STIG-OL07-00-030780
        - 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 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 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 | 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, OL07-00-030680, SV-221808r603260_rule
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"
    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:
        - DISA-STIG-OL07-00-030680
        - 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 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 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 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 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 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 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:
        - DISA-STIG-OL07-00-030680
        - 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 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 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 | 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, OL07-00-030690, SV-221809r603260_rule
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"
    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:
        - DISA-STIG-OL07-00-030690
        - 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 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 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 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 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 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 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:
        - DISA-STIG-OL07-00-030690
        - 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 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 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 | 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"
    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:
        - 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 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 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
              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 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 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
              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:
        - 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 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 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 | 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, OL07-00-030750, SV-221814r603260_rule
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"
    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:
        - DISA-STIG-OL07-00-030750
        - 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 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 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
              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 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 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
              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:
        - DISA-STIG-OL07-00-030750
        - 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 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 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 | 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, OL07-00-030640, SV-221804r603260_rule
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"
    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:
        - DISA-STIG-OL07-00-030640
        - 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 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
              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 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 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 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 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:
        - DISA-STIG-OL07-00-030640
        - 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 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 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 | 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, OL07-00-030670, SV-221807r603260_rule
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"
    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:
        - DISA-STIG-OL07-00-030670
        - 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 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 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 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 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 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 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:
        - DISA-STIG-OL07-00-030670
        - 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
#### Rule Ensure auditd Collects Information on the Use of Privileged Commands - usernetctl [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/usernetctl -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/usernetctl -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_usernetctl
Identifiers and References | References: CCI-000172, 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), FAU_GEN.1.1.c, 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/usernetctl"
    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:
        - 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_usernetctl
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/sbin/usernetctl
      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/usernetctl -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/usernetctl -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/usernetctl
              -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/usernetctl -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/usernetctl -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/usernetctl
              -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:
        - 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_usernetctl
        - 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 | 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:
        - 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:
        - 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:
        - 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:
        - 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
#### 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 | 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:
        - 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:
        - 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:
        - 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:
        - 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
#### 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 | 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:
        - 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:
        - 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:
        - 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:
        - 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
#### 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 | 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:
        - 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:
        - 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
#### 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 | 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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
#### 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 | 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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
#### 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 | 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, OL07-00-030740, SV-221813r603260_rule
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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030740
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030740
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030740
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030740
        - 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 | 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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:
        - 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 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 | 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, OL07-00-030700, SV-221810r603260_rule
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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030700
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030700
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030700
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030700
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030700
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030700
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030700
        - 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:
        - CJIS-5.4.1.1
        - DISA-STIG-OL07-00-030700
        - 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
#### Rule Record Events that Modify User/Group Information [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
    -w /etc/passwd -p wa -k audit_rules_usergroup_modification
    -w /etc/gshadow -p wa -k audit_rules_usergroup_modification
    -w /etc/shadow -p wa -k audit_rules_usergroup_modification
    -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/group -p wa -k audit_rules_usergroup_modification
    -w /etc/passwd -p wa -k audit_rules_usergroup_modification
    -w /etc/gshadow -p wa -k audit_rules_usergroup_modification
    -w /etc/shadow -p wa -k audit_rules_usergroup_modification
    -w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification
Warning: This rule checks for multiple syscalls related to account changes; it was written with DISA STIG in mind. Other policies should use a separate rule for each syscall that needs to be checked. For example: `audit_rules_usergroup_modification_group` `audit_rules_usergroup_modification_gshadow` `audit_rules_usergroup_modification_passwd`
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
Identifiers and References | 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-000130, CCI-000172, CCI-001403, CCI-002130, 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, Req-10.2.5, SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000239-GPOS-00089, SRG-OS-000241-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000476-GPOS-00221, OL07-00-030710, SV-221811r603260_rule
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
    # 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
    # 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
    # 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
    # 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
Group Configure Syslog Group contains 2 rules
[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 Oracle Linux 7, 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.
#### Rule Ensure rsyslog is Installed [ref]
Rsyslog is installed by default. The `rsyslog` package can be installed with the following command:
     $ sudo yum install rsyslog
Rationale: | The rsyslog package provides the rsyslog daemon, which provides system logging services.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_rsyslog_installed
Identifiers and References | References: BP28(R5), NT28(R46), 1, 14, 15, 16, 3, 5, 6, APO11.04, BAI03.05, DSS05.04, DSS05.07, MEA02.01, CCI-001311, CCI-001312, CCI-000366, 164.312(a)(2)(ii), 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, CM-6(a), PR.PT-1, FTP_ITC_EXT.1.1, SRG-OS-000479-GPOS-00224, SRG-OS-000051-GPOS-00024, SRG-OS-000480-GPOS-00227
Remediation Shell script ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if ! rpm -q --quiet "rsyslog" ; then
        yum install -y "rsyslog"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    - name: Ensure rsyslog is installed
      package:
        name: rsyslog
        state: present
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - NIST-800-53-CM-6(a)
        - enable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - package_rsyslog_installed
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    include install_rsyslog

    class install_rsyslog {
      package { 'rsyslog':
        ensure => 'installed',
      }
    }
Remediation Anaconda snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    package --add=rsyslog
Remediation script ⇲
    [[packages]]
    name = "rsyslog"
    version = "*"
#### Rule Enable rsyslog Service [ref]
The `rsyslog` service provides syslog-style logging by default on Oracle Linux 7. The `rsyslog` service can be enabled with the following command:
    $ sudo systemctl enable rsyslog.service
Rationale: | The `rsyslog` service must be running in order to provide logging services, which are essential to system administration.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_rsyslog_enabled
Identifiers and References | References: BP28(R5), NT28(R46), 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO13.01, BAI03.05, BAI04.04, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, CCI-001311, CCI-001312, CCI-001557, CCI-001851, CCI-000366, 164.312(a)(2)(ii), 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, SR 7.1, SR 7.2, 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.14.2.7, A.15.2.1, A.15.2.2, A.17.2.1, CM-6(a), AU-4(1), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.DS-4, PR.PT-1, SRG-OS-000480-GPOS-00227
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 'rsyslog.service'
    "$SYSTEMCTL_EXEC" start 'rsyslog.service'
    "$SYSTEMCTL_EXEC" enable 'rsyslog.service'

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

        - name: Gather the package facts
          package_facts:
            manager: auto

        - name: Enable service rsyslog
          service:
            name: rsyslog
            enabled: 'yes'
            state: started
            masked: 'no'
          when:
            - '"rsyslog" in ansible_facts.packages'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - NIST-800-53-AU-4(1)
        - NIST-800-53-CM-6(a)
        - enable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_rsyslog_enabled
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    include enable_rsyslog

    class enable_rsyslog {
      service {'rsyslog':
        enable => true,
        ensure => 'running',
      }
    }
Remediation script ⇲
    [customizations.services]
    enabled = ["rsyslog"]
Group File Permissions and Masks Group contains 3 groups and 7 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 Oracle Linux 7 installations:
    $ mount -t xfs | awk '{print $3}'
For any systems that use a different local filesystem type, modify this command as appropriate.
Group Verify Permissions on Important Files and Directories Group contains 4 rules
[ref] Permissions for many files on a system must be set restrictively to ensure sensitive information is properly protected. This section discusses important permission restrictions which can be verified to ensure that no harmful discrepancies have arisen.
#### Rule Verify that All World-Writable Directories Have Sticky Bits Set [ref]
When the so-called 'sticky bit' is set on a directory, only the owner of a given file may remove that file from the directory. Without the sticky bit, any user with write access to a directory may remove any file in the directory. Setting the sticky bit prevents users from removing each other's files. In cases where there is no reason for a directory to be world-writable, a better solution is to remove that permission rather than to set the sticky bit. However, if a directory is used by a particular application, consult that application's documentation instead of blindly changing modes. To set the sticky bit on a world-writable directory DIR, run the following command:
    $ sudo chmod +t DIR
Rationale: | Failing to set the sticky bit on public directories allows unauthorized users to delete files in the directory structure. The only authorized public directories are those temporary directories supplied with the system, or those designed to be temporary file repositories. The setting is normally reserved for directories used by the system, by users for temporary file storage (such as `/tmp`), and for directories requiring global read/write access.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_dir_perms_world_writable_sticky_bits
Identifiers and References | References: BP28(R40), 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-001090, 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-000138-GPOS-00069
Remediation Shell script ⇲
    df --local -P | awk '{if (NR!=1) print $6}' \
    | xargs -I '{}' find '{}' -xdev -type d \
    \( -perm -0002 -a ! -perm -1000 \) 2>/dev/null \
    | xargs chmod a+t
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | restrict
    - name: Get all world-writable directories with no sticky bits set
      shell: |
        set -o pipefail
        df --local -P | awk '{if (NR!=1) print $6}' | xargs -I '{}' find '{}' -xdev -type d \( -perm -0002 -a ! -perm -1000 \) 2>/dev/null
      register: dir_output
      tags:
        - NIST-800-53-AC-6(1)
        - NIST-800-53-CM-6(a)
        - dir_perms_world_writable_sticky_bits
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy

    - name: ensure sticky bit is set
      file:
        path: '{{ item }}'
        mode: a+t
      with_items:
        - '{{ dir_output.stdout_lines }}'
      tags:
        - NIST-800-53-AC-6(1)
        - NIST-800-53-CM-6(a)
        - dir_perms_world_writable_sticky_bits
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - restrict_strategy
#### Rule Ensure All SGID Executables Are Authorized [ref]
The SGID (set group id) bit should be set only on files that were installed via authorized means. A straightforward means of identifying unauthorized SGID files is determine if any were not installed as part of an RPM package, which is cryptographically verified. Investigate the origin of any unpackaged SGID files. This configuration check considers authorized SGID files which were installed via RPM. It is assumed that when an individual has sudo access to install an RPM and all packages are signed with an organizationally-recognized GPG key, the software should be considered an approved package on the system. Any SGID file not deployed through an RPM will be flagged for further review.
Rationale: | Executable files with the SGID permission run with the privileges of the owner of the file. SGID files of uncertain provenance could allow for unprivileged users to elevate privileges. The presence of these files should be strictly controlled on the system.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_permissions_unauthorized_sgid
Identifiers and References | References: BP28(R37), BP28(R38), 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 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
#### Rule Ensure All SUID Executables Are Authorized [ref]
The SUID (set user id) bit should be set only on files that were installed via authorized means. A straightforward means of identifying unauthorized SUID files is determine if any were not installed as part of an RPM package, which is cryptographically verified. Investigate the origin of any unpackaged SUID files. This configuration check considers authorized SUID files which were installed via RPM. It is assumed that when an individual has sudo access to install an RPM and all packages are signed with an organizationally-recognized GPG key, the software should be considered an approved package on the system. Any SUID file not deployed through an RPM will be flagged for further review.
Rationale: | Executable files with the SUID permission run with the privileges of the owner of the file. SUID files of uncertain provenance could allow for unprivileged users to elevate privileges. The presence of these files should be strictly controlled on the system.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_permissions_unauthorized_suid
Identifiers and References | References: BP28(R37), BP28(R38), 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 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
#### Rule Ensure No World-Writable Files Exist [ref]
It is generally a good idea to remove global (other) write access to a file when it is discovered. However, check with documentation for specific applications before making changes. Also, monitor for recurring world-writable files, as these may be symptoms of a misconfigured application or user account. Finally, this applies to real files and not virtual files that are a part of pseudo file systems such as `sysfs` or `procfs`.
Rationale: | Data in world-writable files can be modified by any user on the system. In almost all circumstances, files can be configured using a combination of user and group permissions to support whatever legitimate access is needed without the risk caused by world-writable files.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_permissions_unauthorized_world_writable
Identifiers and References | References: BP28(R40), 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 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
Remediation Shell script ⇲
    find / -xdev -type f -perm -002 -exec chmod o-w {} \;
Group Restrict Dynamic Mounting and Unmounting of Filesystems Group contains 1 rule
[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 | 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, OL07-00-020110, SV-221714r603260_rule
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:
        - DISA-STIG-OL07-00-020110
        - 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:
        - DISA-STIG-OL07-00-020110
        - 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:
        - DISA-STIG-OL07-00-020110
        - 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 script ⇲
    [customizations.services]
    disabled = ["autofs"]
Group Restrict Partition Mount Options Group contains 2 rules
[ref] System partitions can be mounted with certain options that limit what files on those partitions can do. These options are set in the `/etc/fstab` configuration file, and can be used to make certain types of malicious behavior more difficult.
#### Rule Add nodev Option to /dev/shm [ref]
The `nodev` mount option can be used to prevent creation of device files in `/dev/shm`. Legitimate character and block devices should not exist within temporary directories like `/dev/shm`. Add the `nodev` option to the fourth column of `/etc/fstab` for the line which controls mounting of `/dev/shm`.
Rationale: | The only legitimate location for device files is the `/dev` directory located on the root partition. The only exception to this is chroot jails.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_mount_option_dev_shm_nodev
Identifiers and References | References: 11, 13, 14, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS05.06, DSS06.06, CCI-001764, 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.11.2.9, 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.8.2.1, A.8.2.2, A.8.2.3, A.8.3.1, A.8.3.3, A.9.1.2, 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-6, AC-6(1), MP-7, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000368-GPOS-00154, OL07-00-021022
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    function perform_remediation {

        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /dev/shm)"

        # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
        if [ "$(grep -c "$mount_point_match_regexp" /etc/fstab)" -eq 0 ]; then
            # runtime opts without some automatic kernel/userspace-added defaults
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                        | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//")
            [ "$previous_mount_opts" ] && previous_mount_opts+=","
            echo "tmpfs /dev/shm tmpfs defaults,${previous_mount_opts}nodev 0 0" >> /etc/fstab
        # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
        elif [ "$(grep "$mount_point_match_regexp" /etc/fstab | grep -c "nodev")" -eq 0 ]; then
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
            sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab
        fi

        if mkdir -p "/dev/shm"; then
            if mountpoint -q "/dev/shm"; then
                mount -o remount --target "/dev/shm"
            else
                mount --target "/dev/shm"
            fi
        fi
    }

    perform_remediation

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | high
Strategy: | configure
    - name: 'Add nodev Option to /dev/shm: Check information associated to mountpoint'
      command: findmnt  '/dev/shm'
      register: device_name
      failed_when: device_name.rc > 1
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - DISA-STIG-OL07-00-021022
        - NIST-800-53-AC-6
        - NIST-800-53-AC-6(1)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-MP-7
        - configure_strategy
        - high_disruption
        - low_complexity
        - medium_severity
        - mount_option_dev_shm_nodev
        - no_reboot_needed

    - name: 'Add nodev Option to /dev/shm: Create mount_info dictionary variable'
      set_fact:
        mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
      with_together:
        - '{{ device_name.stdout_lines[0].split() | list | lower }}'
        - '{{ device_name.stdout_lines[1].split() | list }}'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - device_name.stdout is defined and device_name.stdout_lines is defined
        - (device_name.stdout | length > 0)
      tags:
        - DISA-STIG-OL07-00-021022
        - NIST-800-53-AC-6
        - NIST-800-53-AC-6(1)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-MP-7
        - configure_strategy
        - high_disruption
        - low_complexity
        - medium_severity
        - mount_option_dev_shm_nodev
        - no_reboot_needed

    - name: 'Add nodev Option to /dev/shm: If /dev/shm not mounted, craft mount_info manually'
      set_fact:
        mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
      with_together:
        - - target
          - source
          - fstype
          - options
        - - /dev/shm
          - tmpfs
          - tmpfs
          - defaults
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - ("" | length == 0)
        - (device_name.stdout | length == 0)
      tags:
        - DISA-STIG-OL07-00-021022
        - NIST-800-53-AC-6
        - NIST-800-53-AC-6(1)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-MP-7
        - configure_strategy
        - high_disruption
        - low_complexity
        - medium_severity
        - mount_option_dev_shm_nodev
        - no_reboot_needed

    - name: 'Add nodev Option to /dev/shm: Make sure nodev option is part of the to /dev/shm
        options'
      set_fact:
        mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nodev''
          }) }}'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - mount_info is defined and "nodev" not in mount_info.options
      tags:
        - DISA-STIG-OL07-00-021022
        - NIST-800-53-AC-6
        - NIST-800-53-AC-6(1)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-MP-7
        - configure_strategy
        - high_disruption
        - low_complexity
        - medium_severity
        - mount_option_dev_shm_nodev
        - no_reboot_needed

    - name: 'Add nodev Option to /dev/shm: Ensure /dev/shm is mounted with nodev option'
      mount:
        path: /dev/shm
        src: '{{ mount_info.source }}'
        opts: '{{ mount_info.options }}'
        state: mounted
        fstype: '{{ mount_info.fstype }}'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - (device_name.stdout is defined and (device_name.stdout | length > 0)) or (""
          | length == 0)
      tags:
        - DISA-STIG-OL07-00-021022
        - NIST-800-53-AC-6
        - NIST-800-53-AC-6(1)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-MP-7
        - configure_strategy
        - high_disruption
        - low_complexity
        - medium_severity
        - mount_option_dev_shm_nodev
        - no_reboot_needed
#### Rule Add nosuid Option to /dev/shm [ref]
The `nosuid` mount option can be used to prevent execution of setuid programs in `/dev/shm`. The SUID and SGID permissions should not be required in these world-writable directories. Add the `nosuid` option to the fourth column of `/etc/fstab` for the line which controls mounting of `/dev/shm`.
Rationale: | The presence of SUID and SGID executables should be tightly controlled. Users should not be able to execute SUID or SGID binaries from temporary storage partitions.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_mount_option_dev_shm_nosuid
Identifiers and References | References: 11, 13, 14, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS05.06, DSS06.06, CCI-001764, 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.11.2.9, 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.8.2.1, A.8.2.2, A.8.2.3, A.8.3.1, A.8.3.3, A.9.1.2, 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-6, AC-6(1), MP-7, PR.IP-1, PR.PT-2, PR.PT-3, SRG-OS-000368-GPOS-00154, OL07-00-021023
Remediation Shell script ⇲
    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    function perform_remediation {

        mount_point_match_regexp="$(printf "[[:space:]]%s[[:space:]]" /dev/shm)"

        # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
        if [ "$(grep -c "$mount_point_match_regexp" /etc/fstab)" -eq 0 ]; then
            # runtime opts without some automatic kernel/userspace-added defaults
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                        | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//")
            [ "$previous_mount_opts" ] && previous_mount_opts+=","
            echo "tmpfs /dev/shm tmpfs defaults,${previous_mount_opts}nosuid 0 0" >> /etc/fstab
        # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
        elif [ "$(grep "$mount_point_match_regexp" /etc/fstab | grep -c "nosuid")" -eq 0 ]; then
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
            sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab
        fi

        if mkdir -p "/dev/shm"; then
            if mountpoint -q "/dev/shm"; then
                mount -o remount --target "/dev/shm"
            else
                mount --target "/dev/shm"
            fi
        fi
    }

    perform_remediation

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi
Remediation Ansible snippet ⇲
Complexity: | low
Disruption: | high
Strategy: | configure
    - name: 'Add nosuid Option to /dev/shm: Check information associated to mountpoint'
      command: findmnt  '/dev/shm'
      register: device_name
      failed_when: device_name.rc > 1
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - DISA-STIG-OL07-00-021023
        - NIST-800-53-AC-6
        - NIST-800-53-AC-6(1)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-MP-7
        - configure_strategy
        - high_disruption
        - low_complexity
        - medium_severity
        - mount_option_dev_shm_nosuid
        - no_reboot_needed

    - name: 'Add nosuid Option to /dev/shm: Create mount_info dictionary variable'
      set_fact:
        mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
      with_together:
        - '{{ device_name.stdout_lines[0].split() | list | lower }}'
        - '{{ device_name.stdout_lines[1].split() | list }}'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - device_name.stdout is defined and device_name.stdout_lines is defined
        - (device_name.stdout | length > 0)
      tags:
        - DISA-STIG-OL07-00-021023
        - NIST-800-53-AC-6
        - NIST-800-53-AC-6(1)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-MP-7
        - configure_strategy
        - high_disruption
        - low_complexity
        - medium_severity
        - mount_option_dev_shm_nosuid
        - no_reboot_needed

    - name: 'Add nosuid Option to /dev/shm: If /dev/shm not mounted, craft mount_info
        manually'
      set_fact:
        mount_info: '{{ mount_info|default({})|combine({item.0: item.1}) }}'
      with_together:
        - - target
          - source
          - fstype
          - options
        - - /dev/shm
          - tmpfs
          - tmpfs
          - defaults
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - ("" | length == 0)
        - (device_name.stdout | length == 0)
      tags:
        - DISA-STIG-OL07-00-021023
        - NIST-800-53-AC-6
        - NIST-800-53-AC-6(1)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-MP-7
        - configure_strategy
        - high_disruption
        - low_complexity
        - medium_severity
        - mount_option_dev_shm_nosuid
        - no_reboot_needed

    - name: 'Add nosuid Option to /dev/shm: Make sure nosuid option is part of the to
        /dev/shm options'
      set_fact:
        mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',nosuid''
          }) }}'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - mount_info is defined and "nosuid" not in mount_info.options
      tags:
        - DISA-STIG-OL07-00-021023
        - NIST-800-53-AC-6
        - NIST-800-53-AC-6(1)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-MP-7
        - configure_strategy
        - high_disruption
        - low_complexity
        - medium_severity
        - mount_option_dev_shm_nosuid
        - no_reboot_needed

    - name: 'Add nosuid Option to /dev/shm: Ensure /dev/shm is mounted with nosuid option'
      mount:
        path: /dev/shm
        src: '{{ mount_info.source }}'
        opts: '{{ mount_info.options }}'
        state: mounted
        fstype: '{{ mount_info.fstype }}'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - (device_name.stdout is defined and (device_name.stdout | length > 0)) or (""
          | length == 0)
      tags:
        - DISA-STIG-OL07-00-021023
        - NIST-800-53-AC-6
        - NIST-800-53-AC-6(1)
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - NIST-800-53-CM-7(b)
        - NIST-800-53-MP-7
        - configure_strategy
        - high_disruption
        - low_complexity
        - medium_severity
        - mount_option_dev_shm_nosuid
        - no_reboot_needed
Group Services Group contains 2 groups and 6 rules
[ref] The best protection against vulnerable software is running less software. This section describes how to review the software which Oracle Linux 7 installs on a system and disable software which is not needed. It then enumerates the software packages installed on a default Oracle Linux 7 system and provides guidance about which ones can be safely disabled. Oracle Linux 7 provides a convenient minimal install option that essentially installs the bare necessities for a functional system. When building Oracle Linux 7 systems, it is highly recommended to select the minimal packages and then build up the system from there.
Group Base Services Group contains 5 rules
[ref] This section addresses the base services that are installed on a Oracle Linux 7 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 Automatic Bug Reporting Tool (abrtd) [ref]
The Automatic Bug Reporting Tool (`abrtd`) daemon collects and reports crash data when an application crash is detected. Using a variety of plugins, abrtd can email crash reports to system administrators, log crash reports to files, or forward crash reports to a centralized issue tracking system such as RHTSupport. The `abrtd` service can be disabled with the following command:
    $ sudo systemctl mask --now abrtd.service
Rationale: | Mishandling crash data could expose sensitive information about vulnerabilities in software executing on the system, as well as sensitive information from within a process's address space or registers.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_abrtd_disabled
Identifiers and References | 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, 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-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 'abrtd.service'
    "$SYSTEMCTL_EXEC" disable 'abrtd.service'
    "$SYSTEMCTL_EXEC" mask 'abrtd.service'
    # Disable socket activation if we have a unit file for it
    if "$SYSTEMCTL_EXEC" list-unit-files | grep -q '^abrtd.socket'; then
        "$SYSTEMCTL_EXEC" stop 'abrtd.socket'
        "$SYSTEMCTL_EXEC" mask 'abrtd.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 'abrtd.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 abrtd
      block:

        - name: Disable service abrtd
          systemd:
            name: abrtd.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
          ignore_errors: 'yes'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_abrtd_disabled

    - name: Unit Socket Exists - abrtd.socket
      command: systemctl list-unit-files abrtd.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:
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_abrtd_disabled

    - name: Disable socket abrtd
      systemd:
        name: abrtd.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - '"abrtd.socket" in socket_file_exists.stdout_lines[1]'
      tags:
        - NIST-800-53-CM-6(a)
        - NIST-800-53-CM-7(a)
        - disable_strategy
        - low_complexity
        - low_disruption
        - medium_severity
        - no_reboot_needed
        - service_abrtd_disabled
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    include disable_abrtd

    class disable_abrtd {
      service {'abrtd':
        enable => false,
        ensure => 'stopped',
      }
    }
Remediation script ⇲
    [customizations.services]
    disabled = ["abrtd"]
#### Rule Disable ntpdate Service (ntpdate) [ref]
The `ntpdate` service sets the local hardware clock by polling NTP servers when the system boots. It synchronizes to the NTP servers listed in `/etc/ntp/step-tickers` or `/etc/ntp.conf` and then sets the local hardware clock to the newly synchronized system time. The `ntpdate` service can be disabled with the following command:
    $ sudo systemctl mask --now ntpdate.service
Rationale: | The `ntpdate` service may only be suitable for systems which are rebooted frequently enough that clock drift does not cause problems between reboots. In any event, the functionality of the ntpdate service is now available in the ntpd program and should be considered deprecated.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_service_ntpdate_disabled
Identifiers and References | 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-000382, 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 'ntpdate.service'
    "$SYSTEMCTL_EXEC" disable 'ntpdate.service'
    "$SYSTEMCTL_EXEC" mask 'ntpdate.service'
    # Disable socket activation if we have a unit file for it
    if "$SYSTEMCTL_EXEC" list-unit-files | grep -q '^ntpdate.socket'; then
        "$SYSTEMCTL_EXEC" stop 'ntpdate.socket'
        "$SYSTEMCTL_EXEC" mask 'ntpdate.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 'ntpdate.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 ntpdate
      block:

        - name: Disable service ntpdate
          systemd:
            name: ntpdate.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
          ignore_errors: 'yes'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - 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
        - service_ntpdate_disabled

    - name: Unit Socket Exists - ntpdate.socket
      command: systemctl list-unit-files ntpdate.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:
        - 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
        - service_ntpdate_disabled

    - name: Disable socket ntpdate
      systemd:
        name: ntpdate.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - '"ntpdate.socket" in socket_file_exists.stdout_lines[1]'
      tags:
        - 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
        - service_ntpdate_disabled
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    include disable_ntpdate

    class disable_ntpdate {
      service {'ntpdate':
        enable => false,
        ensure => 'stopped',
      }
    }
Remediation script ⇲
    [customizations.services]
    disabled = ["ntpdate"]
#### Rule Disable Odd Job Daemon (oddjobd) [ref]
The `oddjobd` service exists to provide an interface and access control mechanism through which specified privileged tasks can run tasks for unprivileged client applications. Communication with `oddjobd` through the system message bus. The `oddjobd` service can be disabled with the following command:
    $ sudo systemctl mask --now oddjobd.service
Rationale: | The `oddjobd` service may provide necessary functionality in some environments, and can be disabled if it is not needed. Execution of tasks by privileged programs, on behalf of unprivileged ones, has traditionally been a source of privilege escalation security issues.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_oddjobd_disabled
Identifiers and References | References: 11, 14, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, CCI-000381, 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-7(a), CM-7(b), CM-6(a), PR.IP-1, PR.PT-3
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 'oddjobd.service'
    "$SYSTEMCTL_EXEC" disable 'oddjobd.service'
    "$SYSTEMCTL_EXEC" mask 'oddjobd.service'
    # Disable socket activation if we have a unit file for it
    if "$SYSTEMCTL_EXEC" list-unit-files | grep -q '^oddjobd.socket'; then
        "$SYSTEMCTL_EXEC" stop 'oddjobd.socket'
        "$SYSTEMCTL_EXEC" mask 'oddjobd.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 'oddjobd.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 oddjobd
      block:

        - name: Disable service oddjobd
          systemd:
            name: oddjobd.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
          ignore_errors: 'yes'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - 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_oddjobd_disabled

    - name: Unit Socket Exists - oddjobd.socket
      command: systemctl list-unit-files oddjobd.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:
        - 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_oddjobd_disabled

    - name: Disable socket oddjobd
      systemd:
        name: oddjobd.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - '"oddjobd.socket" in socket_file_exists.stdout_lines[1]'
      tags:
        - 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_oddjobd_disabled
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    include disable_oddjobd

    class disable_oddjobd {
      service {'oddjobd':
        enable => false,
        ensure => 'stopped',
      }
    }
Remediation script ⇲
    [customizations.services]
    disabled = ["oddjobd"]
#### Rule Disable Apache Qpid (qpidd) [ref]
The `qpidd` service provides high speed, secure, guaranteed delivery services. It is an implementation of the Advanced Message Queuing Protocol. By default the qpidd service will bind to port 5672 and listen for connection attempts. The `qpidd` service can be disabled with the following command:
    $ sudo systemctl mask --now qpidd.service
Rationale: | The qpidd service is automatically installed when the `base` package selection is selected during installation. The qpidd service listens for network connections, which increases the attack surface of the system. If the system is not intended to receive AMQP traffic, then the `qpidd` service is not needed and should be disabled or removed.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_service_qpidd_disabled
Identifiers and References | 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-000382, 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 'qpidd.service'
    "$SYSTEMCTL_EXEC" disable 'qpidd.service'
    "$SYSTEMCTL_EXEC" mask 'qpidd.service'
    # Disable socket activation if we have a unit file for it
    if "$SYSTEMCTL_EXEC" list-unit-files | grep -q '^qpidd.socket'; then
        "$SYSTEMCTL_EXEC" stop 'qpidd.socket'
        "$SYSTEMCTL_EXEC" mask 'qpidd.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 'qpidd.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 qpidd
      block:

        - name: Disable service qpidd
          systemd:
            name: qpidd.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
          ignore_errors: 'yes'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - 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
        - service_qpidd_disabled

    - name: Unit Socket Exists - qpidd.socket
      command: systemctl list-unit-files qpidd.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:
        - 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
        - service_qpidd_disabled

    - name: Disable socket qpidd
      systemd:
        name: qpidd.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - '"qpidd.socket" in socket_file_exists.stdout_lines[1]'
      tags:
        - 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
        - service_qpidd_disabled
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    include disable_qpidd

    class disable_qpidd {
      service {'qpidd':
        enable => false,
        ensure => 'stopped',
      }
    }
Remediation script ⇲
    [customizations.services]
    disabled = ["qpidd"]
#### Rule Disable Network Router Discovery Daemon (rdisc) [ref]
The `rdisc` service implements the client side of the ICMP Internet Router Discovery Protocol (IRDP), which allows discovery of routers on the local subnet. If a router is discovered then the local routing table is updated with a corresponding default route. By default this daemon is disabled. The `rdisc` service can be disabled with the following command:
    $ sudo systemctl mask --now rdisc.service
Rationale: | General-purpose systems typically have their network and routing information configured statically by a system administrator. Workstations or some special-purpose systems often use DHCP (instead of IRDP) to retrieve dynamic network configuration information.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_rdisc_disabled
Identifiers and References | References: 1, 11, 12, 13, 14, 15, 16, 18, 3, 4, 6, 8, 9, APO01.06, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS01.05, DSS03.01, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.06, CCI-000382, 4.2.3.4, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, 4.4.3.3, 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.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.11.2.6, A.12.1.1, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.1.2, A.6.2.1, A.6.2.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-4, CM-7(a), CM-7(b), CM-6(a), DE.AE-1, ID.AM-3, PR.AC-3, PR.AC-5, PR.DS-5, 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 'rdisc.service'
    "$SYSTEMCTL_EXEC" disable 'rdisc.service'
    "$SYSTEMCTL_EXEC" mask 'rdisc.service'
    # Disable socket activation if we have a unit file for it
    if "$SYSTEMCTL_EXEC" list-unit-files | grep -q '^rdisc.socket'; then
        "$SYSTEMCTL_EXEC" stop 'rdisc.socket'
        "$SYSTEMCTL_EXEC" mask 'rdisc.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 'rdisc.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 rdisc
      block:

        - name: Disable service rdisc
          systemd:
            name: rdisc.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
          ignore_errors: 'yes'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - NIST-800-53-AC-4
        - 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_rdisc_disabled

    - name: Unit Socket Exists - rdisc.socket
      command: systemctl list-unit-files rdisc.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:
        - NIST-800-53-AC-4
        - 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_rdisc_disabled

    - name: Disable socket rdisc
      systemd:
        name: rdisc.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - '"rdisc.socket" in socket_file_exists.stdout_lines[1]'
      tags:
        - NIST-800-53-AC-4
        - 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_rdisc_disabled
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    include disable_rdisc

    class disable_rdisc {
      service {'rdisc':
        enable => false,
        ensure => 'stopped',
      }
    }
Remediation script ⇲
    [customizations.services]
    disabled = ["rdisc"]
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 Disable At Service (atd) [ref]
The `at` and `batch` commands can be used to schedule tasks that are meant to be executed only once. This allows delayed execution in a manner similar to cron, except that it is not recurring. The daemon `atd` keeps track of tasks scheduled via `at` and `batch`, and executes them at the specified time. The `atd` service can be disabled with the following command:
    $ sudo systemctl mask --now atd.service
Rationale: | The `atd` service could be used by an unsophisticated insider to carry out activities outside of a normal login session, which could complicate accountability. Furthermore, the need to schedule tasks with `at` or `batch` is not common.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_atd_disabled
Identifiers and References | References: 11, 14, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, CCI-000381, 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-7(a), CM-7(b), CM-6(a), PR.IP-1, PR.PT-3
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 'atd.service'
    "$SYSTEMCTL_EXEC" disable 'atd.service'
    "$SYSTEMCTL_EXEC" mask 'atd.service'
    # Disable socket activation if we have a unit file for it
    if "$SYSTEMCTL_EXEC" list-unit-files | grep -q '^atd.socket'; then
        "$SYSTEMCTL_EXEC" stop 'atd.socket'
        "$SYSTEMCTL_EXEC" mask 'atd.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 'atd.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 atd
      block:

        - name: Disable service atd
          systemd:
            name: atd.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
          ignore_errors: 'yes'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
        - 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_atd_disabled

    - name: Unit Socket Exists - atd.socket
      command: systemctl list-unit-files atd.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:
        - 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_atd_disabled

    - name: Disable socket atd
      systemd:
        name: atd.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
        - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        - '"atd.socket" in socket_file_exists.stdout_lines[1]'
      tags:
        - 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_atd_disabled
Remediation Puppet snippet ⇲
Complexity: | low
Disruption: | low
Strategy: | enable
    include disable_atd

    class disable_atd {
      service {'atd':
        enable => false,
        ensure => 'stopped',
      }
    }
Remediation script ⇲
    [customizations.services]
    disabled = ["atd"]

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.