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

> with profile Australian Cyber Security Centre (ACSC) ISM Official
> This profile contains configuration checks for Red Hat Enterprise Linux 8 that align to the Australian Cyber Security Centre (ACSC) Information Security Manual (ISM) with the applicability marking of OFFICIAL. The ISM uses a risk-based approach to cyber security. This profile provides a guide to aligning Red Hat Enterprise Linux security controls with the ISM, which can be used to select controls specific to an organisation's security posture and risk profile. A copy of the ISM can be found at the ACSC website: https://www.cyber.gov.au/ism

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

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

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

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

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

## Profile Information

Profile Title | Australian Cyber Security Centre (ACSC) ISM Official
Profile ID | xccdf_org.ssgproject.content_profile_ism_o

#### CPE Platforms

- cpe:/o:redhat:enterprise_linux:8.0
- cpe:/o:redhat:enterprise_linux:8.1
- cpe:/o:redhat:enterprise_linux:8.10
- cpe:/o:redhat:enterprise_linux:8.2
- cpe:/o:redhat:enterprise_linux:8.3
- cpe:/o:redhat:enterprise_linux:8.4
- cpe:/o:redhat:enterprise_linux:8.5
- cpe:/o:redhat:enterprise_linux:8.6
- cpe:/o:redhat:enterprise_linux:8.7
- cpe:/o:redhat:enterprise_linux:8.8
- cpe:/o:redhat:enterprise_linux:8.9
- cpe:/o:redhat:enterprise_linux:8
- cpe:/o:centos:centos:8

## Revision History

Current version: 0.1.71

- draft (as of 2023-12-16)

## 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. Network Configuration and Firewalls
   6. File Permissions and Masks
   7. SELinux
2. Services
   1. Avahi Server
   2. Application Whitelisting Daemon
   3. Network Time Protocol
   4. Obsolete Services
   5. Proxy Server
   6. Network Routing
   7. SNMP Server
   8. SSH Server
   9. USBGuard daemon

## Checklist

Group Guide to the Secure Configuration of Red Hat Enterprise Linux 8 Group contains 71 groups and 151 rules
Group System Settings Group contains 49 groups and 107 rules
[ref] Contains rules that check correct system settings.
Group Installing and Maintaining Software Group contains 9 groups and 19 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 5 groups and 8 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 2 groups and 4 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 3 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, 11.5.2, SRG-OS-000480-GPOS-00227, 6.1.1

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
Reboot: | false
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
      - 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
      - PCI-DSSv4-11.5.2
      - 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 == "CentOS" or ansible_distribution
        == "OracleLinux")
      tags:
      - CJIS-5.10.4.1
      - NIST-800-171-3.3.8
      - NIST-800-171-3.4.1
      - NIST-800-53-AU-9(3)
      - NIST-800-53-CM-6(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
      - PCI-DSSv4-11.5.2
      - high_complexity
      - high_severity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy
      - rpm_verify_hashes

    - name: 'Set fact: Package manager reinstall command (zypper)'
      set_fact:
        package_manager_reinstall_cmd: zypper in -f -y
      when: ansible_distribution == "SLES"
      tags:
      - CJIS-5.10.4.1
      - NIST-800-171-3.3.8
      - NIST-800-171-3.4.1
      - NIST-800-53-AU-9(3)
      - NIST-800-53-CM-6(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
      - PCI-DSSv4-11.5.2
      - 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
      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
      - 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
      - PCI-DSSv4-11.5.2
      - high_complexity
      - high_severity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy
      - rpm_verify_hashes

    - name: Create list of packages
      command: rpm -qf "{{ item }}"
      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
      - 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
      - PCI-DSSv4-11.5.2
      - 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 }}'''
      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
      - 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
      - PCI-DSSv4-11.5.2
      - high_complexity
      - high_severity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy
      - rpm_verify_hashes

#### Rule Verify and Correct Ownership with RPM [ref]

The RPM package management system can check file ownership permissions of installed software packages, including many that are important to system security. After locating a file with incorrect permissions, which can be found with

    rpm -Va | awk '{ if (substr($0,6,1)=="U" || substr($0,7,1)=="G") print $NF }'

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 --setugids PACKAGENAME

Warning: Profiles may require that specific files be owned by root while the default owner defined by the vendor is different. Such files will be reported as a finding and need to be evaluated according to your policy and deployment environment.

Rationale: | Ownership of binaries and configuration files that is incorrect could allow an unauthorized user to gain privileges that they should not have. The ownership 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_ownership

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-001494, CCI-001496, 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), PR.AC-4, PR.DS-5, PR.IP-1, PR.PT-1, Req-11.5, 11.5.2, SRG-OS-000256-GPOS-00097, SRG-OS-000257-GPOS-00098, SRG-OS-000278-GPOS-00108, 1.8.1.4, 1.8.1.5, 1.8.1.6, 6.1.1, 6.1.2, 6.1.3, 6.1.4, 6.1.5, 6.1.6, 6.1.7, 6.1.8, 6.1.9

Remediation Shell script ⇲

Complexity: | high
Disruption: | medium
Reboot: | false
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,6,1)=="U" || substr($0,7,1)=="G") print $NF }')

    for FILE_PATH in "${FILES_WITH_INCORRECT_PERMS[@]}"
    do
            RPM_PACKAGE=$(rpm -qf "$FILE_PATH")
    	# Use an associative array to store packages as it's keys, not having to care about duplicates.
    	SETPERMS_RPM_DICT["$RPM_PACKAGE"]=1
    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 --setugids "${RPM_PACKAGE}"
    done

Remediation Ansible snippet ⇲

Complexity: | high
Disruption: | medium
Reboot: | false
Strategy: | restrict

    - name: Read list of files with incorrect ownership
      command: rpm -Va --nodeps --nosignature --nofiledigest --nosize --nomtime --nordev
        --nocaps --nolinkto --nomode
      register: files_with_incorrect_ownership
      failed_when: files_with_incorrect_ownership.rc > 1
      changed_when: false
      check_mode: false
      tags:
      - CJIS-5.10.4.1
      - NIST-800-171-3.3.8
      - NIST-800-171-3.4.1
      - NIST-800-53-AU-9(3)
      - NIST-800-53-CM-6(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
      - PCI-DSSv4-11.5.2
      - high_complexity
      - high_severity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy
      - rpm_verify_ownership

    - name: Create list of packages
      command: rpm -qf "{{ item }}"
      with_items: '{{ files_with_incorrect_ownership.stdout_lines | map(''regex_findall'',
        ''^[.]+[U|G]+.* (\/.*)'', ''\1'') | map(''join'') | select(''match'', ''(\/.*)'')
        | list | unique }}'
      register: list_of_packages
      changed_when: false
      check_mode: false
      when: (files_with_incorrect_ownership.stdout_lines | length > 0)
      tags:
      - CJIS-5.10.4.1
      - NIST-800-171-3.3.8
      - NIST-800-171-3.4.1
      - NIST-800-53-AU-9(3)
      - NIST-800-53-CM-6(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
      - PCI-DSSv4-11.5.2
      - high_complexity
      - high_severity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy
      - rpm_verify_ownership

    - name: Correct file ownership with RPM
      command: rpm --setugids '{{ item }}'
      with_items: '{{ list_of_packages.results | map(attribute=''stdout_lines'') | list
        | unique }}'
      when: (files_with_incorrect_ownership.stdout_lines | length > 0)
      tags:
      - CJIS-5.10.4.1
      - NIST-800-171-3.3.8
      - NIST-800-171-3.4.1
      - NIST-800-53-AU-9(3)
      - NIST-800-53-CM-6(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
      - PCI-DSSv4-11.5.2
      - high_complexity
      - high_severity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy
      - rpm_verify_ownership

#### 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, 11.5.2, SRG-OS-000256-GPOS-00097, SRG-OS-000257-GPOS-00098, SRG-OS-000258-GPOS-00099, SRG-OS-000278-GPOS-00108, 1.8.1.4, 1.8.1.5, 1.8.1.6, 6.1.1, 6.1.2, 6.1.3, 6.1.4, 6.1.5, 6.1.6, 6.1.7, 6.1.8, 6.1.9

Remediation Shell script ⇲

Complexity: | high
Disruption: | medium
Reboot: | false
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
Reboot: | false
Strategy: | restrict

    - name: Read list of files with incorrect permissions
      command: rpm -Va --nodeps --nosignature --nofiledigest --nosize --nomtime --nordev
        --nocaps --nolinkto --nouser --nogroup
      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
      - 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
      - PCI-DSSv4-11.5.2
      - high_complexity
      - high_severity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy
      - rpm_verify_permissions

    - name: Create list of packages
      command: rpm -qf "{{ item }}"
      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
      - 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
      - PCI-DSSv4-11.5.2
      - high_complexity
      - high_severity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy
      - rpm_verify_permissions

    - name: Correct file permissions with RPM
      command: rpm --setperms '{{ item }}'
      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
      - 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
      - PCI-DSSv4-11.5.2
      - high_complexity
      - high_severity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy
      - rpm_verify_permissions

Group Verify Integrity with AIDE Group contains 1 rule
[ref] AIDE conducts integrity checks by comparing information about files with previously-gathered information. Ideally, the AIDE database is created immediately after initial system configuration, and then again after any software update. AIDE is highly configurable, with further configuration information located in `/usr/share/doc/aide-VERSION`.

#### Rule Install AIDE [ref]

The `aide` package can be installed with the following command:

    $ sudo yum install aide

Rationale: | The AIDE package must be installed if it is to be available for integrity checking.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_aide_installed

Identifiers and References

References: BP28(R51), 1, 11, 12, 13, 14, 15, 16, 2, 3, 5, 7, 8, 9, 5.10.1.3, APO01.06, BAI01.06, BAI02.01, BAI03.05, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.03, DSS03.05, DSS04.07, DSS05.02, DSS05.03, DSS05.05, DSS05.07, DSS06.02, DSS06.06, CCI-002696, CCI-002699, CCI-001744, 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 4.1, SR 6.2, SR 7.6, 1034, 1288, 1341, 1417, A.11.2.4, A.12.1.2, A.12.2.1, A.12.4.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, A.14.2.7, A.15.2.1, A.8.2.3, CM-6(a), DE.CM-1, DE.CM-7, PR.DS-1, PR.DS-6, PR.DS-8, PR.IP-1, PR.IP-3, Req-11.5, 11.5.2, SRG-OS-000445-GPOS-00199, 1.3.1, SV-251710r880730_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    package --add=aide

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: Ensure aide is installed
      package:
        name: aide
        state: present
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.10.1.3
      - DISA-STIG-RHEL-08-010359
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-11.5
      - PCI-DSSv4-11.5.2
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_aide_installed

Remediation OSBuild Blueprint snippet ⇲

    [[packages]]
    name = "aide"
    version = "*"

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include install_aide

    class install_aide {
      package { 'aide':
        ensure => 'installed',
      }
    }

Group Federal Information Processing Standard (FIPS) Group contains 1 rule

[ref] The Federal Information Processing Standard (FIPS) is a computer security standard which is developed by the U.S. Government and industry working groups to validate the quality of cryptographic modules. The FIPS standard provides four security levels to ensure adequate coverage of different industries, implementation of cryptographic modules, and organizational sizes and requirements.
FIPS 140-2 is the current standard for validating that mechanisms used to access cryptographic modules utilize authentication that meets industry and government requirements. For government systems, this allows Security Levels 1, 2, 3, or 4 for use on Red Hat Enterprise Linux 8.
See http://csrc.nist.gov/publications/PubsFIPS.html for more information.

#### Rule Enable FIPS Mode [ref]

To enable FIPS mode, run the following command:

    fips-mode-setup --enable

The `fips-mode-setup` command will configure the system in FIPS mode by automatically configuring the following:

- Setting the kernel FIPS mode flag (`/proc/sys/crypto/fips_enabled`) to `1`
- Creating `/etc/system-fips`
- Setting the system crypto policy in `/etc/crypto-policies/config` to `FIPS`
- Loading the Dracut `fips` module

Warning: The system needs to be rebooted for these changes to take effect.
Warning: This rule DOES NOT CHECK if the components of the operating system are FIPS certified. You can find the list of FIPS certified modules at https://csrc.nist.gov/projects/cryptographic-module-validation-program/validated-modules/search. This rule checks if the system is running in FIPS mode. See the rule description for more information about what it means.

Rationale: | Use of weak or untested encryption algorithms undermines the purposes of utilizing encryption to protect data. The operating system must implement cryptographic modules adhering to the higher standards approved by the federal government since this provides assurance they have been tested and validated.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_enable_fips_mode
Identifiers and References | References: CCI-000068, CCI-000803, CCI-002450, 1446, CIP-003-8 R4.2, CIP-007-3 R5.1, CM-3(6), SC-12(2), SC-12(3), IA-7, SC-13, CM-6(a), SC-12, FCS_COP.1(1), FCS_COP.1(2), FCS_COP.1(3), FCS_COP.1(4), FCS_CKM.1, FCS_CKM.2, FCS_TLSC_EXT.1, FCS_RBG_EXT.1, SRG-OS-000478-GPOS-00223, SRG-OS-000396-GPOS-00176, SV-230223r928585_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if ( [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && ! ( [ "${container:-}" == "bwrap-osbuild" ] ) ) && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

    var_system_crypto_policy='FIPS'

    fips-mode-setup --enable

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

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

    	false  # end with an error code
    elif test "$rc" != 0; then
    	echo "Error invoking the update-crypto-policies script: $stderr_of_call" >&2
    	false  # end with an error code
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | medium
Disruption: | medium
Reboot: | true
Strategy: | restrict

    - name: XCCDF Value var_system_crypto_policy # promote to variable
      set_fact:
        var_system_crypto_policy: !!str FIPS
      tags:
        - always

    - name: Enable FIPS Mode - Check to See the Current Status of FIPS Mode
      ansible.builtin.command: /usr/bin/fips-mode-setup --check
      register: is_fips_enabled
      ignore_errors: true
      changed_when: false
      when:
      - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        and not ( lookup("env", "container") == "bwrap-osbuild" ) )
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010020
      - NIST-800-53-CM-3(6)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-7
      - NIST-800-53-SC-12
      - NIST-800-53-SC-12(2)
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SC-13
      - enable_fips_mode
      - high_severity
      - medium_complexity
      - medium_disruption
      - reboot_required
      - restrict_strategy

    - name: Enable FIPS Mode - Enable FIPS Mode
      ansible.builtin.command: /usr/bin/fips-mode-setup --enable
      when:
      - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        and not ( lookup("env", "container") == "bwrap-osbuild" ) )
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - is_fips_enabled.stdout.find('FIPS mode is enabled.') == -1
      tags:
      - DISA-STIG-RHEL-08-010020
      - NIST-800-53-CM-3(6)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-7
      - NIST-800-53-SC-12
      - NIST-800-53-SC-12(2)
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SC-13
      - enable_fips_mode
      - high_severity
      - medium_complexity
      - medium_disruption
      - reboot_required
      - restrict_strategy

    - name: Enable FIPS Mode - Configure Crypto Policy
      ansible.builtin.lineinfile:
        path: /etc/crypto-policies/config
        regexp: ^(?!#)(\S+)$
        line: '{{ var_system_crypto_policy }}'
        create: true
      when:
      - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        and not ( lookup("env", "container") == "bwrap-osbuild" ) )
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010020
      - NIST-800-53-CM-3(6)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-7
      - NIST-800-53-SC-12
      - NIST-800-53-SC-12(2)
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SC-13
      - enable_fips_mode
      - high_severity
      - medium_complexity
      - medium_disruption
      - reboot_required
      - restrict_strategy

    - name: Enable FIPS Mode - Verify that Crypto Policy is Set (runtime)
      ansible.builtin.command: /usr/bin/update-crypto-policies --set {{ var_system_crypto_policy
        }}
      when:
      - ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
        and not ( lookup("env", "container") == "bwrap-osbuild" ) )
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010020
      - NIST-800-53-CM-3(6)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-7
      - NIST-800-53-SC-12
      - NIST-800-53-SC-12(2)
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SC-13
      - enable_fips_mode
      - high_severity
      - medium_complexity
      - medium_disruption
      - reboot_required
      - restrict_strategy

Group System Cryptographic Policies Group contains 3 rules

[ref] Linux has the capability to centrally configure cryptographic polices. The command `update-crypto-policies` is used to set the policy applicable for the various cryptographic back-ends, such as SSL/TLS libraries. The configured cryptographic policies will be the default policy used by these backends unless the application user configures them otherwise. When the system has been configured to use the centralized cryptographic policies, the administrator is assured that any application that utilizes the supported backends will follow a policy that adheres to the configured profile. Currently the supported backends are:

- GnuTLS library
- OpenSSL library
- NSS library
- OpenJDK
- Libkrb5
- BIND
- OpenSSH

Applications and languages which rely on any of these backends will follow the system policies as well. Examples are apache httpd, nginx, php, and others.

#### Rule Configure System Cryptography Policy [ref]

To configure the system cryptography policy to use ciphers only from the `FIPS` policy, run the following command:

    $ sudo update-crypto-policies --set FIPS

The rule checks if settings for selected crypto policy are configured as expected. Configuration files in the `/etc/crypto-policies/back-ends` are either symlinks to correct files provided by Crypto-policies package or they are regular files in case crypto policy customizations are applied. Crypto policies may be customized by crypto policy modules, in which case it is delimited from the base policy using a colon.
Warning: The system needs to be rebooted for these changes to take effect.
Warning: System Crypto Modules must be provided by a vendor that undergoes FIPS-140 certifications. FIPS-140 is applicable to all Federal agencies that use cryptographic-based security systems to protect sensitive information in computer and telecommunication systems (including voice systems) as defined in Section 5131 of the Information Technology Management Reform Act of 1996, Public Law 104-106. This standard shall be used in designing and implementing cryptographic modules that Federal departments and agencies operate or are operated for them under contract. See https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.140-2.pdf To meet this, the system has to have cryptographic software provided by a vendor that has undergone this certification. This means providing documentation, test results, design information, and independent third party review by an accredited lab. While open source software is capable of meeting this, it does not meet FIPS-140 unless the vendor submits to this process.

Rationale: | Centralized cryptographic policies simplify applying secure ciphers across an operating system and the applications that run on that operating system. Use of weak or untested encryption algorithms undermines the purposes of utilizing encryption to protect data.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_configure_crypto_policy

Identifiers and References

References: 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.312(e)(1), 164.312(e)(2)(ii), 1446, CIP-003-8 R4.2, CIP-007-3 R5.1, CIP-007-3 R7.1, AC-17(a), AC-17(2), CM-6(a), MA-4(6), SC-13, SC-12(2), SC-12(3), FCS_COP.1(1), FCS_COP.1(2), FCS_COP.1(3), FCS_COP.1(4), FCS_CKM.1, FCS_CKM.2, FCS_TLSC_EXT.1, 2.2.7, SRG-OS-000396-GPOS-00176, SRG-OS-000393-GPOS-00173, SRG-OS-000394-GPOS-00174, 1.10, 1.11, SV-230223r928585_rule

Remediation Shell script ⇲

    var_system_crypto_policy='FIPS'

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

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

    	false  # end with an error code
    elif test "$rc" != 0; then
    	echo "Error invoking the update-crypto-policies script: $stderr_of_call" >&2
    	false  # end with an error code
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: XCCDF Value var_system_crypto_policy # promote to variable
      set_fact:
        var_system_crypto_policy: !!str FIPS
      tags:
        - always

    - name: Configure System Cryptography Policy
      lineinfile:
        path: /etc/crypto-policies/config
        regexp: ^(?!#)(\S+)$
        line: '{{ var_system_crypto_policy }}'
        create: true
      tags:
      - DISA-STIG-RHEL-08-010020
      - NIST-800-53-AC-17(2)
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-MA-4(6)
      - NIST-800-53-SC-12(2)
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SC-13
      - PCI-DSSv4-2.2.7
      - configure_crypto_policy
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that Crypto Policy is Set (runtime)
      command: /usr/bin/update-crypto-policies --set {{ var_system_crypto_policy }}
      tags:
      - DISA-STIG-RHEL-08-010020
      - NIST-800-53-AC-17(2)
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-MA-4(6)
      - NIST-800-53-SC-12(2)
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SC-13
      - PCI-DSSv4-2.2.7
      - configure_crypto_policy
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - restrict_strategy

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        systemd:
          units:
            - name: configure-crypto-policy.service
              enabled: true
              contents: |
                [Unit]
                Before=kubelet.service
                [Service]
                Type=oneshot
                ExecStart=update-crypto-policies --set {{.var_system_crypto_policy}}
                RemainAfterExit=yes
                [Install]
                WantedBy=multi-user.target

#### Rule Configure SSH to use System Crypto Policy [ref]

Crypto Policies provide a centralized control over crypto algorithms usage of many packages. SSH is supported by crypto policy, but the SSH configuration may be set up to ignore it. To check that Crypto Policies settings are configured correctly, ensure that the `CRYPTO_POLICY` variable is either commented or not set at all in the `/etc/sysconfig/sshd`.
Rationale: | Overriding the system crypto policy makes the behavior of the SSH service violate expectations, and makes system configuration more fragmented.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_configure_ssh_crypto_policy
Identifiers and References | References: CCI-001453, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.312(e)(1), 164.312(e)(2)(ii), CIP-003-8 R4.2, CIP-007-3 R5.1, CIP-007-3 R7.1, AC-17(a), AC-17(2), CM-6(a), MA-4(6), SC-13, FCS_SSH_EXT.1, FCS_SSHS_EXT.1, FCS_SSHC_EXT.1, Req-2.2, 2.2.7, SRG-OS-000250-GPOS-00093, 5.2.14, SV-244526r877394_rule

Remediation Shell script ⇲

    SSH_CONF="/etc/sysconfig/sshd"

    sed -i "/^\s*CRYPTO_POLICY.*$/Id" $SSH_CONF

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    - name: Configure SSH to use System Crypto Policy
      lineinfile:
        dest: /etc/sysconfig/sshd
        state: absent
        regexp: ^(?i)\s*CRYPTO_POLICY.*$
      tags:
      - DISA-STIG-RHEL-08-010287
      - NIST-800-53-AC-17(2)
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-MA-4(6)
      - NIST-800-53-SC-13
      - PCI-DSS-Req-2.2
      - PCI-DSSv4-2.2.7
      - configure_ssh_crypto_policy
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required

#### Rule OpenSSL uses strong entropy source [ref]

By default, OpenSSL doesn't always use a SP800-90A compliant random number generator. A way to configure OpenSSL to always use a strong source is to setup a wrapper that defines a shell function that shadows the actual `openssl` binary, and that ensures that the `-rand /dev/random` option is added to every `openssl` invocation. To do so, place the following shell snippet exactly as-is to `/etc/profile.d/openssl-rand.sh`:

    # provide a default -rand /dev/random option to openssl commands that
    # support it

    # written inefficiently for maximum shell compatibility
    openssl()
    (
      openssl_bin=/usr/bin/openssl

      case "$*" in
        # if user specified -rand, honor it
        *\ -rand\ *|*\ -help*) exec $openssl_bin "$@" ;;
      esac

      cmds=`$openssl_bin list -digest-commands -cipher-commands | tr '\n' ' '`
      for i in `$openssl_bin list -commands`; do
        if $openssl_bin list -options "$i" | grep -q '^rand '; then
          cmds=" $i $cmds"
        fi
      done

      case "$cmds" in
        *\ "$1"\ *)
          cmd="$1"; shift
          exec $openssl_bin "$cmd" -rand /dev/random "$@" ;;
      esac

      exec $openssl_bin "$@"
    )

Warning: This setting can cause problems on computers without the hardware random generator, because insufficient entropy blocks the program until enough entropy is available.

Rationale: | This rule ensures that `openssl` invocations always uses SP800-90A compliant random number generator as a default behavior.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_openssl_use_strong_entropy
Identifiers and References | References: 1277, 1552, FCS_RBG_EXT.1.2, SRG-OS-000480-GPOS-00227

Remediation Shell script ⇲

    cat > /etc/profile.d/openssl-rand.sh <<- 'EOM'
    # provide a default -rand /dev/random option to openssl commands that
    # support it

    # written inefficiently for maximum shell compatibility
    openssl()
    (
      openssl_bin=/usr/bin/openssl

      case "$*" in
        # if user specified -rand, honor it
        *\ -rand\ *|*\ -help*) exec $openssl_bin "$@" ;;
      esac

      cmds=`$openssl_bin list -digest-commands -cipher-commands | tr '\n' ' '`
      for i in `$openssl_bin list -commands`; do
        if $openssl_bin list -options "$i" | grep -q '^rand '; then
          cmds=" $i $cmds"
        fi
      done

      case "$cmds" in
        *\ "$1"\ *)
          cmd="$1"; shift
          exec $openssl_bin "$cmd" -rand /dev/random "$@" ;;
      esac

      exec $openssl_bin "$@"
    )
    EOM

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Put a file with shell wrapper to configure OpenSSL to always use strong entropy
      copy:
        dest: /etc/profile.d/openssl-rand.sh
        content: |
          # provide a default -rand /dev/random option to openssl commands that
          # support it

          # written inefficiently for maximum shell compatibility
          openssl()
          (
            openssl_bin=/usr/bin/openssl

            case "$*" in
              # if user specified -rand, honor it
              *\ -rand\ *|*\ -help*) exec $openssl_bin "$@" ;;
            esac

            cmds=`$openssl_bin list -digest-commands -cipher-commands | tr '\n' ' '`
            for i in `$openssl_bin list -commands`; do
              if $openssl_bin list -options "$i" | grep -q '^rand '; then
                cmds=" $i $cmds"
              fi
            done

            case "$cmds" in
              *\ "$1"\ *)
                cmd="$1"; shift
                exec $openssl_bin "$cmd" -rand /dev/random "$@" ;;
            esac

            exec $openssl_bin "$@"
          )
      tags:
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - openssl_use_strong_entropy
      - restrict_strategy

Group Sudo Group contains 4 rules

[ref] `Sudo`, which stands for "su 'do'", provides the ability to delegate authority to certain users, groups of users, or system administrators. When configured for system users and/or groups, `Sudo` can allow a user or group to execute privileged commands that normally only `root` is allowed to execute.
For more information on `Sudo` and addition `Sudo` configuration options, see https://www.sudo.ws.

#### Rule Install sudo Package [ref]

The `sudo` package can be installed with the following command:

    $ sudo yum install sudo

Rationale: | `sudo` is a program designed to allow a system administrator to give limited root privileges to users and log root activity. The basic philosophy is to give as few privileges as possible but still allow system users to get their work done.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_sudo_installed
Identifiers and References | References: BP28(R19), 1382, 1384, 1386, CM-6(a), FMT_MOF_EXT.1, 2.2.6, SRG-OS-000324-GPOS-00125, 5.3.1

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    package --add=sudo

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: Ensure sudo is installed
      package:
        name: sudo
        state: present
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_sudo_installed

Remediation OSBuild Blueprint snippet ⇲

    [[packages]]
    name = "sudo"
    version = "*"

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include install_sudo

    class install_sudo {
      package { 'sudo':
        ensure => 'installed',
      }
    }

#### Rule Ensure Users Re-Authenticate for Privilege Escalation - sudo !authenticate [ref]

The sudo `!authenticate` option, when specified, allows a user to execute commands using sudo without having to authenticate. This should be disabled by making sure that the `!authenticate` option does not exist in `/etc/sudoers` configuration file or any sudo configuration snippets in `/etc/sudoers.d/`.
Rationale: | Without re-authentication, users may access resources or perform tasks for which they do not have authorization. When operating systems provide the capability to escalate a functional capability, it is critical that the user re-authenticate.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sudo_remove_no_authenticate

Identifiers and References

References: BP28(R5), BP28(R59), 1, 12, 15, 16, 5, DSS05.04, DSS05.10, DSS06.03, DSS06.10, CCI-002038, 4.3.3.5.1, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 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, A.18.1.4, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-11, CM-6(a), PR.AC-1, PR.AC-7, SRG-OS-000373-GPOS-00156, SRG-OS-000373-GPOS-00157, SRG-OS-000373-GPOS-00158, SV-230272r854027_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

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

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

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Find /etc/sudoers.d/ files
      ansible.builtin.find:
        paths:
        - /etc/sudoers.d/
      register: sudoers
      tags:
      - DISA-STIG-RHEL-08-010381
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-11
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_remove_no_authenticate

    - name: Remove lines containing !authenticate from sudoers files
      ansible.builtin.replace:
        regexp: (^(?!#).*[\s]+\!authenticate.*$)
        replace: '# \g<1>'
        path: '{{ item.path }}'
        validate: /usr/sbin/visudo -cf %s
      with_items:
      - path: /etc/sudoers
      - '{{ sudoers.files }}'
      tags:
      - DISA-STIG-RHEL-08-010381
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-11
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_remove_no_authenticate

#### Rule Ensure Users Re-Authenticate for Privilege Escalation - sudo NOPASSWD [ref]

The sudo `NOPASSWD` tag, when specified, allows a user to execute commands using sudo without having to authenticate. This should be disabled by making sure that the `NOPASSWD` tag does not exist in `/etc/sudoers` configuration file or any sudo configuration snippets in `/etc/sudoers.d/`.
Warning: This rule is disabled on Red Hat Virtualization Hosts and Managers, it will report not applicable. RHV requires to perform operations as root without being asked for password.

Rationale: | Without re-authentication, users may access resources or perform tasks for which they do not have authorization. When operating systems provide the capability to escalate a functional capability, it is critical that the user re-authenticate.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sudo_remove_nopasswd

Identifiers and References

References: BP28(R5), BP28(R59), 1, 12, 15, 16, 5, DSS05.04, DSS05.10, DSS06.03, DSS06.10, CCI-002038, 4.3.3.5.1, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 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, A.18.1.4, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-11, CM-6(a), PR.AC-1, PR.AC-7, SRG-OS-000373-GPOS-00156, SRG-OS-000373-GPOS-00157, SRG-OS-000373-GPOS-00158, SV-230271r854026_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

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

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

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Find /etc/sudoers.d/ files
      ansible.builtin.find:
        paths:
        - /etc/sudoers.d/
      register: sudoers
      tags:
      - DISA-STIG-RHEL-08-010380
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-11
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_remove_nopasswd

    - name: Remove lines containing NOPASSWD from sudoers files
      ansible.builtin.replace:
        regexp: (^(?!#).*[\s]+NOPASSWD[\s]*\:.*$)
        replace: '# \g<1>'
        path: '{{ item.path }}'
        validate: /usr/sbin/visudo -cf %s
      with_items:
      - path: /etc/sudoers
      - '{{ sudoers.files }}'
      tags:
      - DISA-STIG-RHEL-08-010380
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-11
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_remove_nopasswd

#### Rule Ensure Users Re-Authenticate for Privilege Escalation - sudo [ref]

The sudo `NOPASSWD` and `!authenticate` option, when specified, allows a user to execute commands using sudo without having to authenticate. This should be disabled by making sure that `NOPASSWD` and/or `!authenticate` do not exist in `/etc/sudoers` configuration file or any sudo configuration snippets in `/etc/sudoers.d/`."
Rationale: | Without re-authentication, users may access resources or perform tasks for which they do not have authorization. When operating systems provide the capability to escalate a functional capability, it is critical that the user re-authenticate.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sudo_require_authentication

Identifiers and References

References: 1, 12, 15, 16, 5, DSS05.04, DSS05.10, DSS06.03, DSS06.10, CCI-002038, 4.3.3.5.1, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 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, A.18.1.4, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-11, CM-6(a), PR.AC-1, PR.AC-7, 2.2.6, SRG-OS-000373-GPOS-00156, 5.3.4

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

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

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

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

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

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Find /etc/sudoers.d/ files
      ansible.builtin.find:
        paths:
        - /etc/sudoers.d/
      register: sudoers
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-11
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_require_authentication

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

    - name: Find /etc/sudoers.d/ files
      ansible.builtin.find:
        paths:
        - /etc/sudoers.d/
      register: sudoers
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-11
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_require_authentication

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

Group System Tooling / Utilities Group contains 1 rule
[ref] The following checks evaluate the system for recommended base packages -- both for installation and removal.

#### Rule Install rear Package [ref]

The `rear` package can be installed with the following command:

    $ sudo yum install rear

Rationale: | `rear` contains the Relax-and-Recover (ReaR) utility. ReaR produces a bootable image of a system and restores from backup using this image.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_rear_installed
Identifiers and References |

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    # Remediation is applicable only in certain platforms
    if ! ( ( ( grep -q aarch64 /proc/sys/kernel/osrelease && grep -qP "^ID=[\"']?ol[\"']?$" "/etc/os-release" && { real="$(grep -P "^VERSION_ID=[\"']?[\w.]+[\"']?$" /etc/os-release | sed "s/^VERSION_ID=[\"']\?\([^\"']\+\)[\"']\?$/\1/")"; expected="9.0"; printf "%s\n%s" "$expected" "$real" | sort -VC; } ) || ( grep -q aarch64 /proc/sys/kernel/osrelease && grep -qP "^ID=[\"']?rhel[\"']?$" "/etc/os-release" && { real="$(grep -P "^VERSION_ID=[\"']?[\w.]+[\"']?$" /etc/os-release | sed "s/^VERSION_ID=[\"']\?\([^\"']\+\)[\"']\?$/\1/")"; expected="9.0"; printf "%s\n%s" "$expected" "$real" | sort -VC; } ) || ( grep -qP "^ID=[\"']?rhel[\"']?$" "/etc/os-release" && { real="$(grep -P "^VERSION_ID=[\"']?[\w.]+[\"']?$" /etc/os-release | sed "s/^VERSION_ID=[\"']\?\([^\"']\+\)[\"']\?$/\1/")"; expected="8.4"; printf "%s\n%s" "$real" "$expected" | sort -VC; } && grep -q s390x /proc/sys/kernel/osrelease ) ) ); then

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    package --add=rear

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: Ensure rear is installed
      package:
        name: rear
        state: present
      when: not ( ( ( ansible_architecture == "aarch64" and ansible_distribution == 'OracleLinux'
        and ansible_distribution_version is version('9.0', '>=') ) or ( ansible_architecture
        == "aarch64" and ansible_distribution == 'RedHat' and ansible_distribution_version
        is version('9.0', '>=') ) or ( ansible_distribution == 'RedHat' and ansible_distribution_version
        is version('8.4', '<=') and ansible_architecture == "s390x" ) ) )
      tags:
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_rear_installed

Remediation OSBuild Blueprint snippet ⇲

    [[packages]]
    name = "rear"
    version = "*"

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include install_rear

    class install_rear {
      package { 'rear':
        ensure => 'installed',
      }
    }

Group Updating Software Group contains 6 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.
Red Hat Enterprise Linux 8 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 Configure dnf-automatic to Install Only Security Updates [ref]

To configure `dnf-automatic` to install only security updates automatically, set `upgrade_type` to `security` under `[commands]` section in `/etc/dnf/automatic.conf`.
Rationale: | By default, `dnf-automatic` installs all available updates. Reducing the amount of updated packages only to updates that were issued as a part of a security advisory increases the system stability.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_dnf-automatic_security_updates_only
Identifiers and References | References: BP28(R8), SI-2(5), CM-6(a), SI-2(c), FMT_SMF_EXT.1, SRG-OS-000191-GPOS-00080

Remediation Shell script ⇲

    found=false

    # set value in all files if they contain section or key
    for f in $(echo -n "/etc/dnf/automatic.conf"); do
        if [ ! -e "$f" ]; then
            continue
        fi

        # find key in section and change value
        if grep -qzosP "[[:space:]]*\[commands\]([^\n\[]*\n+)+?[[:space:]]*upgrade_type" "$f"; then
                sed -i "s/upgrade_type[^(\n)]*/upgrade_type = security/" "$f"
                found=true

        # find section and add key = value to it
        elif grep -qs "[[:space:]]*\[commands\]" "$f"; then
                sed -i "/[[:space:]]*\[commands\]/a upgrade_type = security" "$f"
                found=true
        fi
    done

    # if section not in any file, append section with key = value to FIRST file in files parameter
    if ! $found ; then
        file=$(echo "/etc/dnf/automatic.conf" | cut -f1 -d ' ')
        mkdir -p "$(dirname "$file")"
        echo -e "[commands]\nupgrade_type = security" >> "$file"
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | unknown

    - name: Configure dnf-automatic to Install Only Security Updates
      ini_file:
        dest: /etc/dnf/automatic.conf
        section: commands
        option: upgrade_type
        value: security
        create: true
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SI-2(5)
      - NIST-800-53-SI-2(c)
      - dnf-automatic_security_updates_only
      - low_complexity
      - low_severity
      - medium_disruption
      - no_reboot_needed
      - unknown_strategy

#### 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, 6.3.3, SRG-OS-000366-GPOS-00153, 1.2.3, SV-230264r880711_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q yum; then

    # 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
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^gpgcheck\\>.*/$escaped_formatted_output/gi" "/etc/yum.conf"
    else
        if [[ -s "/etc/yum.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/yum.conf" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/yum.conf"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/yum.conf" >> "/etc/yum.conf"
        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
Reboot: | false
Strategy: | configure

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.10.4.1
      - DISA-STIG-RHEL-08-010370
      - 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
      - PCI-DSSv4-6.3.3
      - 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-RHEL-08-010370
      - 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
      - PCI-DSSv4-6.3.3
      - configure_strategy
      - ensure_gpgcheck_globally_activated
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed

#### Rule Ensure gpgcheck Enabled for Local Packages [ref]

`yum` should be configured to verify the signature(s) of local packages prior to installation. To configure `yum` to verify signatures of local packages, set the `localpkg_gpgcheck` to `1` in `/etc/yum.conf`.
Rationale: | Changes to any software components can have significant effects to the overall security of the operating system. This requirement ensures the software has not been tampered and has been provided by a trusted vendor. Accordingly, patches, service packs, device drivers, or operating system components must be signed with a certificate recognized and approved by the organization.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_ensure_gpgcheck_local_packages

Identifiers and References

References: BP28(R15), 11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.4.8, CCI-001749, 164.308(a)(1)(ii)(D), 164.312(b), 164.312(c)(1), 164.312(c)(2), 164.312(e)(2)(i), 4.3.4.3.2, 4.3.4.3.3, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, CM-11(a), CM-11(b), CM-6(a), CM-5(3), SA-12, SA-12(10), PR.IP-1, FPT_TUD_EXT.1, FPT_TUD_EXT.2, SRG-OS-000366-GPOS-00153, SV-230265r877463_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q yum; then

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

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

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

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-010371
      - NIST-800-171-3.4.8
      - NIST-800-53-CM-11(a)
      - NIST-800-53-CM-11(b)
      - NIST-800-53-CM-5(3)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SA-12
      - NIST-800-53-SA-12(10)
      - ensure_gpgcheck_local_packages
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed
      - unknown_strategy

    - name: Ensure GPG check Enabled for Local Packages (yum)
      block:

      - name: Check stats of yum
        stat:
          path: /etc/yum.conf
        register: pkg

      - name: Check if config file of yum is a symlink
        ansible.builtin.set_fact:
          pkg_config_file_symlink: '{{ pkg.stat.lnk_target if pkg.stat.lnk_target is match("^/.*")
            else "/etc/yum.conf" | dirname ~ "/" ~ pkg.stat.lnk_target }}'
        when: pkg.stat.lnk_target is defined

      - name: Ensure GPG check Enabled for Local Packages (yum)
        ini_file:
          dest: '{{ pkg_config_file_symlink |  default("/etc/yum.conf") }}'
          section: main
          option: localpkg_gpgcheck
          value: 1
          no_extra_spaces: true
          create: true
      when: '"yum" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-08-010371
      - NIST-800-171-3.4.8
      - NIST-800-53-CM-11(a)
      - NIST-800-53-CM-11(b)
      - NIST-800-53-CM-5(3)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SA-12
      - NIST-800-53-SA-12(10)
      - ensure_gpgcheck_local_packages
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed
      - unknown_strategy

#### Rule Ensure gpgcheck Enabled for All yum Package Repositories [ref]

To ensure signature checking is not disabled for any repos, remove any lines from files in `/etc/yum.repos.d` of the form:

    gpgcheck=0

Rationale:

Verifying the authenticity of the software prior to installation validates the integrity of the patch or upgrade received from a vendor. This ensures the software has not been tampered with and that it has been provided by a trusted vendor. Self-signed certificates are disallowed by this requirement. Certificates used to verify the software must be from an approved Certificate Authority (CA)."

Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_ensure_gpgcheck_never_disabled

Identifiers and References

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, 6.3.3, SRG-OS-000366-GPOS-00153, SV-230264r880711_rule

Remediation Shell script ⇲

    sed -i 's/gpgcheck\s*=.*/gpgcheck=1/g' /etc/yum.repos.d/*

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | enable

    - name: Grep for yum repo section names
      shell: |
        set -o pipefail
        grep -HEr '^\[.+\]' -r /etc/yum.repos.d/
      register: repo_grep_results
      failed_when: repo_grep_results.rc not in [0, 1]
      changed_when: false
      tags:
      - CJIS-5.10.4.1
      - DISA-STIG-RHEL-08-010370
      - 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
      - PCI-DSSv4-6.3.3
      - enable_strategy
      - ensure_gpgcheck_never_disabled
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed

    - name: Set gpgcheck=1 for each yum repo
      ini_file:
        path: '{{ item[0] }}'
        section: '{{ item[1] }}'
        option: gpgcheck
        value: '1'
        no_extra_spaces: true
      loop: '{{ repo_grep_results.stdout | regex_findall( ''(.+\.repo):\[(.+)\]\n?'' )
        }}'
      tags:
      - CJIS-5.10.4.1
      - DISA-STIG-RHEL-08-010370
      - 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
      - PCI-DSSv4-6.3.3
      - enable_strategy
      - ensure_gpgcheck_never_disabled
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed

#### Rule Ensure Red Hat GPG Key Installed [ref]

To ensure the system can cryptographically verify base software packages come from Red Hat (and to connect to the Red Hat Network to receive them), the Red Hat GPG key must properly be installed. To install the Red Hat GPG key, run:

    $ sudo subscription-manager register

If the system is not connected to the Internet or an RHN Satellite, then install the Red Hat GPG key from trusted media such as the Red Hat installation CD-ROM or DVD. Assuming the disc is mounted in `/media/cdrom`, use the following command as the root user to import it into the keyring:

    $ sudo rpm --import /media/cdrom/RPM-GPG-KEY

Alternatively, the key may be pre-loaded during the RHEL installation. In such cases, the key can be installed by running the following command:

    sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release

Rationale: | Changes to software components can have significant effects on the overall security of the operating system. This requirement ensures the software has not been tampered with and that it has been provided by a trusted vendor. The Red Hat GPG key is necessary to cryptographically verify packages are from Red Hat.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_ensure_redhat_gpgkey_installed

Identifiers and References

References: BP28(R15), 11, 2, 3, 9, 5.10.4.1, APO01.06, BAI03.05, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS06.02, 3.4.8, CCI-001749, 164.308(a)(1)(ii)(D), 164.312(b), 164.312(c)(1), 164.312(c)(2), 164.312(e)(2)(i), 4.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4, SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 7.6, A.11.2.4, A.12.1.2, A.12.2.1, A.12.5.1, A.12.6.2, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, CIP-003-8 R4.2, CIP-003-8 R6, CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1, CM-5(3), SI-7, SC-12, SC-12(3), CM-6(a), PR.DS-6, PR.DS-8, PR.IP-1, FPT_TUD_EXT.1, FPT_TUD_EXT.2, Req-6.2, 6.3.3, SRG-OS-000366-GPOS-00153, 1.2.2, SV-256973r902752_rule

Remediation Shell script ⇲

    # The two fingerprints below are retrieved from https://access.redhat.com/security/team/key
    readonly REDHAT_RELEASE_FINGERPRINT="567E347AD0044ADE55BA8A5F199E2F91FD431D51"
    readonly REDHAT_AUXILIARY_FINGERPRINT="6A6AA7C97C8890AEC6AEBFE2F76F66C3D4082792"

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

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

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

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

      GPG_RESULT=$?
      # No CRC error, safe to proceed
      if [ "${GPG_RESULT}" -eq "0" ]
      then
        echo "${GPG_OUT[*]}" | grep -vE "${REDHAT_RELEASE_FINGERPRINT}|${REDHAT_AUXILIARY_FINGERPRINT}" || {
          # If $REDHAT_RELEASE_KEY file doesn't contain any keys with unknown fingerprint, import it
          rpm --import "${REDHAT_RELEASE_KEY}"
        }
      fi
    fi

Remediation Ansible snippet ⇲

Complexity: | medium
Disruption: | medium
Reboot: | false
Strategy: | restrict

    - name: Read permission of GPG key directory
      stat:
        path: /etc/pki/rpm-gpg/
      register: gpg_key_directory_permission
      check_mode: false
      tags:
      - CJIS-5.10.4.1
      - DISA-STIG-RHEL-08-010019
      - NIST-800-171-3.4.8
      - NIST-800-53-CM-5(3)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-12
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SI-7
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-6.3.3
      - ensure_redhat_gpgkey_installed
      - high_severity
      - medium_complexity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy

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

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

    - name: Set Fact - Valid fingerprints
      set_fact:
        gpg_valid_fingerprints: ("567E347AD0044ADE55BA8A5F199E2F91FD431D51" "6A6AA7C97C8890AEC6AEBFE2F76F66C3D4082792")
      tags:
      - CJIS-5.10.4.1
      - DISA-STIG-RHEL-08-010019
      - NIST-800-171-3.4.8
      - NIST-800-53-CM-5(3)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-12
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SI-7
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-6.3.3
      - ensure_redhat_gpgkey_installed
      - high_severity
      - medium_complexity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy

    - name: Import RedHat GPG key
      rpm_key:
        state: present
        key: /etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
      when:
      - gpg_key_directory_permission.stat.mode <= '0755'
      - (gpg_installed_fingerprints | difference(gpg_valid_fingerprints)) | length ==
        0
      - gpg_installed_fingerprints | length > 0
      - ansible_distribution == "RedHat"
      tags:
      - CJIS-5.10.4.1
      - DISA-STIG-RHEL-08-010019
      - NIST-800-171-3.4.8
      - NIST-800-53-CM-5(3)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-12
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SI-7
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-6.3.3
      - ensure_redhat_gpgkey_installed
      - high_severity
      - medium_complexity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy

#### Rule Ensure Software Patches Installed [ref]

If the system is joined to the Red Hat Network, a Red Hat Satellite Server, 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 Red Hat Network 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 Red Hat Enterprise Linux 8 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, 6.3.3, SRG-OS-000480-GPOS-00227, 1.9, SV-230222r627750_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-RHEL-08-010010
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SI-2(5)
      - NIST-800-53-SI-2(c)
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-6.3.3
      - 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 9 groups and 16 rules

[ref] In traditional Unix security, if an attacker gains shell access to a certain login account, they can perform any action or access any file to which that account has access. Therefore, making it more difficult for unauthorized people to gain shell access to accounts, particularly to privileged accounts, is a necessary part of securing a system. This section introduces mechanisms for restricting access to accounts under Red Hat Enterprise Linux 8.

Group Protect Accounts by Configuring PAM Group contains 3 groups and 6 rules

[ref] PAM, or Pluggable Authentication Modules, is a system which implements modular authentication for Linux programs. PAM provides a flexible and configurable architecture for authentication, and it should be configured to minimize exposure to unnecessary risk. This section contains guidance on how to accomplish that.
PAM is implemented as a set of shared objects which are loaded and invoked whenever an application wishes to authenticate a user. Typically, the application must be running as root in order to take advantage of PAM, because PAM's modules often need to be able to access sensitive stores of account information, such as /etc/shadow. Traditional privileged network listeners (e.g. sshd) or SUID programs (e.g. sudo) already meet this requirement. An SUID root application, userhelper, is provided so that programs which are not SUID or privileged themselves can still take advantage of PAM.
PAM looks in the directory `/etc/pam.d` for application-specific configuration information. For instance, if the program login attempts to authenticate a user, then PAM's libraries follow the instructions in the file `/etc/pam.d/login` to determine what actions should be taken.
One very important file in `/etc/pam.d` is `/etc/pam.d/system-auth`. This file, which is included by many other PAM configuration files, defines 'default' system authentication measures. Modifying this file is a good way to make far-reaching authentication changes, for instance when implementing a centralized authentication service.
Warning: Be careful when making changes to PAM's configuration files. The syntax for these files is complex, and modifications can have unexpected consequences. The default configurations shipped with applications should be sufficient for most users.
Warning: Running `authconfig` or `system-config-authentication` will re-write the PAM configuration files, destroying any manually made changes and replacing them with a series of system defaults. One reference to the configuration file syntax can be found at https://fossies.org/linux/Linux-PAM-docs/doc/sag/Linux-PAM_SAG.pdf.

Group Set Lockouts for Failed Password Attempts Group contains 4 rules

[ref] The `pam_faillock` PAM module provides the capability to lock out user accounts after a number of failed login attempts. Its documentation is available in `/usr/share/doc/pam-VERSION/txts/README.pam_faillock`.
Warning: Locking out user accounts presents the risk of a denial-of-service attack. The lockout policy must weigh whether the risk of such a denial-of-service attack outweighs the benefits of thwarting password guessing attacks.

#### Rule Lock Accounts After Failed Password Attempts [ref]

This rule configures the system to lock out accounts after a number of incorrect login attempts using `pam_faillock.so`. pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. Ensure that the file `/etc/security/faillock.conf` contains the following entry: `deny = <count>` Where count should be less than or equal to 3 and greater than 0. In order to avoid errors when manually editing these files, it is recommended to use the appropriate tools, such as `authselect` or `authconfig`, depending on the OS version.
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. If the system supports the `/etc/security/faillock.conf` file, the pam_faillock parameters should be defined in `faillock.conf` file.

Rationale: | By limiting the number of failed logon attempts, the risk of unauthorized system access via user password guessing, also known as brute-forcing, is reduced. Limits are imposed by locking the account.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, 5.5.3, DSS05.04, DSS05.10, DSS06.10, 3.1.8, CCI-000044, CCI-002236, CCI-002237, CCI-002238, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, CM-6(a), AC-7(a), PR.AC-7, FIA_AFL.1, Req-8.1.6, 8.3.4, SRG-OS-000329-GPOS-00128, SRG-OS-000021-GPOS-00005, 5.4.2, 5.5.2, SV-230333r743966_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    var_accounts_passwords_pam_faillock_deny='3'

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

    authselect apply-changes -b
    else

    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")
    for pam_file in "${AUTH_FILES[@]}"
    do
        if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then
            sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth        required      pam_faillock.so preauth silent' "$pam_file"
            sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth        required      pam_faillock.so authfail' "$pam_file"
            sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account     required      pam_faillock.so' "$pam_file"
        fi
        sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required     \3/g' "$pam_file"
    done

    fi

    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")

    FAILLOCK_CONF="/etc/security/faillock.conf"
    if [ -f $FAILLOCK_CONF ]; then
        regex="^\s*deny\s*="
        line="deny = $var_accounts_passwords_pam_faillock_deny"
        if ! grep -q $regex $FAILLOCK_CONF; then
            echo $line >> $FAILLOCK_CONF
        else
            sed -i --follow-symlinks 's|^\s*\(deny\s*=\s*\)\(\S\+\)|\1'"$var_accounts_passwords_pam_faillock_deny"'|g' $FAILLOCK_CONF
        fi
        for pam_file in "${AUTH_FILES[@]}"
        do
            if [ -e "$pam_file" ] ; then
                PAM_FILE_PATH="$pam_file"
                if [ -f /usr/bin/authselect ]; then

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

                    CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
                    # If not already in use, a custom profile is created preserving the enabled features.
                    if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                        ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                        authselect create-profile hardening -b $CURRENT_PROFILE
                        CURRENT_PROFILE="custom/hardening"

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

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

                    authselect apply-changes -b
                fi

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

                    authselect apply-changes -b
                fi
            else
                echo "$pam_file was not found" >&2
            fi
        done
    else
        for pam_file in "${AUTH_FILES[@]}"
        do
            if ! grep -qE '^\s*auth.*pam_faillock\.so (preauth|authfail).*deny' "$pam_file"; then
                sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*silent.*/ s/$/ deny='"$var_accounts_passwords_pam_faillock_deny"'/' "$pam_file"
                sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*authfail.*/ s/$/ deny='"$var_accounts_passwords_pam_faillock_deny"'/' "$pam_file"
            else
                sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*preauth.*silent.*\)\('"deny"'=\)[0-9]\+\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_deny"'\3/' "$pam_file"
                sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*authfail.*\)\('"deny"'=\)[0-9]\+\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_deny"'\3/' "$pam_file"
            fi
        done
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020011
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.6
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_deny
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Lock Accounts After Failed Password Attempts - Check if system relies on authselect
        tool
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present
      when: '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020011
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.6
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_deny
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Lock Accounts After Failed Password Attempts - Remediation where authselect
        tool is present
      block:

      - name: Lock Accounts After Failed Password Attempts - Check integrity of authselect
          current profile
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        failed_when: false

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

      - name: Lock Accounts After Failed Password Attempts - Get authselect current features
        ansible.builtin.shell:
          cmd: authselect current | tail -n+3 | awk '{ print $2 }'
        register: result_authselect_features
        changed_when: false
        when:
        - result_authselect_check_cmd is success

      - name: Lock Accounts After Failed Password Attempts - Ensure "with-faillock" feature
          is enabled using authselect tool
        ansible.builtin.command:
          cmd: authselect enable-feature with-faillock
        register: result_authselect_enable_feature_cmd
        when:
        - result_authselect_check_cmd is success
        - result_authselect_features.stdout is not search("with-faillock")

      - name: Lock Accounts After Failed Password Attempts - Ensure authselect changes
          are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_enable_feature_cmd is not skipped
        - result_authselect_enable_feature_cmd is success
      when:
      - '"pam" in ansible_facts.packages'
      - result_authselect_present.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020011
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.6
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_deny
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Lock Accounts After Failed Password Attempts - Remediation where authselect
        tool is not present
      block:

      - name: Lock Accounts After Failed Password Attempts - Check if pam_faillock.so
          is already enabled
        ansible.builtin.lineinfile:
          path: /etc/pam.d/system-auth
          regexp: .*auth.*pam_faillock\.so (preauth|authfail)
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_faillock_is_enabled

      - name: Lock Accounts After Failed Password Attempts - Enable pam_faillock.so preauth
          editing PAM files
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          line: auth        required      pam_faillock.so preauth
          insertbefore: ^auth.*sufficient.*pam_unix\.so.*
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_is_enabled.found == 0

      - name: Lock Accounts After Failed Password Attempts - Enable pam_faillock.so authfail
          editing PAM files
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          line: auth        required      pam_faillock.so authfail
          insertbefore: ^auth.*required.*pam_deny\.so.*
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_is_enabled.found == 0

      - name: Lock Accounts After Failed Password Attempts - Enable pam_faillock.so account
          section editing PAM files
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          line: account     required      pam_faillock.so
          insertbefore: ^account.*required.*pam_unix\.so.*
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_is_enabled.found == 0
      when:
      - '"pam" in ansible_facts.packages'
      - not result_authselect_present.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020011
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.6
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_deny
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_passwords_pam_faillock_deny # promote to variable
      set_fact:
        var_accounts_passwords_pam_faillock_deny: !!str 3
      tags:
        - always

    - name: Lock Accounts After Failed Password Attempts - Check the presence of /etc/security/faillock.conf
        file
      ansible.builtin.stat:
        path: /etc/security/faillock.conf
      register: result_faillock_conf_check
      when: '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020011
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.6
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_deny
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Lock Accounts After Failed Password Attempts - Ensure the pam_faillock.so
        deny parameter in /etc/security/faillock.conf
      ansible.builtin.lineinfile:
        path: /etc/security/faillock.conf
        regexp: ^\s*deny\s*=
        line: deny = {{ var_accounts_passwords_pam_faillock_deny }}
        state: present
      when:
      - '"pam" in ansible_facts.packages'
      - result_faillock_conf_check.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020011
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.6
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_deny
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Lock Accounts After Failed Password Attempts - Ensure the pam_faillock.so
        deny parameter not in PAM files
      block:

      - name: Lock Accounts After Failed Password Attempts - Check if /etc/pam.d/system-auth
          file is present
        ansible.builtin.stat:
          path: /etc/pam.d/system-auth
        register: result_pam_file_present

      - name: Lock Accounts After Failed Password Attempts - Check the proper remediation
          for the system
        block:

        - name: Lock Accounts After Failed Password Attempts - Define the PAM file to
            be edited as a local fact
          ansible.builtin.set_fact:
            pam_file_path: /etc/pam.d/system-auth

        - name: Lock Accounts After Failed Password Attempts - Check if system relies
            on authselect tool
          ansible.builtin.stat:
            path: /usr/bin/authselect
          register: result_authselect_present

        - name: Lock Accounts After Failed Password Attempts - Ensure authselect custom
            profile is used if authselect is present
          block:

          - name: Lock Accounts After Failed Password Attempts - Check integrity of authselect
              current profile
            ansible.builtin.command:
              cmd: authselect check
            register: result_authselect_check_cmd
            changed_when: false
            failed_when: false

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

          - name: Lock Accounts After Failed Password Attempts - Get authselect current
              profile
            ansible.builtin.shell:
              cmd: authselect current -r | awk '{ print $1 }'
            register: result_authselect_profile
            changed_when: false
            when:
            - result_authselect_check_cmd is success

          - name: Lock Accounts After Failed Password Attempts - Define the current authselect
              profile as a local fact
            ansible.builtin.set_fact:
              authselect_current_profile: '{{ result_authselect_profile.stdout }}'
              authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
            when:
            - result_authselect_profile is not skipped
            - result_authselect_profile.stdout is match("custom/")

          - name: Lock Accounts After Failed Password Attempts - Define the new authselect
              custom profile as a local fact
            ansible.builtin.set_fact:
              authselect_current_profile: '{{ result_authselect_profile.stdout }}'
              authselect_custom_profile: custom/hardening
            when:
            - result_authselect_profile is not skipped
            - result_authselect_profile.stdout is not match("custom/")

          - name: Lock Accounts After Failed Password Attempts - Get authselect current
              features to also enable them in the custom profile
            ansible.builtin.shell:
              cmd: authselect current | tail -n+3 | awk '{ print $2 }'
            register: result_authselect_features
            changed_when: false
            when:
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")

          - name: Lock Accounts After Failed Password Attempts - Check if any custom profile
              with the same name was already created
            ansible.builtin.stat:
              path: /etc/authselect/{{ authselect_custom_profile }}
            register: result_authselect_custom_profile_present
            changed_when: false
            when:
            - authselect_current_profile is not match("custom/")

          - name: Lock Accounts After Failed Password Attempts - Create an authselect
              custom profile based on the current profile
            ansible.builtin.command:
              cmd: authselect create-profile hardening -b {{ authselect_current_profile
                }}
            when:
            - result_authselect_check_cmd is success
            - authselect_current_profile is not match("custom/")
            - not result_authselect_custom_profile_present.stat.exists

          - name: Lock Accounts After Failed Password Attempts - Ensure authselect changes
              are applied
            ansible.builtin.command:
              cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")
            - authselect_custom_profile is not match(authselect_current_profile)

          - name: Lock Accounts After Failed Password Attempts - Ensure the authselect
              custom profile is selected
            ansible.builtin.command:
              cmd: authselect select {{ authselect_custom_profile }}
            register: result_pam_authselect_select_profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")
            - authselect_custom_profile is not match(authselect_current_profile)

          - name: Lock Accounts After Failed Password Attempts - Restore the authselect
              features in the custom profile
            ansible.builtin.command:
              cmd: authselect enable-feature {{ item }}
            loop: '{{ result_authselect_features.stdout_lines }}'
            register: result_pam_authselect_restore_features
            when:
            - result_authselect_profile is not skipped
            - result_authselect_features is not skipped
            - result_pam_authselect_select_profile is not skipped

          - name: Lock Accounts After Failed Password Attempts - Ensure authselect changes
              are applied
            ansible.builtin.command:
              cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - result_pam_authselect_restore_features is not skipped

          - name: Lock Accounts After Failed Password Attempts - Change the PAM file to
              be edited according to the custom authselect profile
            ansible.builtin.set_fact:
              pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
                | basename }}
          when:
          - result_authselect_present.stat.exists

        - name: Lock Accounts After Failed Password Attempts - Ensure the "deny" option
            from "pam_faillock.so" is not present in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: (.*auth.*pam_faillock.so.*)\bdeny\b=?[0-9a-zA-Z]*(.*)
            replace: \1\2
          register: result_pam_option_removal

        - name: Lock Accounts After Failed Password Attempts - Ensure authselect changes
            are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b
          when:
          - result_authselect_present.stat.exists
          - result_pam_option_removal is changed
        when:
        - result_pam_file_present.stat.exists

      - name: Lock Accounts After Failed Password Attempts - Check if /etc/pam.d/password-auth
          file is present
        ansible.builtin.stat:
          path: /etc/pam.d/password-auth
        register: result_pam_file_present

      - name: Lock Accounts After Failed Password Attempts - Check the proper remediation
          for the system
        block:

        - name: Lock Accounts After Failed Password Attempts - Define the PAM file to
            be edited as a local fact
          ansible.builtin.set_fact:
            pam_file_path: /etc/pam.d/password-auth

        - name: Lock Accounts After Failed Password Attempts - Check if system relies
            on authselect tool
          ansible.builtin.stat:
            path: /usr/bin/authselect
          register: result_authselect_present

        - name: Lock Accounts After Failed Password Attempts - Ensure authselect custom
            profile is used if authselect is present
          block:

          - name: Lock Accounts After Failed Password Attempts - Check integrity of authselect
              current profile
            ansible.builtin.command:
              cmd: authselect check
            register: result_authselect_check_cmd
            changed_when: false
            failed_when: false

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

          - name: Lock Accounts After Failed Password Attempts - Get authselect current
              profile
            ansible.builtin.shell:
              cmd: authselect current -r | awk '{ print $1 }'
            register: result_authselect_profile
            changed_when: false
            when:
            - result_authselect_check_cmd is success

          - name: Lock Accounts After Failed Password Attempts - Define the current authselect
              profile as a local fact
            ansible.builtin.set_fact:
              authselect_current_profile: '{{ result_authselect_profile.stdout }}'
              authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
            when:
            - result_authselect_profile is not skipped
            - result_authselect_profile.stdout is match("custom/")

          - name: Lock Accounts After Failed Password Attempts - Define the new authselect
              custom profile as a local fact
            ansible.builtin.set_fact:
              authselect_current_profile: '{{ result_authselect_profile.stdout }}'
              authselect_custom_profile: custom/hardening
            when:
            - result_authselect_profile is not skipped
            - result_authselect_profile.stdout is not match("custom/")

          - name: Lock Accounts After Failed Password Attempts - Get authselect current
              features to also enable them in the custom profile
            ansible.builtin.shell:
              cmd: authselect current | tail -n+3 | awk '{ print $2 }'
            register: result_authselect_features
            changed_when: false
            when:
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")

          - name: Lock Accounts After Failed Password Attempts - Check if any custom profile
              with the same name was already created
            ansible.builtin.stat:
              path: /etc/authselect/{{ authselect_custom_profile }}
            register: result_authselect_custom_profile_present
            changed_when: false
            when:
            - authselect_current_profile is not match("custom/")

          - name: Lock Accounts After Failed Password Attempts - Create an authselect
              custom profile based on the current profile
            ansible.builtin.command:
              cmd: authselect create-profile hardening -b {{ authselect_current_profile
                }}
            when:
            - result_authselect_check_cmd is success
            - authselect_current_profile is not match("custom/")
            - not result_authselect_custom_profile_present.stat.exists

          - name: Lock Accounts After Failed Password Attempts - Ensure authselect changes
              are applied
            ansible.builtin.command:
              cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")
            - authselect_custom_profile is not match(authselect_current_profile)

          - name: Lock Accounts After Failed Password Attempts - Ensure the authselect
              custom profile is selected
            ansible.builtin.command:
              cmd: authselect select {{ authselect_custom_profile }}
            register: result_pam_authselect_select_profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")
            - authselect_custom_profile is not match(authselect_current_profile)

          - name: Lock Accounts After Failed Password Attempts - Restore the authselect
              features in the custom profile
            ansible.builtin.command:
              cmd: authselect enable-feature {{ item }}
            loop: '{{ result_authselect_features.stdout_lines }}'
            register: result_pam_authselect_restore_features
            when:
            - result_authselect_profile is not skipped
            - result_authselect_features is not skipped
            - result_pam_authselect_select_profile is not skipped

          - name: Lock Accounts After Failed Password Attempts - Ensure authselect changes
              are applied
            ansible.builtin.command:
              cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - result_pam_authselect_restore_features is not skipped

          - name: Lock Accounts After Failed Password Attempts - Change the PAM file to
              be edited according to the custom authselect profile
            ansible.builtin.set_fact:
              pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
                | basename }}
          when:
          - result_authselect_present.stat.exists

        - name: Lock Accounts After Failed Password Attempts - Ensure the "deny" option
            from "pam_faillock.so" is not present in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: (.*auth.*pam_faillock.so.*)\bdeny\b=?[0-9a-zA-Z]*(.*)
            replace: \1\2
          register: result_pam_option_removal

        - name: Lock Accounts After Failed Password Attempts - Ensure authselect changes
            are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b
          when:
          - result_authselect_present.stat.exists
          - result_pam_option_removal is changed
        when:
        - result_pam_file_present.stat.exists
      when:
      - '"pam" in ansible_facts.packages'
      - result_faillock_conf_check.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020011
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.6
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_deny
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Lock Accounts After Failed Password Attempts - Ensure the pam_faillock.so
        deny parameter in PAM files
      block:

      - name: Lock Accounts After Failed Password Attempts - Check if pam_faillock.so
          deny parameter is already enabled in pam files
        ansible.builtin.lineinfile:
          path: /etc/pam.d/system-auth
          regexp: .*auth.*pam_faillock\.so (preauth|authfail).*deny
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_faillock_deny_parameter_is_present

      - name: Lock Accounts After Failed Password Attempts - Ensure the inclusion of pam_faillock.so
          preauth deny parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*)
          line: \1required\3 deny={{ var_accounts_passwords_pam_faillock_deny }}
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_deny_parameter_is_present.found == 0

      - name: Lock Accounts After Failed Password Attempts - Ensure the inclusion of pam_faillock.so
          authfail deny parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)
          line: \1required\3 deny={{ var_accounts_passwords_pam_faillock_deny }}
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_deny_parameter_is_present.found == 0

      - name: Lock Accounts After Failed Password Attempts - Ensure the desired value
          for pam_faillock.so preauth deny parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*)(deny)=[0-9]+(.*)
          line: \1required\3\4={{ var_accounts_passwords_pam_faillock_deny }}\5
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_deny_parameter_is_present.found > 0

      - name: Lock Accounts After Failed Password Attempts - Ensure the desired value
          for pam_faillock.so authfail deny parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)(deny)=[0-9]+(.*)
          line: \1required\3\4={{ var_accounts_passwords_pam_faillock_deny }}\5
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_deny_parameter_is_present.found > 0
      when:
      - '"pam" in ansible_facts.packages'
      - not result_faillock_conf_check.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020011
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.6
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_deny
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Configure the root Account for Failed Password Attempts [ref]

This rule configures the system to lock out the `root` account after a number of incorrect login attempts using `pam_faillock.so`. pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. In order to avoid errors when manually editing these files, it is recommended to use the appropriate tools, such as `authselect` or `authconfig`, depending on the OS version.
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. If the system supports the `/etc/security/faillock.conf` file, the pam_faillock parameters should be defined in `faillock.conf` file.

Rationale: | By limiting the number of failed logon attempts, the risk of unauthorized system access via user password guessing, also known as brute-forcing, is reduced. Limits are imposed by locking the account.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny_root

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, DSS05.04, DSS05.10, DSS06.10, CCI-002238, CCI-000044, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, CM-6(a), AC-7(b), IA-5(c), PR.AC-7, FMT_MOF_EXT.1, SRG-OS-000329-GPOS-00128, SRG-OS-000021-GPOS-00005, SV-230345r743984_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

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

    authselect apply-changes -b
    else

    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")
    for pam_file in "${AUTH_FILES[@]}"
    do
        if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then
            sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth        required      pam_faillock.so preauth silent' "$pam_file"
            sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth        required      pam_faillock.so authfail' "$pam_file"
            sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account     required      pam_faillock.so' "$pam_file"
        fi
        sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required     \3/g' "$pam_file"
    done

    fi

    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")

    FAILLOCK_CONF="/etc/security/faillock.conf"
    if [ -f $FAILLOCK_CONF ]; then
        regex="^\s*even_deny_root"
        line="even_deny_root"
        if ! grep -q $regex $FAILLOCK_CONF; then
            echo $line >> $FAILLOCK_CONF
        fi
        for pam_file in "${AUTH_FILES[@]}"
        do
            if [ -e "$pam_file" ] ; then
                PAM_FILE_PATH="$pam_file"
                if [ -f /usr/bin/authselect ]; then

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

                    CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
                    # If not already in use, a custom profile is created preserving the enabled features.
                    if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                        ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                        authselect create-profile hardening -b $CURRENT_PROFILE
                        CURRENT_PROFILE="custom/hardening"

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

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

                    authselect apply-changes -b
                fi

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

                    authselect apply-changes -b
                fi
            else
                echo "$pam_file was not found" >&2
            fi
        done
    else
        for pam_file in "${AUTH_FILES[@]}"
        do
            if ! grep -qE '^\s*auth.*pam_faillock\.so (preauth|authfail).*even_deny_root' "$pam_file"; then
                sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*silent.*/ s/$/ even_deny_root/' "$pam_file"
                sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*authfail.*/ s/$/ even_deny_root/' "$pam_file"
            fi
        done
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-020023
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(c)
      - accounts_passwords_pam_faillock_deny_root
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Configure the root Account for Failed Password Attempts - Check if system
        relies on authselect tool
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present
      when: '"pam" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-08-020023
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(c)
      - accounts_passwords_pam_faillock_deny_root
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Configure the root Account for Failed Password Attempts - Remediation where
        authselect tool is present
      block:

      - name: Configure the root Account for Failed Password Attempts - Check integrity
          of authselect current profile
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        failed_when: false

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

      - name: Configure the root Account for Failed Password Attempts - Get authselect
          current features
        ansible.builtin.shell:
          cmd: authselect current | tail -n+3 | awk '{ print $2 }'
        register: result_authselect_features
        changed_when: false
        when:
        - result_authselect_check_cmd is success

      - name: Configure the root Account for Failed Password Attempts - Ensure "with-faillock"
          feature is enabled using authselect tool
        ansible.builtin.command:
          cmd: authselect enable-feature with-faillock
        register: result_authselect_enable_feature_cmd
        when:
        - result_authselect_check_cmd is success
        - result_authselect_features.stdout is not search("with-faillock")

      - name: Configure the root Account for Failed Password Attempts - Ensure authselect
          changes are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_enable_feature_cmd is not skipped
        - result_authselect_enable_feature_cmd is success
      when:
      - '"pam" in ansible_facts.packages'
      - result_authselect_present.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020023
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(c)
      - accounts_passwords_pam_faillock_deny_root
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Configure the root Account for Failed Password Attempts - Remediation where
        authselect tool is not present
      block:

      - name: Configure the root Account for Failed Password Attempts - Check if pam_faillock.so
          is already enabled
        ansible.builtin.lineinfile:
          path: /etc/pam.d/system-auth
          regexp: .*auth.*pam_faillock\.so (preauth|authfail)
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_faillock_is_enabled

      - name: Configure the root Account for Failed Password Attempts - Enable pam_faillock.so
          preauth editing PAM files
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          line: auth        required      pam_faillock.so preauth
          insertbefore: ^auth.*sufficient.*pam_unix\.so.*
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_is_enabled.found == 0

      - name: Configure the root Account for Failed Password Attempts - Enable pam_faillock.so
          authfail editing PAM files
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          line: auth        required      pam_faillock.so authfail
          insertbefore: ^auth.*required.*pam_deny\.so.*
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_is_enabled.found == 0

      - name: Configure the root Account for Failed Password Attempts - Enable pam_faillock.so
          account section editing PAM files
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          line: account     required      pam_faillock.so
          insertbefore: ^account.*required.*pam_unix\.so.*
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_is_enabled.found == 0
      when:
      - '"pam" in ansible_facts.packages'
      - not result_authselect_present.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020023
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(c)
      - accounts_passwords_pam_faillock_deny_root
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Configure the root Account for Failed Password Attempts - Check the presence
        of /etc/security/faillock.conf file
      ansible.builtin.stat:
        path: /etc/security/faillock.conf
      register: result_faillock_conf_check
      when: '"pam" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-08-020023
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(c)
      - accounts_passwords_pam_faillock_deny_root
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Configure the root Account for Failed Password Attempts - Ensure the pam_faillock.so
        even_deny_root parameter in /etc/security/faillock.conf
      ansible.builtin.lineinfile:
        path: /etc/security/faillock.conf
        regexp: ^\s*even_deny_root
        line: even_deny_root
        state: present
      when:
      - '"pam" in ansible_facts.packages'
      - result_faillock_conf_check.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020023
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(c)
      - accounts_passwords_pam_faillock_deny_root
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Configure the root Account for Failed Password Attempts - Ensure the pam_faillock.so
        even_deny_root parameter not in PAM files
      block:

      - name: Configure the root Account for Failed Password Attempts - Check if /etc/pam.d/system-auth
          file is present
        ansible.builtin.stat:
          path: /etc/pam.d/system-auth
        register: result_pam_file_present

      - name: Configure the root Account for Failed Password Attempts - Check the proper
          remediation for the system
        block:

        - name: Configure the root Account for Failed Password Attempts - Define the PAM
            file to be edited as a local fact
          ansible.builtin.set_fact:
            pam_file_path: /etc/pam.d/system-auth

        - name: Configure the root Account for Failed Password Attempts - Check if system
            relies on authselect tool
          ansible.builtin.stat:
            path: /usr/bin/authselect
          register: result_authselect_present

        - name: Configure the root Account for Failed Password Attempts - Ensure authselect
            custom profile is used if authselect is present
          block:

          - name: Configure the root Account for Failed Password Attempts - Check integrity
              of authselect current profile
            ansible.builtin.command:
              cmd: authselect check
            register: result_authselect_check_cmd
            changed_when: false
            failed_when: false

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

          - name: Configure the root Account for Failed Password Attempts - Get authselect
              current profile
            ansible.builtin.shell:
              cmd: authselect current -r | awk '{ print $1 }'
            register: result_authselect_profile
            changed_when: false
            when:
            - result_authselect_check_cmd is success

          - name: Configure the root Account for Failed Password Attempts - Define the
              current authselect profile as a local fact
            ansible.builtin.set_fact:
              authselect_current_profile: '{{ result_authselect_profile.stdout }}'
              authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
            when:
            - result_authselect_profile is not skipped
            - result_authselect_profile.stdout is match("custom/")

          - name: Configure the root Account for Failed Password Attempts - Define the
              new authselect custom profile as a local fact
            ansible.builtin.set_fact:
              authselect_current_profile: '{{ result_authselect_profile.stdout }}'
              authselect_custom_profile: custom/hardening
            when:
            - result_authselect_profile is not skipped
            - result_authselect_profile.stdout is not match("custom/")

          - name: Configure the root Account for Failed Password Attempts - Get authselect
              current features to also enable them in the custom profile
            ansible.builtin.shell:
              cmd: authselect current | tail -n+3 | awk '{ print $2 }'
            register: result_authselect_features
            changed_when: false
            when:
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")

          - name: Configure the root Account for Failed Password Attempts - Check if any
              custom profile with the same name was already created
            ansible.builtin.stat:
              path: /etc/authselect/{{ authselect_custom_profile }}
            register: result_authselect_custom_profile_present
            changed_when: false
            when:
            - authselect_current_profile is not match("custom/")

          - name: Configure the root Account for Failed Password Attempts - Create an
              authselect custom profile based on the current profile
            ansible.builtin.command:
              cmd: authselect create-profile hardening -b {{ authselect_current_profile
                }}
            when:
            - result_authselect_check_cmd is success
            - authselect_current_profile is not match("custom/")
            - not result_authselect_custom_profile_present.stat.exists

          - name: Configure the root Account for Failed Password Attempts - Ensure authselect
              changes are applied
            ansible.builtin.command:
              cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")
            - authselect_custom_profile is not match(authselect_current_profile)

          - name: Configure the root Account for Failed Password Attempts - Ensure the
              authselect custom profile is selected
            ansible.builtin.command:
              cmd: authselect select {{ authselect_custom_profile }}
            register: result_pam_authselect_select_profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")
            - authselect_custom_profile is not match(authselect_current_profile)

          - name: Configure the root Account for Failed Password Attempts - Restore the
              authselect features in the custom profile
            ansible.builtin.command:
              cmd: authselect enable-feature {{ item }}
            loop: '{{ result_authselect_features.stdout_lines }}'
            register: result_pam_authselect_restore_features
            when:
            - result_authselect_profile is not skipped
            - result_authselect_features is not skipped
            - result_pam_authselect_select_profile is not skipped

          - name: Configure the root Account for Failed Password Attempts - Ensure authselect
              changes are applied
            ansible.builtin.command:
              cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - result_pam_authselect_restore_features is not skipped

          - name: Configure the root Account for Failed Password Attempts - Change the
              PAM file to be edited according to the custom authselect profile
            ansible.builtin.set_fact:
              pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
                | basename }}
          when:
          - result_authselect_present.stat.exists

        - name: Configure the root Account for Failed Password Attempts - Ensure the "even_deny_root"
            option from "pam_faillock.so" is not present in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: (.*auth.*pam_faillock.so.*)\beven_deny_root\b=?[0-9a-zA-Z]*(.*)
            replace: \1\2
          register: result_pam_option_removal

        - name: Configure the root Account for Failed Password Attempts - Ensure authselect
            changes are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b
          when:
          - result_authselect_present.stat.exists
          - result_pam_option_removal is changed
        when:
        - result_pam_file_present.stat.exists

      - name: Configure the root Account for Failed Password Attempts - Check if /etc/pam.d/password-auth
          file is present
        ansible.builtin.stat:
          path: /etc/pam.d/password-auth
        register: result_pam_file_present

      - name: Configure the root Account for Failed Password Attempts - Check the proper
          remediation for the system
        block:

        - name: Configure the root Account for Failed Password Attempts - Define the PAM
            file to be edited as a local fact
          ansible.builtin.set_fact:
            pam_file_path: /etc/pam.d/password-auth

        - name: Configure the root Account for Failed Password Attempts - Check if system
            relies on authselect tool
          ansible.builtin.stat:
            path: /usr/bin/authselect
          register: result_authselect_present

        - name: Configure the root Account for Failed Password Attempts - Ensure authselect
            custom profile is used if authselect is present
          block:

          - name: Configure the root Account for Failed Password Attempts - Check integrity
              of authselect current profile
            ansible.builtin.command:
              cmd: authselect check
            register: result_authselect_check_cmd
            changed_when: false
            failed_when: false

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

          - name: Configure the root Account for Failed Password Attempts - Get authselect
              current profile
            ansible.builtin.shell:
              cmd: authselect current -r | awk '{ print $1 }'
            register: result_authselect_profile
            changed_when: false
            when:
            - result_authselect_check_cmd is success

          - name: Configure the root Account for Failed Password Attempts - Define the
              current authselect profile as a local fact
            ansible.builtin.set_fact:
              authselect_current_profile: '{{ result_authselect_profile.stdout }}'
              authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
            when:
            - result_authselect_profile is not skipped
            - result_authselect_profile.stdout is match("custom/")

          - name: Configure the root Account for Failed Password Attempts - Define the
              new authselect custom profile as a local fact
            ansible.builtin.set_fact:
              authselect_current_profile: '{{ result_authselect_profile.stdout }}'
              authselect_custom_profile: custom/hardening
            when:
            - result_authselect_profile is not skipped
            - result_authselect_profile.stdout is not match("custom/")

          - name: Configure the root Account for Failed Password Attempts - Get authselect
              current features to also enable them in the custom profile
            ansible.builtin.shell:
              cmd: authselect current | tail -n+3 | awk '{ print $2 }'
            register: result_authselect_features
            changed_when: false
            when:
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")

          - name: Configure the root Account for Failed Password Attempts - Check if any
              custom profile with the same name was already created
            ansible.builtin.stat:
              path: /etc/authselect/{{ authselect_custom_profile }}
            register: result_authselect_custom_profile_present
            changed_when: false
            when:
            - authselect_current_profile is not match("custom/")

          - name: Configure the root Account for Failed Password Attempts - Create an
              authselect custom profile based on the current profile
            ansible.builtin.command:
              cmd: authselect create-profile hardening -b {{ authselect_current_profile
                }}
            when:
            - result_authselect_check_cmd is success
            - authselect_current_profile is not match("custom/")
            - not result_authselect_custom_profile_present.stat.exists

          - name: Configure the root Account for Failed Password Attempts - Ensure authselect
              changes are applied
            ansible.builtin.command:
              cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")
            - authselect_custom_profile is not match(authselect_current_profile)

          - name: Configure the root Account for Failed Password Attempts - Ensure the
              authselect custom profile is selected
            ansible.builtin.command:
              cmd: authselect select {{ authselect_custom_profile }}
            register: result_pam_authselect_select_profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")
            - authselect_custom_profile is not match(authselect_current_profile)

          - name: Configure the root Account for Failed Password Attempts - Restore the
              authselect features in the custom profile
            ansible.builtin.command:
              cmd: authselect enable-feature {{ item }}
            loop: '{{ result_authselect_features.stdout_lines }}'
            register: result_pam_authselect_restore_features
            when:
            - result_authselect_profile is not skipped
            - result_authselect_features is not skipped
            - result_pam_authselect_select_profile is not skipped

          - name: Configure the root Account for Failed Password Attempts - Ensure authselect
              changes are applied
            ansible.builtin.command:
              cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - result_pam_authselect_restore_features is not skipped

          - name: Configure the root Account for Failed Password Attempts - Change the
              PAM file to be edited according to the custom authselect profile
            ansible.builtin.set_fact:
              pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
                | basename }}
          when:
          - result_authselect_present.stat.exists

        - name: Configure the root Account for Failed Password Attempts - Ensure the "even_deny_root"
            option from "pam_faillock.so" is not present in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: (.*auth.*pam_faillock.so.*)\beven_deny_root\b=?[0-9a-zA-Z]*(.*)
            replace: \1\2
          register: result_pam_option_removal

        - name: Configure the root Account for Failed Password Attempts - Ensure authselect
            changes are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b
          when:
          - result_authselect_present.stat.exists
          - result_pam_option_removal is changed
        when:
        - result_pam_file_present.stat.exists
      when:
      - '"pam" in ansible_facts.packages'
      - result_faillock_conf_check.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020023
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(c)
      - accounts_passwords_pam_faillock_deny_root
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Configure the root Account for Failed Password Attempts - Ensure the pam_faillock.so
        even_deny_root parameter in PAM files
      block:

      - name: Configure the root Account for Failed Password Attempts - Check if pam_faillock.so
          even_deny_root parameter is already enabled in pam files
        ansible.builtin.lineinfile:
          path: /etc/pam.d/system-auth
          regexp: .*auth.*pam_faillock\.so (preauth|authfail).*even_deny_root
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_faillock_even_deny_root_parameter_is_present

      - name: Configure the root Account for Failed Password Attempts - Ensure the inclusion
          of pam_faillock.so preauth even_deny_root parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*)
          line: \1required\3 even_deny_root
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_even_deny_root_parameter_is_present.found == 0

      - name: Configure the root Account for Failed Password Attempts - Ensure the inclusion
          of pam_faillock.so authfail even_deny_root parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)
          line: \1required\3 even_deny_root
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_even_deny_root_parameter_is_present.found == 0
      when:
      - '"pam" in ansible_facts.packages'
      - not result_faillock_conf_check.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020023
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(c)
      - accounts_passwords_pam_faillock_deny_root
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Set Interval For Counting Failed Password Attempts [ref]

Utilizing `pam_faillock.so`, the `fail_interval` directive configures the system to lock out an account after a number of incorrect login attempts within a specified time period. Ensure that the file `/etc/security/faillock.conf` contains the following entry: `fail_interval = <interval-in-seconds>` where `interval-in-seconds` is `900` or greater. In order to avoid errors when manually editing these files, it is recommended to use the appropriate tools, such as `authselect` or `authconfig`, depending on the OS version.
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. If the system supports the `/etc/security/faillock.conf` file, the pam_faillock parameters should be defined in `faillock.conf` file.

Rationale: | By limiting the number of failed logon attempts the risk of unauthorized system access via user password guessing, otherwise known as brute-forcing, is reduced. Limits are imposed by locking the account.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_interval

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, DSS05.04, DSS05.10, DSS06.10, CCI-000044, CCI-002236, CCI-002237, CCI-002238, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, CM-6(a), AC-7(a), PR.AC-7, FIA_AFL.1, SRG-OS-000329-GPOS-00128, SRG-OS-000021-GPOS-00005, SV-230334r627750_rule, SV-230335r743969_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    var_accounts_passwords_pam_faillock_fail_interval='900'

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

    authselect apply-changes -b
    else

    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")
    for pam_file in "${AUTH_FILES[@]}"
    do
        if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then
            sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth        required      pam_faillock.so preauth silent' "$pam_file"
            sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth        required      pam_faillock.so authfail' "$pam_file"
            sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account     required      pam_faillock.so' "$pam_file"
        fi
        sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required     \3/g' "$pam_file"
    done

    fi

    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")

    FAILLOCK_CONF="/etc/security/faillock.conf"
    if [ -f $FAILLOCK_CONF ]; then
        regex="^\s*fail_interval\s*="
        line="fail_interval = $var_accounts_passwords_pam_faillock_fail_interval"
        if ! grep -q $regex $FAILLOCK_CONF; then
            echo $line >> $FAILLOCK_CONF
        else
            sed -i --follow-symlinks 's|^\s*\(fail_interval\s*=\s*\)\(\S\+\)|\1'"$var_accounts_passwords_pam_faillock_fail_interval"'|g' $FAILLOCK_CONF
        fi
        for pam_file in "${AUTH_FILES[@]}"
        do
            if [ -e "$pam_file" ] ; then
                PAM_FILE_PATH="$pam_file"
                if [ -f /usr/bin/authselect ]; then

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

                    CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
                    # If not already in use, a custom profile is created preserving the enabled features.
                    if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                        ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                        authselect create-profile hardening -b $CURRENT_PROFILE
                        CURRENT_PROFILE="custom/hardening"

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

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

                    authselect apply-changes -b
                fi

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

                    authselect apply-changes -b
                fi
            else
                echo "$pam_file was not found" >&2
            fi
        done
    else
        for pam_file in "${AUTH_FILES[@]}"
        do
            if ! grep -qE '^\s*auth.*pam_faillock\.so (preauth|authfail).*fail_interval' "$pam_file"; then
                sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*silent.*/ s/$/ fail_interval='"$var_accounts_passwords_pam_faillock_fail_interval"'/' "$pam_file"
                sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*authfail.*/ s/$/ fail_interval='"$var_accounts_passwords_pam_faillock_fail_interval"'/' "$pam_file"
            else
                sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*preauth.*silent.*\)\('"fail_interval"'=\)[0-9]\+\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_fail_interval"'\3/' "$pam_file"
                sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*authfail.*\)\('"fail_interval"'=\)[0-9]\+\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_fail_interval"'\3/' "$pam_file"
            fi
        done
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-020012
      - DISA-STIG-RHEL-08-020013
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - accounts_passwords_pam_faillock_interval
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Interval For Counting Failed Password Attempts - Check if system relies
        on authselect tool
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present
      when: '"pam" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-08-020012
      - DISA-STIG-RHEL-08-020013
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - accounts_passwords_pam_faillock_interval
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Interval For Counting Failed Password Attempts - Remediation where authselect
        tool is present
      block:

      - name: Set Interval For Counting Failed Password Attempts - Check integrity of
          authselect current profile
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        failed_when: false

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

      - name: Set Interval For Counting Failed Password Attempts - Get authselect current
          features
        ansible.builtin.shell:
          cmd: authselect current | tail -n+3 | awk '{ print $2 }'
        register: result_authselect_features
        changed_when: false
        when:
        - result_authselect_check_cmd is success

      - name: Set Interval For Counting Failed Password Attempts - Ensure "with-faillock"
          feature is enabled using authselect tool
        ansible.builtin.command:
          cmd: authselect enable-feature with-faillock
        register: result_authselect_enable_feature_cmd
        when:
        - result_authselect_check_cmd is success
        - result_authselect_features.stdout is not search("with-faillock")

      - name: Set Interval For Counting Failed Password Attempts - Ensure authselect changes
          are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_enable_feature_cmd is not skipped
        - result_authselect_enable_feature_cmd is success
      when:
      - '"pam" in ansible_facts.packages'
      - result_authselect_present.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020012
      - DISA-STIG-RHEL-08-020013
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - accounts_passwords_pam_faillock_interval
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Interval For Counting Failed Password Attempts - Remediation where authselect
        tool is not present
      block:

      - name: Set Interval For Counting Failed Password Attempts - Check if pam_faillock.so
          is already enabled
        ansible.builtin.lineinfile:
          path: /etc/pam.d/system-auth
          regexp: .*auth.*pam_faillock\.so (preauth|authfail)
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_faillock_is_enabled

      - name: Set Interval For Counting Failed Password Attempts - Enable pam_faillock.so
          preauth editing PAM files
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          line: auth        required      pam_faillock.so preauth
          insertbefore: ^auth.*sufficient.*pam_unix\.so.*
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_is_enabled.found == 0

      - name: Set Interval For Counting Failed Password Attempts - Enable pam_faillock.so
          authfail editing PAM files
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          line: auth        required      pam_faillock.so authfail
          insertbefore: ^auth.*required.*pam_deny\.so.*
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_is_enabled.found == 0

      - name: Set Interval For Counting Failed Password Attempts - Enable pam_faillock.so
          account section editing PAM files
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          line: account     required      pam_faillock.so
          insertbefore: ^account.*required.*pam_unix\.so.*
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_is_enabled.found == 0
      when:
      - '"pam" in ansible_facts.packages'
      - not result_authselect_present.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020012
      - DISA-STIG-RHEL-08-020013
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - accounts_passwords_pam_faillock_interval
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_passwords_pam_faillock_fail_interval # promote to variable
      set_fact:
        var_accounts_passwords_pam_faillock_fail_interval: !!str 900
      tags:
        - always

    - name: Set Interval For Counting Failed Password Attempts - Check the presence of
        /etc/security/faillock.conf file
      ansible.builtin.stat:
        path: /etc/security/faillock.conf
      register: result_faillock_conf_check
      when: '"pam" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-08-020012
      - DISA-STIG-RHEL-08-020013
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - accounts_passwords_pam_faillock_interval
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Interval For Counting Failed Password Attempts - Ensure the pam_faillock.so
        fail_interval parameter in /etc/security/faillock.conf
      ansible.builtin.lineinfile:
        path: /etc/security/faillock.conf
        regexp: ^\s*fail_interval\s*=
        line: fail_interval = {{ var_accounts_passwords_pam_faillock_fail_interval }}
        state: present
      when:
      - '"pam" in ansible_facts.packages'
      - result_faillock_conf_check.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020012
      - DISA-STIG-RHEL-08-020013
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - accounts_passwords_pam_faillock_interval
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Interval For Counting Failed Password Attempts - Ensure the pam_faillock.so
        fail_interval parameter not in PAM files
      block:

      - name: Set Interval For Counting Failed Password Attempts - Check if /etc/pam.d/system-auth
          file is present
        ansible.builtin.stat:
          path: /etc/pam.d/system-auth
        register: result_pam_file_present

      - name: Set Interval For Counting Failed Password Attempts - Check the proper remediation
          for the system
        block:

        - name: Set Interval For Counting Failed Password Attempts - Define the PAM file
            to be edited as a local fact
          ansible.builtin.set_fact:
            pam_file_path: /etc/pam.d/system-auth

        - name: Set Interval For Counting Failed Password Attempts - Check if system relies
            on authselect tool
          ansible.builtin.stat:
            path: /usr/bin/authselect
          register: result_authselect_present

        - name: Set Interval For Counting Failed Password Attempts - Ensure authselect
            custom profile is used if authselect is present
          block:

          - name: Set Interval For Counting Failed Password Attempts - Check integrity
              of authselect current profile
            ansible.builtin.command:
              cmd: authselect check
            register: result_authselect_check_cmd
            changed_when: false
            failed_when: false

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

          - name: Set Interval For Counting Failed Password Attempts - Get authselect
              current profile
            ansible.builtin.shell:
              cmd: authselect current -r | awk '{ print $1 }'
            register: result_authselect_profile
            changed_when: false
            when:
            - result_authselect_check_cmd is success

          - name: Set Interval For Counting Failed Password Attempts - Define the current
              authselect profile as a local fact
            ansible.builtin.set_fact:
              authselect_current_profile: '{{ result_authselect_profile.stdout }}'
              authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
            when:
            - result_authselect_profile is not skipped
            - result_authselect_profile.stdout is match("custom/")

          - name: Set Interval For Counting Failed Password Attempts - Define the new
              authselect custom profile as a local fact
            ansible.builtin.set_fact:
              authselect_current_profile: '{{ result_authselect_profile.stdout }}'
              authselect_custom_profile: custom/hardening
            when:
            - result_authselect_profile is not skipped
            - result_authselect_profile.stdout is not match("custom/")

          - name: Set Interval For Counting Failed Password Attempts - Get authselect
              current features to also enable them in the custom profile
            ansible.builtin.shell:
              cmd: authselect current | tail -n+3 | awk '{ print $2 }'
            register: result_authselect_features
            changed_when: false
            when:
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")

          - name: Set Interval For Counting Failed Password Attempts - Check if any custom
              profile with the same name was already created
            ansible.builtin.stat:
              path: /etc/authselect/{{ authselect_custom_profile }}
            register: result_authselect_custom_profile_present
            changed_when: false
            when:
            - authselect_current_profile is not match("custom/")

          - name: Set Interval For Counting Failed Password Attempts - Create an authselect
              custom profile based on the current profile
            ansible.builtin.command:
              cmd: authselect create-profile hardening -b {{ authselect_current_profile
                }}
            when:
            - result_authselect_check_cmd is success
            - authselect_current_profile is not match("custom/")
            - not result_authselect_custom_profile_present.stat.exists

          - name: Set Interval For Counting Failed Password Attempts - Ensure authselect
              changes are applied
            ansible.builtin.command:
              cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")
            - authselect_custom_profile is not match(authselect_current_profile)

          - name: Set Interval For Counting Failed Password Attempts - Ensure the authselect
              custom profile is selected
            ansible.builtin.command:
              cmd: authselect select {{ authselect_custom_profile }}
            register: result_pam_authselect_select_profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")
            - authselect_custom_profile is not match(authselect_current_profile)

          - name: Set Interval For Counting Failed Password Attempts - Restore the authselect
              features in the custom profile
            ansible.builtin.command:
              cmd: authselect enable-feature {{ item }}
            loop: '{{ result_authselect_features.stdout_lines }}'
            register: result_pam_authselect_restore_features
            when:
            - result_authselect_profile is not skipped
            - result_authselect_features is not skipped
            - result_pam_authselect_select_profile is not skipped

          - name: Set Interval For Counting Failed Password Attempts - Ensure authselect
              changes are applied
            ansible.builtin.command:
              cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - result_pam_authselect_restore_features is not skipped

          - name: Set Interval For Counting Failed Password Attempts - Change the PAM
              file to be edited according to the custom authselect profile
            ansible.builtin.set_fact:
              pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
                | basename }}
          when:
          - result_authselect_present.stat.exists

        - name: Set Interval For Counting Failed Password Attempts - Ensure the "fail_interval"
            option from "pam_faillock.so" is not present in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: (.*auth.*pam_faillock.so.*)\bfail_interval\b=?[0-9a-zA-Z]*(.*)
            replace: \1\2
          register: result_pam_option_removal

        - name: Set Interval For Counting Failed Password Attempts - Ensure authselect
            changes are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b
          when:
          - result_authselect_present.stat.exists
          - result_pam_option_removal is changed
        when:
        - result_pam_file_present.stat.exists

      - name: Set Interval For Counting Failed Password Attempts - Check if /etc/pam.d/password-auth
          file is present
        ansible.builtin.stat:
          path: /etc/pam.d/password-auth
        register: result_pam_file_present

      - name: Set Interval For Counting Failed Password Attempts - Check the proper remediation
          for the system
        block:

        - name: Set Interval For Counting Failed Password Attempts - Define the PAM file
            to be edited as a local fact
          ansible.builtin.set_fact:
            pam_file_path: /etc/pam.d/password-auth

        - name: Set Interval For Counting Failed Password Attempts - Check if system relies
            on authselect tool
          ansible.builtin.stat:
            path: /usr/bin/authselect
          register: result_authselect_present

        - name: Set Interval For Counting Failed Password Attempts - Ensure authselect
            custom profile is used if authselect is present
          block:

          - name: Set Interval For Counting Failed Password Attempts - Check integrity
              of authselect current profile
            ansible.builtin.command:
              cmd: authselect check
            register: result_authselect_check_cmd
            changed_when: false
            failed_when: false

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

          - name: Set Interval For Counting Failed Password Attempts - Get authselect
              current profile
            ansible.builtin.shell:
              cmd: authselect current -r | awk '{ print $1 }'
            register: result_authselect_profile
            changed_when: false
            when:
            - result_authselect_check_cmd is success

          - name: Set Interval For Counting Failed Password Attempts - Define the current
              authselect profile as a local fact
            ansible.builtin.set_fact:
              authselect_current_profile: '{{ result_authselect_profile.stdout }}'
              authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
            when:
            - result_authselect_profile is not skipped
            - result_authselect_profile.stdout is match("custom/")

          - name: Set Interval For Counting Failed Password Attempts - Define the new
              authselect custom profile as a local fact
            ansible.builtin.set_fact:
              authselect_current_profile: '{{ result_authselect_profile.stdout }}'
              authselect_custom_profile: custom/hardening
            when:
            - result_authselect_profile is not skipped
            - result_authselect_profile.stdout is not match("custom/")

          - name: Set Interval For Counting Failed Password Attempts - Get authselect
              current features to also enable them in the custom profile
            ansible.builtin.shell:
              cmd: authselect current | tail -n+3 | awk '{ print $2 }'
            register: result_authselect_features
            changed_when: false
            when:
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")

          - name: Set Interval For Counting Failed Password Attempts - Check if any custom
              profile with the same name was already created
            ansible.builtin.stat:
              path: /etc/authselect/{{ authselect_custom_profile }}
            register: result_authselect_custom_profile_present
            changed_when: false
            when:
            - authselect_current_profile is not match("custom/")

          - name: Set Interval For Counting Failed Password Attempts - Create an authselect
              custom profile based on the current profile
            ansible.builtin.command:
              cmd: authselect create-profile hardening -b {{ authselect_current_profile
                }}
            when:
            - result_authselect_check_cmd is success
            - authselect_current_profile is not match("custom/")
            - not result_authselect_custom_profile_present.stat.exists

          - name: Set Interval For Counting Failed Password Attempts - Ensure authselect
              changes are applied
            ansible.builtin.command:
              cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")
            - authselect_custom_profile is not match(authselect_current_profile)

          - name: Set Interval For Counting Failed Password Attempts - Ensure the authselect
              custom profile is selected
            ansible.builtin.command:
              cmd: authselect select {{ authselect_custom_profile }}
            register: result_pam_authselect_select_profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")
            - authselect_custom_profile is not match(authselect_current_profile)

          - name: Set Interval For Counting Failed Password Attempts - Restore the authselect
              features in the custom profile
            ansible.builtin.command:
              cmd: authselect enable-feature {{ item }}
            loop: '{{ result_authselect_features.stdout_lines }}'
            register: result_pam_authselect_restore_features
            when:
            - result_authselect_profile is not skipped
            - result_authselect_features is not skipped
            - result_pam_authselect_select_profile is not skipped

          - name: Set Interval For Counting Failed Password Attempts - Ensure authselect
              changes are applied
            ansible.builtin.command:
              cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - result_pam_authselect_restore_features is not skipped

          - name: Set Interval For Counting Failed Password Attempts - Change the PAM
              file to be edited according to the custom authselect profile
            ansible.builtin.set_fact:
              pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
                | basename }}
          when:
          - result_authselect_present.stat.exists

        - name: Set Interval For Counting Failed Password Attempts - Ensure the "fail_interval"
            option from "pam_faillock.so" is not present in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: (.*auth.*pam_faillock.so.*)\bfail_interval\b=?[0-9a-zA-Z]*(.*)
            replace: \1\2
          register: result_pam_option_removal

        - name: Set Interval For Counting Failed Password Attempts - Ensure authselect
            changes are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b
          when:
          - result_authselect_present.stat.exists
          - result_pam_option_removal is changed
        when:
        - result_pam_file_present.stat.exists
      when:
      - '"pam" in ansible_facts.packages'
      - result_faillock_conf_check.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020012
      - DISA-STIG-RHEL-08-020013
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - accounts_passwords_pam_faillock_interval
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Interval For Counting Failed Password Attempts - Ensure the pam_faillock.so
        fail_interval parameter in PAM files
      block:

      - name: Set Interval For Counting Failed Password Attempts - Check if pam_faillock.so
          fail_interval parameter is already enabled in pam files
        ansible.builtin.lineinfile:
          path: /etc/pam.d/system-auth
          regexp: .*auth.*pam_faillock\.so (preauth|authfail).*fail_interval
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_faillock_fail_interval_parameter_is_present

      - name: Set Interval For Counting Failed Password Attempts - Ensure the inclusion
          of pam_faillock.so preauth fail_interval parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*)
          line: \1required\3 fail_interval={{ var_accounts_passwords_pam_faillock_fail_interval
            }}
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_fail_interval_parameter_is_present.found == 0

      - name: Set Interval For Counting Failed Password Attempts - Ensure the inclusion
          of pam_faillock.so authfail fail_interval parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)
          line: \1required\3 fail_interval={{ var_accounts_passwords_pam_faillock_fail_interval
            }}
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_fail_interval_parameter_is_present.found == 0

      - name: Set Interval For Counting Failed Password Attempts - Ensure the desired
          value for pam_faillock.so preauth fail_interval parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*)(fail_interval)=[0-9]+(.*)
          line: \1required\3\4={{ var_accounts_passwords_pam_faillock_fail_interval }}\5
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_fail_interval_parameter_is_present.found > 0

      - name: Set Interval For Counting Failed Password Attempts - Ensure the desired
          value for pam_faillock.so authfail fail_interval parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)(fail_interval)=[0-9]+(.*)
          line: \1required\3\4={{ var_accounts_passwords_pam_faillock_fail_interval }}\5
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_fail_interval_parameter_is_present.found > 0
      when:
      - '"pam" in ansible_facts.packages'
      - not result_faillock_conf_check.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020012
      - DISA-STIG-RHEL-08-020013
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - accounts_passwords_pam_faillock_interval
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Set Lockout Time for Failed Password Attempts [ref]

This rule configures the system to lock out accounts during a specified time period after a number of incorrect login attempts using `pam_faillock.so`. Ensure that the file `/etc/security/faillock.conf` contains the following entry: `unlock_time=<interval-in-seconds>` where `interval-in-seconds` is `0` or greater. pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. In order to avoid any errors when manually editing these files, it is recommended to use the appropriate tools, such as `authselect` or `authconfig`, depending on the OS version. If `unlock_time` is set to `0`, manual intervention by an administrator is required to unlock a user. This should be done using the `faillock` tool.
Warning: If the system supports the new `/etc/security/faillock.conf` file but the pam_faillock.so parameters are defined directly in `/etc/pam.d/system-auth` and `/etc/pam.d/password-auth`, the remediation will migrate the `unlock_time` parameter to `/etc/security/faillock.conf` to ensure compatibility with `authselect` tool. The parameters `deny` and `fail_interval`, if used, also have to be migrated by their respective remediation.
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. If the system supports the `/etc/security/faillock.conf` file, the pam_faillock parameters should be defined in `faillock.conf` file.

Rationale: | By limiting the number of failed logon attempts the risk of unauthorized system access via user password guessing, otherwise known as brute-forcing, is reduced. Limits are imposed by locking the account.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_unlock_time

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, 5.5.3, DSS05.04, DSS05.10, DSS06.10, 3.1.8, CCI-000044, CCI-002236, CCI-002237, CCI-002238, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, CM-6(a), AC-7(b), PR.AC-7, FIA_AFL.1, Req-8.1.7, 8.3.4, SRG-OS-000329-GPOS-00128, SRG-OS-000021-GPOS-00005, 5.5.2, SV-230336r627750_rule, SV-230337r743972_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    var_accounts_passwords_pam_faillock_unlock_time='0'

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

    authselect apply-changes -b
    else

    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")
    for pam_file in "${AUTH_FILES[@]}"
    do
        if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then
            sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth        required      pam_faillock.so preauth silent' "$pam_file"
            sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth        required      pam_faillock.so authfail' "$pam_file"
            sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account     required      pam_faillock.so' "$pam_file"
        fi
        sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required     \3/g' "$pam_file"
    done

    fi

    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")

    FAILLOCK_CONF="/etc/security/faillock.conf"
    if [ -f $FAILLOCK_CONF ]; then
        regex="^\s*unlock_time\s*="
        line="unlock_time = $var_accounts_passwords_pam_faillock_unlock_time"
        if ! grep -q $regex $FAILLOCK_CONF; then
            echo $line >> $FAILLOCK_CONF
        else
            sed -i --follow-symlinks 's|^\s*\(unlock_time\s*=\s*\)\(\S\+\)|\1'"$var_accounts_passwords_pam_faillock_unlock_time"'|g' $FAILLOCK_CONF
        fi
        for pam_file in "${AUTH_FILES[@]}"
        do
            if [ -e "$pam_file" ] ; then
                PAM_FILE_PATH="$pam_file"
                if [ -f /usr/bin/authselect ]; then

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

                    CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
                    # If not already in use, a custom profile is created preserving the enabled features.
                    if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                        ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                        authselect create-profile hardening -b $CURRENT_PROFILE
                        CURRENT_PROFILE="custom/hardening"

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

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

                    authselect apply-changes -b
                fi

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

                    authselect apply-changes -b
                fi
            else
                echo "$pam_file was not found" >&2
            fi
        done
    else
        for pam_file in "${AUTH_FILES[@]}"
        do
            if ! grep -qE '^\s*auth.*pam_faillock\.so (preauth|authfail).*unlock_time' "$pam_file"; then
                sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*silent.*/ s/$/ unlock_time='"$var_accounts_passwords_pam_faillock_unlock_time"'/' "$pam_file"
                sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*authfail.*/ s/$/ unlock_time='"$var_accounts_passwords_pam_faillock_unlock_time"'/' "$pam_file"
            else
                sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*preauth.*silent.*\)\('"unlock_time"'=\)[0-9]\+\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_unlock_time"'\3/' "$pam_file"
                sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*authfail.*\)\('"unlock_time"'=\)[0-9]\+\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_unlock_time"'\3/' "$pam_file"
            fi
        done
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020014
      - DISA-STIG-RHEL-08-020015
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.7
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_unlock_time
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Lockout Time for Failed Password Attempts - Check if system relies on
        authselect tool
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present
      when: '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020014
      - DISA-STIG-RHEL-08-020015
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.7
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_unlock_time
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Lockout Time for Failed Password Attempts - Remediation where authselect
        tool is present
      block:

      - name: Set Lockout Time for Failed Password Attempts - Check integrity of authselect
          current profile
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        failed_when: false

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

      - name: Set Lockout Time for Failed Password Attempts - Get authselect current features
        ansible.builtin.shell:
          cmd: authselect current | tail -n+3 | awk '{ print $2 }'
        register: result_authselect_features
        changed_when: false
        when:
        - result_authselect_check_cmd is success

      - name: Set Lockout Time for Failed Password Attempts - Ensure "with-faillock" feature
          is enabled using authselect tool
        ansible.builtin.command:
          cmd: authselect enable-feature with-faillock
        register: result_authselect_enable_feature_cmd
        when:
        - result_authselect_check_cmd is success
        - result_authselect_features.stdout is not search("with-faillock")

      - name: Set Lockout Time for Failed Password Attempts - Ensure authselect changes
          are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_enable_feature_cmd is not skipped
        - result_authselect_enable_feature_cmd is success
      when:
      - '"pam" in ansible_facts.packages'
      - result_authselect_present.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020014
      - DISA-STIG-RHEL-08-020015
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.7
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_unlock_time
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Lockout Time for Failed Password Attempts - Remediation where authselect
        tool is not present
      block:

      - name: Set Lockout Time for Failed Password Attempts - Check if pam_faillock.so
          is already enabled
        ansible.builtin.lineinfile:
          path: /etc/pam.d/system-auth
          regexp: .*auth.*pam_faillock\.so (preauth|authfail)
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_faillock_is_enabled

      - name: Set Lockout Time for Failed Password Attempts - Enable pam_faillock.so preauth
          editing PAM files
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          line: auth        required      pam_faillock.so preauth
          insertbefore: ^auth.*sufficient.*pam_unix\.so.*
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_is_enabled.found == 0

      - name: Set Lockout Time for Failed Password Attempts - Enable pam_faillock.so authfail
          editing PAM files
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          line: auth        required      pam_faillock.so authfail
          insertbefore: ^auth.*required.*pam_deny\.so.*
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_is_enabled.found == 0

      - name: Set Lockout Time for Failed Password Attempts - Enable pam_faillock.so account
          section editing PAM files
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          line: account     required      pam_faillock.so
          insertbefore: ^account.*required.*pam_unix\.so.*
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_is_enabled.found == 0
      when:
      - '"pam" in ansible_facts.packages'
      - not result_authselect_present.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020014
      - DISA-STIG-RHEL-08-020015
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.7
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_unlock_time
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_passwords_pam_faillock_unlock_time # promote to variable
      set_fact:
        var_accounts_passwords_pam_faillock_unlock_time: !!str 0
      tags:
        - always

    - name: Set Lockout Time for Failed Password Attempts - Check the presence of /etc/security/faillock.conf
        file
      ansible.builtin.stat:
        path: /etc/security/faillock.conf
      register: result_faillock_conf_check
      when: '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020014
      - DISA-STIG-RHEL-08-020015
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.7
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_unlock_time
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Lockout Time for Failed Password Attempts - Ensure the pam_faillock.so
        unlock_time parameter in /etc/security/faillock.conf
      ansible.builtin.lineinfile:
        path: /etc/security/faillock.conf
        regexp: ^\s*unlock_time\s*=
        line: unlock_time = {{ var_accounts_passwords_pam_faillock_unlock_time }}
        state: present
      when:
      - '"pam" in ansible_facts.packages'
      - result_faillock_conf_check.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020014
      - DISA-STIG-RHEL-08-020015
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.7
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_unlock_time
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Lockout Time for Failed Password Attempts - Ensure the pam_faillock.so
        unlock_time parameter not in PAM files
      block:

      - name: Set Lockout Time for Failed Password Attempts - Check if /etc/pam.d/system-auth
          file is present
        ansible.builtin.stat:
          path: /etc/pam.d/system-auth
        register: result_pam_file_present

      - name: Set Lockout Time for Failed Password Attempts - Check the proper remediation
          for the system
        block:

        - name: Set Lockout Time for Failed Password Attempts - Define the PAM file to
            be edited as a local fact
          ansible.builtin.set_fact:
            pam_file_path: /etc/pam.d/system-auth

        - name: Set Lockout Time for Failed Password Attempts - Check if system relies
            on authselect tool
          ansible.builtin.stat:
            path: /usr/bin/authselect
          register: result_authselect_present

        - name: Set Lockout Time for Failed Password Attempts - Ensure authselect custom
            profile is used if authselect is present
          block:

          - name: Set Lockout Time for Failed Password Attempts - Check integrity of authselect
              current profile
            ansible.builtin.command:
              cmd: authselect check
            register: result_authselect_check_cmd
            changed_when: false
            failed_when: false

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

          - name: Set Lockout Time for Failed Password Attempts - Get authselect current
              profile
            ansible.builtin.shell:
              cmd: authselect current -r | awk '{ print $1 }'
            register: result_authselect_profile
            changed_when: false
            when:
            - result_authselect_check_cmd is success

          - name: Set Lockout Time for Failed Password Attempts - Define the current authselect
              profile as a local fact
            ansible.builtin.set_fact:
              authselect_current_profile: '{{ result_authselect_profile.stdout }}'
              authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
            when:
            - result_authselect_profile is not skipped
            - result_authselect_profile.stdout is match("custom/")

          - name: Set Lockout Time for Failed Password Attempts - Define the new authselect
              custom profile as a local fact
            ansible.builtin.set_fact:
              authselect_current_profile: '{{ result_authselect_profile.stdout }}'
              authselect_custom_profile: custom/hardening
            when:
            - result_authselect_profile is not skipped
            - result_authselect_profile.stdout is not match("custom/")

          - name: Set Lockout Time for Failed Password Attempts - Get authselect current
              features to also enable them in the custom profile
            ansible.builtin.shell:
              cmd: authselect current | tail -n+3 | awk '{ print $2 }'
            register: result_authselect_features
            changed_when: false
            when:
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")

          - name: Set Lockout Time for Failed Password Attempts - Check if any custom
              profile with the same name was already created
            ansible.builtin.stat:
              path: /etc/authselect/{{ authselect_custom_profile }}
            register: result_authselect_custom_profile_present
            changed_when: false
            when:
            - authselect_current_profile is not match("custom/")

          - name: Set Lockout Time for Failed Password Attempts - Create an authselect
              custom profile based on the current profile
            ansible.builtin.command:
              cmd: authselect create-profile hardening -b {{ authselect_current_profile
                }}
            when:
            - result_authselect_check_cmd is success
            - authselect_current_profile is not match("custom/")
            - not result_authselect_custom_profile_present.stat.exists

          - name: Set Lockout Time for Failed Password Attempts - Ensure authselect changes
              are applied
            ansible.builtin.command:
              cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")
            - authselect_custom_profile is not match(authselect_current_profile)

          - name: Set Lockout Time for Failed Password Attempts - Ensure the authselect
              custom profile is selected
            ansible.builtin.command:
              cmd: authselect select {{ authselect_custom_profile }}
            register: result_pam_authselect_select_profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")
            - authselect_custom_profile is not match(authselect_current_profile)

          - name: Set Lockout Time for Failed Password Attempts - Restore the authselect
              features in the custom profile
            ansible.builtin.command:
              cmd: authselect enable-feature {{ item }}
            loop: '{{ result_authselect_features.stdout_lines }}'
            register: result_pam_authselect_restore_features
            when:
            - result_authselect_profile is not skipped
            - result_authselect_features is not skipped
            - result_pam_authselect_select_profile is not skipped

          - name: Set Lockout Time for Failed Password Attempts - Ensure authselect changes
              are applied
            ansible.builtin.command:
              cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - result_pam_authselect_restore_features is not skipped

          - name: Set Lockout Time for Failed Password Attempts - Change the PAM file
              to be edited according to the custom authselect profile
            ansible.builtin.set_fact:
              pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
                | basename }}
          when:
          - result_authselect_present.stat.exists

        - name: Set Lockout Time for Failed Password Attempts - Ensure the "unlock_time"
            option from "pam_faillock.so" is not present in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: (.*auth.*pam_faillock.so.*)\bunlock_time\b=?[0-9a-zA-Z]*(.*)
            replace: \1\2
          register: result_pam_option_removal

        - name: Set Lockout Time for Failed Password Attempts - Ensure authselect changes
            are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b
          when:
          - result_authselect_present.stat.exists
          - result_pam_option_removal is changed
        when:
        - result_pam_file_present.stat.exists

      - name: Set Lockout Time for Failed Password Attempts - Check if /etc/pam.d/password-auth
          file is present
        ansible.builtin.stat:
          path: /etc/pam.d/password-auth
        register: result_pam_file_present

      - name: Set Lockout Time for Failed Password Attempts - Check the proper remediation
          for the system
        block:

        - name: Set Lockout Time for Failed Password Attempts - Define the PAM file to
            be edited as a local fact
          ansible.builtin.set_fact:
            pam_file_path: /etc/pam.d/password-auth

        - name: Set Lockout Time for Failed Password Attempts - Check if system relies
            on authselect tool
          ansible.builtin.stat:
            path: /usr/bin/authselect
          register: result_authselect_present

        - name: Set Lockout Time for Failed Password Attempts - Ensure authselect custom
            profile is used if authselect is present
          block:

          - name: Set Lockout Time for Failed Password Attempts - Check integrity of authselect
              current profile
            ansible.builtin.command:
              cmd: authselect check
            register: result_authselect_check_cmd
            changed_when: false
            failed_when: false

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

          - name: Set Lockout Time for Failed Password Attempts - Get authselect current
              profile
            ansible.builtin.shell:
              cmd: authselect current -r | awk '{ print $1 }'
            register: result_authselect_profile
            changed_when: false
            when:
            - result_authselect_check_cmd is success

          - name: Set Lockout Time for Failed Password Attempts - Define the current authselect
              profile as a local fact
            ansible.builtin.set_fact:
              authselect_current_profile: '{{ result_authselect_profile.stdout }}'
              authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
            when:
            - result_authselect_profile is not skipped
            - result_authselect_profile.stdout is match("custom/")

          - name: Set Lockout Time for Failed Password Attempts - Define the new authselect
              custom profile as a local fact
            ansible.builtin.set_fact:
              authselect_current_profile: '{{ result_authselect_profile.stdout }}'
              authselect_custom_profile: custom/hardening
            when:
            - result_authselect_profile is not skipped
            - result_authselect_profile.stdout is not match("custom/")

          - name: Set Lockout Time for Failed Password Attempts - Get authselect current
              features to also enable them in the custom profile
            ansible.builtin.shell:
              cmd: authselect current | tail -n+3 | awk '{ print $2 }'
            register: result_authselect_features
            changed_when: false
            when:
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")

          - name: Set Lockout Time for Failed Password Attempts - Check if any custom
              profile with the same name was already created
            ansible.builtin.stat:
              path: /etc/authselect/{{ authselect_custom_profile }}
            register: result_authselect_custom_profile_present
            changed_when: false
            when:
            - authselect_current_profile is not match("custom/")

          - name: Set Lockout Time for Failed Password Attempts - Create an authselect
              custom profile based on the current profile
            ansible.builtin.command:
              cmd: authselect create-profile hardening -b {{ authselect_current_profile
                }}
            when:
            - result_authselect_check_cmd is success
            - authselect_current_profile is not match("custom/")
            - not result_authselect_custom_profile_present.stat.exists

          - name: Set Lockout Time for Failed Password Attempts - Ensure authselect changes
              are applied
            ansible.builtin.command:
              cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")
            - authselect_custom_profile is not match(authselect_current_profile)

          - name: Set Lockout Time for Failed Password Attempts - Ensure the authselect
              custom profile is selected
            ansible.builtin.command:
              cmd: authselect select {{ authselect_custom_profile }}
            register: result_pam_authselect_select_profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - authselect_current_profile is not match("custom/")
            - authselect_custom_profile is not match(authselect_current_profile)

          - name: Set Lockout Time for Failed Password Attempts - Restore the authselect
              features in the custom profile
            ansible.builtin.command:
              cmd: authselect enable-feature {{ item }}
            loop: '{{ result_authselect_features.stdout_lines }}'
            register: result_pam_authselect_restore_features
            when:
            - result_authselect_profile is not skipped
            - result_authselect_features is not skipped
            - result_pam_authselect_select_profile is not skipped

          - name: Set Lockout Time for Failed Password Attempts - Ensure authselect changes
              are applied
            ansible.builtin.command:
              cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
            when:
            - result_authselect_check_cmd is success
            - result_authselect_profile is not skipped
            - result_pam_authselect_restore_features is not skipped

          - name: Set Lockout Time for Failed Password Attempts - Change the PAM file
              to be edited according to the custom authselect profile
            ansible.builtin.set_fact:
              pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
                | basename }}
          when:
          - result_authselect_present.stat.exists

        - name: Set Lockout Time for Failed Password Attempts - Ensure the "unlock_time"
            option from "pam_faillock.so" is not present in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: (.*auth.*pam_faillock.so.*)\bunlock_time\b=?[0-9a-zA-Z]*(.*)
            replace: \1\2
          register: result_pam_option_removal

        - name: Set Lockout Time for Failed Password Attempts - Ensure authselect changes
            are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b
          when:
          - result_authselect_present.stat.exists
          - result_pam_option_removal is changed
        when:
        - result_pam_file_present.stat.exists
      when:
      - '"pam" in ansible_facts.packages'
      - result_faillock_conf_check.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020014
      - DISA-STIG-RHEL-08-020015
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.7
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_unlock_time
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Lockout Time for Failed Password Attempts - Ensure the pam_faillock.so
        unlock_time parameter in PAM files
      block:

      - name: Set Lockout Time for Failed Password Attempts - Check if pam_faillock.so
          unlock_time parameter is already enabled in pam files
        ansible.builtin.lineinfile:
          path: /etc/pam.d/system-auth
          regexp: .*auth.*pam_faillock\.so (preauth|authfail).*unlock_time
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_faillock_unlock_time_parameter_is_present

      - name: Set Lockout Time for Failed Password Attempts - Ensure the inclusion of
          pam_faillock.so preauth unlock_time parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*)
          line: \1required\3 unlock_time={{ var_accounts_passwords_pam_faillock_unlock_time
            }}
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_unlock_time_parameter_is_present.found == 0

      - name: Set Lockout Time for Failed Password Attempts - Ensure the inclusion of
          pam_faillock.so authfail unlock_time parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)
          line: \1required\3 unlock_time={{ var_accounts_passwords_pam_faillock_unlock_time
            }}
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_unlock_time_parameter_is_present.found == 0

      - name: Set Lockout Time for Failed Password Attempts - Ensure the desired value
          for pam_faillock.so preauth unlock_time parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*)(unlock_time)=[0-9]+(.*)
          line: \1required\3\4={{ var_accounts_passwords_pam_faillock_unlock_time }}\5
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_unlock_time_parameter_is_present.found > 0

      - name: Set Lockout Time for Failed Password Attempts - Ensure the desired value
          for pam_faillock.so authfail unlock_time parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)(unlock_time)=[0-9]+(.*)
          line: \1required\3\4={{ var_accounts_passwords_pam_faillock_unlock_time }}\5
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_unlock_time_parameter_is_present.found > 0
      when:
      - '"pam" in ansible_facts.packages'
      - not result_faillock_conf_check.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020014
      - DISA-STIG-RHEL-08-020015
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.7
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_unlock_time
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Group Set Password Quality Requirements Group contains 1 group and 1 rule

[ref] The default `pam_pwquality` PAM module provides strength checking for passwords. It performs a number of checks, such as making sure passwords are not similar to dictionary words, are of at least a certain length, are not the previous password reversed, and are not simply a change of case from the previous password. It can also require passwords to be in certain character classes. The `pam_pwquality` module is the preferred way of configuring password requirements.
The man pages `pam_pwquality(8)` provide information on the capabilities and configuration of each.

Group Set Password Quality Requirements with pam_pwquality Group contains 1 rule

[ref] The `pam_pwquality` PAM module can be configured to meet requirements for a variety of policies.
For example, to configure `pam_pwquality` to require at least one uppercase character, lowercase character, digit, and other (special) character, make sure that `pam_pwquality` exists in `/etc/pam.d/system-auth`:

    password    requisite     pam_pwquality.so try_first_pass local_users_only retry=3 authtok_type=

If no such line exists, add one as the first line of the password section in `/etc/pam.d/system-auth`. Next, modify the settings in `/etc/security/pwquality.conf` to match the following:

    difok = 4
    minlen = 14
    dcredit = -1
    ucredit = -1
    lcredit = -1
    ocredit = -1
    maxrepeat = 3

The arguments can be modified to ensure compliance with your organization's security policy. Discussion of each parameter follows.

#### Rule Ensure PAM Enforces Password Requirements - Minimum Length [ref]

The pam_pwquality module's `minlen` parameter controls requirements for minimum characters required in a password. Add `minlen=14` after pam_pwquality to set minimum password length requirements.

Rationale:

The shorter the password, the lower the number of possible combinations that need to be tested before the password is compromised.
Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password length is one factor of several that helps to determine strength and how long it takes to crack a password. Use of more characters in a password helps to exponentially increase the time and/or resources required to compromise the password.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_minlen

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, 5, 5.6.2.1.1, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-000205, 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, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, 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(c), IA-5(1)(a), CM-6(a), IA-5(4), PR.AC-1, PR.AC-6, PR.AC-7, FMT_SMF_EXT.1, Req-8.2.3, 8.3.6, SRG-OS-000078-GPOS-00046, 5.5.1, SV-230369r858785_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    var_password_pam_minlen='14'

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

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

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.1.1
      - DISA-STIG-RHEL-08-020230
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_minlen
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_password_pam_minlen # promote to variable
      set_fact:
        var_password_pam_minlen: !!str 14
      tags:
        - always

    - name: Ensure PAM Enforces Password Requirements - Minimum Length - Ensure PAM variable
        minlen is set accordingly
      ansible.builtin.lineinfile:
        create: true
        dest: /etc/security/pwquality.conf
        regexp: ^#?\s*minlen
        line: minlen = {{ var_password_pam_minlen }}
      when: '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1.1
      - DISA-STIG-RHEL-08-020230
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_minlen
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Ensure PAM Displays Last Logon/Access Notification [ref]

To configure the system to notify users of last logon/access using `pam_lastlog`, add or correct the `pam_lastlog` settings in `/etc/pam.d/postlogin` to include `showfailed` option, such as:

    session     [default=1]    pam_lastlog.so showfailed

And make sure that the `silent` option is not set for this specific line.
Warning: If the system relies on `authselect` tool to manage PAM settings, the remediation will also use `authselect` tool. However, if any manual modification was made in PAM files, the `authselect` integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report.
Warning: `authselect` contains an authselect feature to easily and properly enable Last Logon notifications with `pam_lastlog.so` module. If a custom profile was created and used in the system before this authselect feature was available, the new feature can't be used with this custom profile and the remediation will fail. In this case, the custom profile should be recreated or manually updated.

Rationale: | Users need to be aware of activity that occurs regarding their account. Providing users with information regarding the number of unsuccessful attempts that were made to login to their account allows the user to determine if any unauthorized activity has occurred and gives them an opportunity to notify administrators.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_display_login_attempts

Identifiers and References

References: 1, 12, 15, 16, 5.5.2, DSS05.04, DSS05.10, DSS06.10, CCI-000052, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, 0582, 0584, 05885, 0586, 0846, 0957, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, AC-9, AC-9(1), PR.AC-7, Req-10.2.4, 10.2.1.4, SRG-OS-000480-GPOS-00227, SV-230381r858726_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

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

            authselect apply-changes -b
        else

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

            CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
            # If not already in use, a custom profile is created preserving the enabled features.
            if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                authselect create-profile hardening -b $CURRENT_PROFILE
                CURRENT_PROFILE="custom/hardening"

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

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

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

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

                    CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
                    # If not already in use, a custom profile is created preserving the enabled features.
                    if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                        ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                        authselect create-profile hardening -b $CURRENT_PROFILE
                        CURRENT_PROFILE="custom/hardening"

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

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

                    authselect apply-changes -b
                fi
                if ! grep -qP '^\s*session\s+'"\[default=1\]"'\s+pam_lastlog.so\s*.*' "$PAM_FILE_PATH"; then
                        # Line matching group + control + module was not found. Check group + module.
                        if [ "$(grep -cP '^\s*session\s+.*\s+pam_lastlog.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then
                            # The control is updated only if one single line matches.
                            sed -i -E --follow-symlinks 's/^(\s*session\s+).*(\bpam_lastlog.so.*)/\1'"\[default=1\]"' \2/' "$PAM_FILE_PATH"
                        else
                            LAST_MATCH_LINE=$(grep -nP "^\s*session\s+.*pam_succeed_if\.so.*" "$PAM_FILE_PATH" | tail -n 1 | cut -d: -f 1)
                            if [ ! -z $LAST_MATCH_LINE ]; then
                                sed -i --follow-symlinks $LAST_MATCH_LINE' a session     '"\[default=1\]"'    pam_lastlog.so' "$PAM_FILE_PATH"
                            else
                                echo 'session    '"\[default=1\]"'    pam_lastlog.so' >> "$PAM_FILE_PATH"
                            fi
                        fi
                    fi
                    # Check the option
                    if ! grep -qP '^\s*session\s+'"\[default=1\]"'\s+pam_lastlog.so\s*.*\sshowfailed\b' "$PAM_FILE_PATH"; then
                        sed -i -E --follow-symlinks '/\s*session\s+'"\[default=1\]"'\s+pam_lastlog.so.*/ s/$/ showfailed/' "$PAM_FILE_PATH"
                    fi
                if [ -f /usr/bin/authselect ]; then

                    authselect apply-changes -b
                fi
            else
                echo "$PAM_FILE_PATH was not found" >&2
            fi
            if [ -e "$PAM_FILE_PATH" ] ; then
                PAM_FILE_PATH="$PAM_FILE_PATH"
                if [ -f /usr/bin/authselect ]; then

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

                    CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
                    # If not already in use, a custom profile is created preserving the enabled features.
                    if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                        ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                        authselect create-profile hardening -b $CURRENT_PROFILE
                        CURRENT_PROFILE="custom/hardening"

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

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

                    authselect apply-changes -b
                fi

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

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

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

                    CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
                    # If not already in use, a custom profile is created preserving the enabled features.
                    if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                        ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                        authselect create-profile hardening -b $CURRENT_PROFILE
                        CURRENT_PROFILE="custom/hardening"

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

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

                    authselect apply-changes -b
                fi
                if ! grep -qP '^\s*session\s+'"\[default=1\]"'\s+pam_lastlog.so\s*.*' "$PAM_FILE_PATH"; then
                        # Line matching group + control + module was not found. Check group + module.
                        if [ "$(grep -cP '^\s*session\s+.*\s+pam_lastlog.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then
                            # The control is updated only if one single line matches.
                            sed -i -E --follow-symlinks 's/^(\s*session\s+).*(\bpam_lastlog.so.*)/\1'"\[default=1\]"' \2/' "$PAM_FILE_PATH"
                        else
                            LAST_MATCH_LINE=$(grep -nP "^\s*session\s+.*pam_succeed_if\.so.*" "$PAM_FILE_PATH" | tail -n 1 | cut -d: -f 1)
                            if [ ! -z $LAST_MATCH_LINE ]; then
                                sed -i --follow-symlinks $LAST_MATCH_LINE' a session     '"\[default=1\]"'    pam_lastlog.so' "$PAM_FILE_PATH"
                            else
                                echo 'session    '"\[default=1\]"'    pam_lastlog.so' >> "$PAM_FILE_PATH"
                            fi
                        fi
                    fi
                    # Check the option
                    if ! grep -qP '^\s*session\s+'"\[default=1\]"'\s+pam_lastlog.so\s*.*\sshowfailed\b' "$PAM_FILE_PATH"; then
                        sed -i -E --follow-symlinks '/\s*session\s+'"\[default=1\]"'\s+pam_lastlog.so.*/ s/$/ showfailed/' "$PAM_FILE_PATH"
                    fi
                if [ -f /usr/bin/authselect ]; then

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

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

                    CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
                    # If not already in use, a custom profile is created preserving the enabled features.
                    if [[ ! $CURRENT_PROFILE == custom/* ]]; then
                        ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
                        authselect create-profile hardening -b $CURRENT_PROFILE
                        CURRENT_PROFILE="custom/hardening"

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

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

                    authselect apply-changes -b
                fi

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

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.5.2
      - DISA-STIG-RHEL-08-020340
      - NIST-800-53-AC-9
      - NIST-800-53-AC-9(1)
      - PCI-DSS-Req-10.2.4
      - PCI-DSSv4-10.2.1.4
      - configure_strategy
      - display_login_attempts
      - low_complexity
      - low_disruption
      - low_severity
      - no_reboot_needed

    - name: Ensure PAM Displays Last Logon/Access Notification - Check if system relies
        on authselect tool
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present
      when: '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.5.2
      - DISA-STIG-RHEL-08-020340
      - NIST-800-53-AC-9
      - NIST-800-53-AC-9(1)
      - PCI-DSS-Req-10.2.4
      - PCI-DSSv4-10.2.1.4
      - configure_strategy
      - display_login_attempts
      - low_complexity
      - low_disruption
      - low_severity
      - no_reboot_needed

    - name: Ensure PAM Displays Last Logon/Access Notification - Collect the Available
        authselect Features
      ansible.builtin.command:
        cmd: authselect list-features minimal
      register: result_authselect_available_features
      changed_when: false
      when:
      - '"pam" in ansible_facts.packages'
      - result_authselect_present.stat.exists
      tags:
      - CJIS-5.5.2
      - DISA-STIG-RHEL-08-020340
      - NIST-800-53-AC-9
      - NIST-800-53-AC-9(1)
      - PCI-DSS-Req-10.2.4
      - PCI-DSSv4-10.2.1.4
      - configure_strategy
      - display_login_attempts
      - low_complexity
      - low_disruption
      - low_severity
      - no_reboot_needed

    - name: Ensure PAM Displays Last Logon/Access Notification - Configure pam_lastlog.so
        Using authselect Feature
      block:

      - name: Ensure PAM Displays Last Logon/Access Notification - Check integrity of
          authselect current profile
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        failed_when: false

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

      - name: Ensure PAM Displays Last Logon/Access Notification - Get authselect Features
          Currently Enabled
        ansible.builtin.shell:
          cmd: authselect current | tail -n+3 | awk '{ print $2 }'
        register: result_authselect_features
        changed_when: false
        when:
        - result_authselect_check_cmd is success

      - name: Ensure PAM Displays Last Logon/Access Notification - Ensure "with-silent-lastlog"
          Feature is Disabled Using authselect Tool
        ansible.builtin.command:
          cmd: authselect disable-feature with-silent-lastlog
        register: result_authselect_disable_feature_cmd
        when:
        - result_authselect_check_cmd is success
        - result_authselect_features.stdout is search("with-silent-lastlog")

      - name: Ensure PAM Displays Last Logon/Access Notification - Ensure authselect changes
          are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_disable_feature_cmd is not skipped
        - result_authselect_disable_feature_cmd is success
      when:
      - '"pam" in ansible_facts.packages'
      - result_authselect_present.stat.exists
      - result_authselect_available_features.stdout is search("with-silent-lastlog")
      tags:
      - CJIS-5.5.2
      - DISA-STIG-RHEL-08-020340
      - NIST-800-53-AC-9
      - NIST-800-53-AC-9(1)
      - PCI-DSS-Req-10.2.4
      - PCI-DSSv4-10.2.1.4
      - configure_strategy
      - display_login_attempts
      - low_complexity
      - low_disruption
      - low_severity
      - no_reboot_needed

    - name: Ensure PAM Displays Last Logon/Access Notification - Configure pam_lastlog.so
        in appropriate PAM files
      block:

      - name: Ensure PAM Displays Last Logon/Access Notification - Define the PAM file
          to be edited as a local fact
        ansible.builtin.set_fact:
          pam_file_path: /etc/pam.d/postlogin

      - name: Ensure PAM Displays Last Logon/Access Notification - Check if system relies
          on authselect tool
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

      - name: Ensure PAM Displays Last Logon/Access Notification - Ensure authselect custom
          profile is used if authselect is present
        block:

        - name: Ensure PAM Displays Last Logon/Access Notification - Check integrity of
            authselect current profile
          ansible.builtin.command:
            cmd: authselect check
          register: result_authselect_check_cmd
          changed_when: false
          failed_when: false

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

        - name: Ensure PAM Displays Last Logon/Access Notification - Get authselect current
            profile
          ansible.builtin.shell:
            cmd: authselect current -r | awk '{ print $1 }'
          register: result_authselect_profile
          changed_when: false
          when:
          - result_authselect_check_cmd is success

        - name: Ensure PAM Displays Last Logon/Access Notification - Define the current
            authselect profile as a local fact
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is match("custom/")

        - name: Ensure PAM Displays Last Logon/Access Notification - Define the new authselect
            custom profile as a local fact
          ansible.builtin.set_fact:
            authselect_current_profile: '{{ result_authselect_profile.stdout }}'
            authselect_custom_profile: custom/hardening
          when:
          - result_authselect_profile is not skipped
          - result_authselect_profile.stdout is not match("custom/")

        - name: Ensure PAM Displays Last Logon/Access Notification - Get authselect current
            features to also enable them in the custom profile
          ansible.builtin.shell:
            cmd: authselect current | tail -n+3 | awk '{ print $2 }'
          register: result_authselect_features
          changed_when: false
          when:
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")

        - name: Ensure PAM Displays Last Logon/Access Notification - Check if any custom
            profile with the same name was already created
          ansible.builtin.stat:
            path: /etc/authselect/{{ authselect_custom_profile }}
          register: result_authselect_custom_profile_present
          changed_when: false
          when:
          - authselect_current_profile is not match("custom/")

        - name: Ensure PAM Displays Last Logon/Access Notification - Create an authselect
            custom profile based on the current profile
          ansible.builtin.command:
            cmd: authselect create-profile hardening -b {{ authselect_current_profile
              }}
          when:
          - result_authselect_check_cmd is success
          - authselect_current_profile is not match("custom/")
          - not result_authselect_custom_profile_present.stat.exists

        - name: Ensure PAM Displays Last Logon/Access Notification - Ensure authselect
            changes are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: Ensure PAM Displays Last Logon/Access Notification - Ensure the authselect
            custom profile is selected
          ansible.builtin.command:
            cmd: authselect select {{ authselect_custom_profile }}
          register: result_pam_authselect_select_profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - authselect_current_profile is not match("custom/")
          - authselect_custom_profile is not match(authselect_current_profile)

        - name: Ensure PAM Displays Last Logon/Access Notification - Restore the authselect
            features in the custom profile
          ansible.builtin.command:
            cmd: authselect enable-feature {{ item }}
          loop: '{{ result_authselect_features.stdout_lines }}'
          register: result_pam_authselect_restore_features
          when:
          - result_authselect_profile is not skipped
          - result_authselect_features is not skipped
          - result_pam_authselect_select_profile is not skipped

        - name: Ensure PAM Displays Last Logon/Access Notification - Ensure authselect
            changes are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
          when:
          - result_authselect_check_cmd is success
          - result_authselect_profile is not skipped
          - result_pam_authselect_restore_features is not skipped

        - name: Ensure PAM Displays Last Logon/Access Notification - Change the PAM file
            to be edited according to the custom authselect profile
          ansible.builtin.set_fact:
            pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
              | basename }}
        when:
        - result_authselect_present.stat.exists

      - name: Ensure PAM Displays Last Logon/Access Notification - Check if expected PAM
          module line is present in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*session\s+{{ '[default=1]' | regex_escape() }}\s+pam_lastlog.so\s*.*
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_line_present

      - name: Ensure PAM Displays Last Logon/Access Notification - Include or update the
          PAM module line in {{ pam_file_path }}
        block:

        - name: Ensure PAM Displays Last Logon/Access Notification - Check if required
            PAM module line is present in {{ pam_file_path }} with different control
          ansible.builtin.lineinfile:
            path: '{{ pam_file_path }}'
            regexp: ^\s*session\s+.*\s+pam_lastlog.so\s*
            state: absent
          check_mode: true
          changed_when: false
          register: result_pam_line_other_control_present

        - name: Ensure PAM Displays Last Logon/Access Notification - Ensure the correct
            control for the required PAM module line in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: ^(\s*session\s+).*(\bpam_lastlog.so.*)
            replace: \1[default=1] \2
          register: result_pam_module_edit
          when:
          - result_pam_line_other_control_present.found == 1

        - name: Ensure PAM Displays Last Logon/Access Notification - Ensure the required
            PAM module line is included in {{ pam_file_path }}
          ansible.builtin.lineinfile:
            dest: '{{ pam_file_path }}'
            insertafter: ^\s*session\s+.*pam_succeed_if\.so.*
            line: session    [default=1]    pam_lastlog.so
          register: result_pam_module_add
          when:
          - result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found
            > 1

        - name: Ensure PAM Displays Last Logon/Access Notification - Ensure authselect
            changes are applied
          ansible.builtin.command:
            cmd: authselect apply-changes -b
          when:
          - result_authselect_present is defined
          - result_authselect_present.stat.exists
          - |-
            (result_pam_module_add is defined and result_pam_module_add.changed)
             or (result_pam_module_edit is defined and result_pam_module_edit.changed)
        when:
        - result_pam_line_present.found is defined
        - result_pam_line_present.found == 0

      - name: Ensure PAM Displays Last Logon/Access Notification - Check if the required
          PAM module option is present in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*session\s+{{ '[default=1]' | regex_escape() }}\s+pam_lastlog.so\s*.*\sshowfailed\b
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_module_showfailed_option_present

      - name: Ensure PAM Displays Last Logon/Access Notification - Ensure the "showfailed"
          PAM option for "pam_lastlog.so" is included in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          backrefs: true
          regexp: ^(\s*session\s+{{ '[default=1]' | regex_escape() }}\s+pam_lastlog.so.*)
          line: \1 showfailed
          state: present
        register: result_pam_showfailed_add
        when:
        - result_pam_module_showfailed_option_present.found == 0

      - name: Ensure PAM Displays Last Logon/Access Notification - Ensure the "silent"
          option from "pam_lastlog.so" is not present in {{ pam_file_path }}
        ansible.builtin.replace:
          dest: '{{ pam_file_path }}'
          regexp: (.*session.*{{ '[default=1]' | regex_escape() }}.*pam_lastlog.so.*)\bsilent\b=?[0-9a-zA-Z]*(.*)
          replace: \1\2
        register: result_pam_option_removal
      when: '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.5.2
      - DISA-STIG-RHEL-08-020340
      - NIST-800-53-AC-9
      - NIST-800-53-AC-9(1)
      - PCI-DSS-Req-10.2.4
      - PCI-DSSv4-10.2.1.4
      - configure_strategy
      - display_login_attempts
      - low_complexity
      - low_disruption
      - low_severity
      - no_reboot_needed

Group Protect Physical Console Access Group contains 2 rules
[ref] It is impossible to fully protect a system from an attacker with physical access, so securing the space in which the system is located should be considered a necessary step. However, there are some steps which, if taken, make it more difficult for an attacker to quickly or undetectably modify a system from its console.

#### Rule Require Authentication for Emergency Systemd Target [ref]

Emergency mode is intended as a system recovery method, providing a single user root access to the system during a failed boot sequence. By default, Emergency mode is protected by requiring a password and is set in `/usr/lib/systemd/system/emergency.service`.
Rationale: | This prevents attackers with physical access from trivially bypassing security on the machine and gaining root access. Such accesses are further prevented by configuring the bootloader password.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_require_emergency_target_auth

Identifiers and References

References: 1, 11, 12, 14, 15, 16, 18, 3, 5, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.06, DSS06.10, 3.1.1, 3.4.5, CCI-000213, 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.6.1.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, IA-2, AC-3, CM-6(a), PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.PT-3, FIA_UAU.1, SRG-OS-000080-GPOS-00048, 1.4.3, SV-244523r743818_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

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

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Require emergency mode password
      lineinfile:
        create: true
        dest: /usr/lib/systemd/system/emergency.service
        regexp: ^#?ExecStart=
        line: ExecStart=-/usr/lib/systemd/systemd-sulogin-shell emergency
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010152
      - NIST-800-171-3.1.1
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-3
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-2
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - require_emergency_target_auth
      - restrict_strategy

#### Rule Require Authentication for Single User Mode [ref]

Single-user mode is intended as a system recovery method, providing a single user root access to the system by providing a boot option at startup. By default, single-user mode is protected by requiring a password and is set in `/usr/lib/systemd/system/rescue.service`.
Rationale: | This prevents attackers with physical access from trivially bypassing security on the machine and gaining root access. Such accesses are further prevented by configuring the bootloader password.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_require_singleuser_auth

Identifiers and References

References: 1, 11, 12, 14, 15, 16, 18, 3, 5, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.06, DSS06.10, 3.1.1, 3.4.5, CCI-000213, 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.6.1.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.2.3, CIP-004-6 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.2, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3, IA-2, AC-3, CM-6(a), PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.PT-3, FIA_UAU.1, SRG-OS-000080-GPOS-00048, 1.4.3, SV-230236r743928_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

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

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Require single user mode password
      lineinfile:
        create: true
        dest: /usr/lib/systemd/system/rescue.service
        regexp: ^#?ExecStart=
        line: ExecStart=-/usr/lib/systemd/systemd-sulogin-shell rescue
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010151
      - NIST-800-171-3.1.1
      - NIST-800-171-3.4.5
      - NIST-800-53-AC-3
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-2
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - require_singleuser_auth
      - restrict_strategy

Group Protect Accounts by Restricting Password-Based Login Group contains 3 groups and 7 rules

[ref] Conventionally, Unix shell accounts are accessed by providing a username and password to a login program, which tests these values for correctness using the `/etc/passwd` and `/etc/shadow` files. Password-based login is vulnerable to guessing of weak passwords, and to sniffing and man-in-the-middle attacks against passwords entered over a network or at an insecure console. Therefore, mechanisms for accessing accounts by entering usernames and passwords should be restricted to those which are operationally necessary.

Group Set Password Expiration Parameters Group contains 3 rules

[ref] The file `/etc/login.defs` controls several password-related settings. Programs such as `passwd`, `su`, and `login` consult `/etc/login.defs` to determine behavior with regard to password aging, expiration warnings, and length. See the man page `login.defs(5)` for more information.
Users should be forced to change their passwords, in order to decrease the utility of compromised passwords. However, the need to change passwords often should be balanced against the risk that users will reuse or write down passwords if forced to change them too often. Forcing password changes every 90-360 days, depending on the environment, is recommended. Set the appropriate value as `PASS_MAX_DAYS` and apply it to existing accounts with the `-M` flag.
The `PASS_MIN_DAYS` (`-m`) setting prevents password changes for 7 days after the first change, to discourage password cycling. If you use this setting, train users to contact an administrator for an emergency password change in case a new password becomes compromised. The `PASS_WARN_AGE` (`-W`) setting gives users 7 days of warnings at login time that their passwords are about to expire.
For example, for each existing human user USER, expiration parameters could be adjusted to a 180 day maximum password age, 7 day minimum password age, and 7 day warning period with the following command:

    $ sudo chage -M 180 -m 7 -W 7 USER

#### Rule Set Password Maximum Age [ref]

To specify password maximum age for new accounts, edit the file `/etc/login.defs` and add or correct the following line:

    PASS_MAX_DAYS 60

A value of 180 days is sufficient for many environments. The DoD requirement is 60. The profile requirement is `60`.

Rationale:

Any password, no matter how complex, can eventually be cracked. Therefore, passwords need to be changed periodically. If the operating system does not limit the lifetime of passwords and force users to change their passwords, there is the risk that the operating system passwords could be compromised.
Setting the password maximum age ensures users are required to periodically change their passwords. Requiring shorter password lifetimes increases the risk of users writing down the password in a convenient location subject to physical compromise.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_maximum_age_login_defs

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, 5, 5.6.2.1, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.5.6, CCI-000199, 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, 0418, 1055, 1402, 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(f), IA-5(1)(d), CM-6(a), PR.AC-1, PR.AC-6, PR.AC-7, Req-8.2.4, 8.3.9, SRG-OS-000076-GPOS-00044, 5.6.1.1, SV-230366r646878_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q shadow-utils; then

    var_accounts_maximum_age_login_defs='60'

    grep -q ^PASS_MAX_DAYS /etc/login.defs && \
      sed -i "s/PASS_MAX_DAYS.*/PASS_MAX_DAYS     $var_accounts_maximum_age_login_defs/g" /etc/login.defs
    if ! [ $? -eq 0 ]; then
        echo "PASS_MAX_DAYS      $var_accounts_maximum_age_login_defs" >> /etc/login.defs
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.1
      - DISA-STIG-RHEL-08-020200
      - NIST-800-171-3.5.6
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.4
      - PCI-DSSv4-8.3.9
      - accounts_maximum_age_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_maximum_age_login_defs # promote to variable
      set_fact:
        var_accounts_maximum_age_login_defs: !!str 60
      tags:
        - always

    - name: Set Password Maximum Age
      lineinfile:
        create: true
        dest: /etc/login.defs
        regexp: ^#?PASS_MAX_DAYS
        line: PASS_MAX_DAYS {{ var_accounts_maximum_age_login_defs }}
      when: '"shadow-utils" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1
      - DISA-STIG-RHEL-08-020200
      - NIST-800-171-3.5.6
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.4
      - PCI-DSSv4-8.3.9
      - accounts_maximum_age_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Set Password Minimum Age [ref]

To specify password minimum age for new accounts, edit the file `/etc/login.defs` and add or correct the following line:

    PASS_MIN_DAYS 1

A value of 1 day is considered sufficient for many environments. The DoD requirement is 1. The profile requirement is `1`.

Rationale:

Enforcing a minimum password lifetime helps to prevent repeated password changes to defeat the password reuse or history enforcement requirement. If users are allowed to immediately and continually change their password, then the password could be repeatedly changed in a short period of time to defeat the organization's policy regarding password reuse.
Setting the minimum password age protects against users cycling back to a favorite password after satisfying the password reuse requirement.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_minimum_age_login_defs

Identifiers and References

References: 1, 12, 15, 16, 5, 5.6.2.1.1, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.5.8, CCI-000198, 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, 0418, 1055, 1402, 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(f), IA-5(1)(d), CM-6(a), PR.AC-1, PR.AC-6, PR.AC-7, SRG-OS-000075-GPOS-00043, 5.6.1.2, SV-230365r858727_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q shadow-utils; then

    var_accounts_minimum_age_login_defs='1'

    grep -q ^PASS_MIN_DAYS /etc/login.defs && \
      sed -i "s/PASS_MIN_DAYS.*/PASS_MIN_DAYS     $var_accounts_minimum_age_login_defs/g" /etc/login.defs
    if ! [ $? -eq 0 ]; then
        echo "PASS_MIN_DAYS      $var_accounts_minimum_age_login_defs" >> /etc/login.defs
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.1.1
      - DISA-STIG-RHEL-08-020190
      - NIST-800-171-3.5.8
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - accounts_minimum_age_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_minimum_age_login_defs # promote to variable
      set_fact:
        var_accounts_minimum_age_login_defs: !!str 1
      tags:
        - always

    - name: Set Password Minimum Age
      lineinfile:
        create: true
        dest: /etc/login.defs
        regexp: ^#?PASS_MIN_DAYS
        line: PASS_MIN_DAYS {{ var_accounts_minimum_age_login_defs }}
      when: '"shadow-utils" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1.1
      - DISA-STIG-RHEL-08-020190
      - NIST-800-171-3.5.8
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - accounts_minimum_age_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Set Password Warning Age [ref]

To specify how many days prior to password expiration that a warning will be issued to users, edit the file `/etc/login.defs` and add or correct the following line:

    PASS_WARN_AGE 7

The DoD requirement is 7. The profile requirement is `7`.

Rationale: | Setting the password warning age enables users to make the change at a practical time.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_warn_age_login_defs

Identifiers and References

References: 1, 12, 13, 14, 15, 16, 18, 3, 5, 7, 8, DSS01.03, DSS03.05, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.5.8, 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 6.2, 0418, 1055, 1402, A.12.4.1, A.12.4.3, A.18.1.4, A.6.1.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, IA-5(f), IA-5(1)(d), CM-6(a), DE.CM-1, DE.CM-3, PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, Req-8.2.4, 8.3.9, 5.6.1.3

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q shadow-utils; then

    var_accounts_password_warn_age_login_defs='7'

    grep -q ^PASS_WARN_AGE /etc/login.defs && \
    sed -i "s/PASS_WARN_AGE.*/PASS_WARN_AGE\t$var_accounts_password_warn_age_login_defs/g" /etc/login.defs
    if ! [ $? -eq 0 ]
    then
      echo -e "PASS_WARN_AGE\t$var_accounts_password_warn_age_login_defs" >> /etc/login.defs
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-171-3.5.8
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.4
      - PCI-DSSv4-8.3.9
      - accounts_password_warn_age_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_password_warn_age_login_defs # promote to variable
      set_fact:
        var_accounts_password_warn_age_login_defs: !!str 7
      tags:
        - always

    - name: Set Password Warning Age
      lineinfile:
        dest: /etc/login.defs
        regexp: ^PASS_WARN_AGE *[0-9]*
        state: present
        line: PASS_WARN_AGE        {{ var_accounts_password_warn_age_login_defs }}
        create: true
      when: '"shadow-utils" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.5.8
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.4
      - PCI-DSSv4-8.3.9
      - accounts_password_warn_age_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

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, 8.3.2

#### 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` and `/etc/pam.d/password-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, 8.3.1, SRG-OS-000480-GPOS-00227, 5.4.1, SV-244540r743869_rule, SV-244541r743872_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

    authselect apply-changes -b
    else

    if grep -qP '^\s*auth\s+'"sufficient"'\s+pam_unix.so\s.*\bnullok\b' "/etc/pam.d/system-auth"; then
        sed -i -E --follow-symlinks 's/(.*auth.*'"sufficient"'.*pam_unix.so.*)\snullok=?[[:alnum:]]*(.*)/\1\2/g' "/etc/pam.d/system-auth"
    fi

    if grep -qP '^\s*password\s+'"sufficient"'\s+pam_unix.so\s.*\bnullok\b' "/etc/pam.d/system-auth"; then
        sed -i -E --follow-symlinks 's/(.*password.*'"sufficient"'.*pam_unix.so.*)\snullok=?[[:alnum:]]*(.*)/\1\2/g' "/etc/pam.d/system-auth"
    fi

    if grep -qP '^\s*auth\s+'"sufficient"'\s+pam_unix.so\s.*\bnullok\b' "/etc/pam.d/password-auth"; then
        sed -i -E --follow-symlinks 's/(.*auth.*'"sufficient"'.*pam_unix.so.*)\snullok=?[[:alnum:]]*(.*)/\1\2/g' "/etc/pam.d/password-auth"
    fi

    if grep -qP '^\s*password\s+'"sufficient"'\s+pam_unix.so\s.*\bnullok\b' "/etc/pam.d/password-auth"; then
        sed -i -E --follow-symlinks 's/(.*password.*'"sufficient"'.*pam_unix.so.*)\snullok=?[[:alnum:]]*(.*)/\1\2/g' "/etc/pam.d/password-auth"
    fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure

    - name: Prevent Login to Accounts With Empty Password - 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-RHEL-08-020331
      - DISA-STIG-RHEL-08-020332
      - 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
      - PCI-DSSv4-8.3.1
      - configure_strategy
      - high_severity
      - low_complexity
      - medium_disruption
      - no_empty_passwords
      - no_reboot_needed

    - name: Prevent Login to Accounts With Empty Password - Remediate using authselect
      block:

      - name: Prevent Login to Accounts With Empty Password - Check integrity of authselect
          current profile
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        failed_when: false

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

      - name: Prevent Login to Accounts With Empty Password - Get authselect current features
        ansible.builtin.shell:
          cmd: authselect current | tail -n+3 | awk '{ print $2 }'
        register: result_authselect_features
        changed_when: false
        when:
        - result_authselect_check_cmd is success

      - name: Prevent Login to Accounts With Empty Password - Ensure "without-nullok"
          feature is enabled using authselect tool
        ansible.builtin.command:
          cmd: authselect enable-feature without-nullok
        register: result_authselect_enable_feature_cmd
        when:
        - result_authselect_check_cmd is success
        - result_authselect_features.stdout is not search("without-nullok")

      - name: Prevent Login to Accounts With Empty Password - Ensure authselect changes
          are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_enable_feature_cmd is not skipped
        - result_authselect_enable_feature_cmd is success
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - result_authselect_present.stat.exists
      tags:
      - CJIS-5.5.2
      - DISA-STIG-RHEL-08-020331
      - DISA-STIG-RHEL-08-020332
      - 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
      - PCI-DSSv4-8.3.1
      - configure_strategy
      - high_severity
      - low_complexity
      - medium_disruption
      - no_empty_passwords
      - no_reboot_needed

    - name: Prevent Login to Accounts With Empty Password - Remediate directly editing
        PAM files
      ansible.builtin.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-RHEL-08-020331
      - DISA-STIG-RHEL-08-020332
      - 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
      - PCI-DSSv4-8.3.1
      - configure_strategy
      - high_severity
      - low_complexity
      - medium_disruption
      - no_empty_passwords
      - no_reboot_needed

Remediation Kubernetes snippet ⇲

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,%23%20Generated%20by%20authselect%20on%20Sat%20Oct%2027%2014%3A59%3A36%202018%0A%23%20Do%20not%20modify%20this%20file%20manually.%0A%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_env.so%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_faildelay.so%20delay%3D2000000%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_fprintd.so%0Aauth%20%20%20%20%20%20%20%20%5Bdefault%3D1%20ignore%3Dignore%20success%3Dok%5D%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3E%3D%201000%20quiet%0Aauth%20%20%20%20%20%20%20%20%5Bdefault%3D1%20ignore%3Dignore%20success%3Dok%5D%20%20%20%20%20%20%20%20%20pam_localuser.so%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%20try_first_pass%0Aauth%20%20%20%20%20%20%20%20requisite%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3E%3D%201000%20quiet_success%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%20forward_pass%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_deny.so%0A%0Aaccount%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%0Aaccount%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_localuser.so%0Aaccount%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3C%201000%20quiet%0Aaccount%20%20%20%20%20%5Bdefault%3Dbad%20success%3Dok%20user_unknown%3Dignore%5D%20pam_sss.so%0Aaccount%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_permit.so%0A%0Apassword%20%20%20%20requisite%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_pwquality.so%20try_first_pass%20local_users_only%0Apassword%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%20sha512%20shadow%20try_first_pass%20use_authtok%0Apassword%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%20use_authtok%0Apassword%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_deny.so%0A%0Asession%20%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_keyinit.so%20revoke%0Asession%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_limits.so%0A-session%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_systemd.so%0Asession%20%20%20%20%20%5Bsuccess%3D1%20default%3Dignore%5D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20service%20in%20crond%20quiet%20use_uid%0Asession%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%0Asession%20%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%0A
            mode: 0644
            path: /etc/pam.d/password-auth
            overwrite: true
          - contents:
              source: data:,%23%20Generated%20by%20authselect%20on%20Sat%20Oct%2027%2014%3A59%3A36%202018%0A%23%20Do%20not%20modify%20this%20file%20manually.%0A%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_env.so%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_faildelay.so%20delay%3D2000000%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_fprintd.so%0Aauth%20%20%20%20%20%20%20%20%5Bdefault%3D1%20ignore%3Dignore%20success%3Dok%5D%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3E%3D%201000%20quiet%0Aauth%20%20%20%20%20%20%20%20%5Bdefault%3D1%20ignore%3Dignore%20success%3Dok%5D%20%20%20%20%20%20%20%20%20pam_localuser.so%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%20try_first_pass%0Aauth%20%20%20%20%20%20%20%20requisite%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3E%3D%201000%20quiet_success%0Aauth%20%20%20%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%20forward_pass%0Aauth%20%20%20%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_deny.so%0A%0Aaccount%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%0Aaccount%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_localuser.so%0Aaccount%20%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20uid%20%3C%201000%20quiet%0Aaccount%20%20%20%20%20%5Bdefault%3Dbad%20success%3Dok%20user_unknown%3Dignore%5D%20pam_sss.so%0Aaccount%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_permit.so%0A%0Apassword%20%20%20%20requisite%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_pwquality.so%20try_first_pass%20local_users_only%0Apassword%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%20sha512%20shadow%20try_first_pass%20use_authtok%0Apassword%20%20%20%20sufficient%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%20use_authtok%0Apassword%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_deny.so%0A%0Asession%20%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_keyinit.so%20revoke%0Asession%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_limits.so%0A-session%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_systemd.so%0Asession%20%20%20%20%20%5Bsuccess%3D1%20default%3Dignore%5D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_succeed_if.so%20service%20in%20crond%20quiet%20use_uid%0Asession%20%20%20%20%20required%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_unix.so%0Asession%20%20%20%20%20optional%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pam_sss.so%0A
            mode: 0644
            path: /etc/pam.d/system-auth
            overwrite: true

Group Restrict Root Logins Group contains 2 rules

[ref] Direct root logins should be allowed only for emergency use. In normal situations, the administrator should access the system via a unique unprivileged account, and then use `su` or `sudo` to execute privileged commands. Discouraging administrators from accessing the root account directly ensures an audit trail in organizations with multiple administrators. Locking down the channels through which root can connect directly also reduces opportunities for password-guessing against the root account. The `login` program uses the file `/etc/securetty` to determine which interfaces should allow root logins. The virtual devices `/dev/console` and `/dev/tty*` represent the system consoles (accessible via the Ctrl-Alt-F1 through Ctrl-Alt-F6 keyboard sequences on a default installation). The default securetty file also contains `/dev/vc/*`. These are likely to be deprecated in most environments, but may be retained for compatibility. Root should also be prohibited from connecting via network protocols. Other sections of this document include guidance describing how to prevent root from logging in via SSH.

#### Rule Verify Only Root Has UID 0 [ref]

If any account other than root has a UID of 0, this misconfiguration should be investigated and the accounts other than root should be removed or have their UID changed. If the account is associated with system commands or applications the UID should be changed to one greater than "0" but less than "1000." Otherwise assign a UID greater than "1000" that has not already been assigned.
Rationale: | An account has root authority if it has a UID of 0. Multiple accounts with a UID of 0 afford more opportunity for potential intruders to guess a password for a privileged account. Proper configuration of sudo is recommended to afford multiple system administrators access to root privileges in an accountable manner.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_no_uid_except_zero

Identifiers and References

References: 1, 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.02, DSS06.03, DSS06.10, 3.1.1, 3.1.5, CCI-000366, 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, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.2.3, CIP-004-6 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.2, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3, IA-2, AC-6(5), IA-4(b), PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.DS-5, Req-8.5, 8.2.1, SRG-OS-000480-GPOS-00227, 6.2.8, SV-230534r627750_rule

Remediation Shell script ⇲

    awk -F: '$3 == 0 && $1 != "root" { print $1 }' /etc/passwd | xargs --no-run-if-empty --max-lines=1 passwd -l

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Get all /etc/passwd file entries
      getent:
        database: passwd
        split: ':'
      tags:
      - DISA-STIG-RHEL-08-040200
      - NIST-800-171-3.1.1
      - NIST-800-171-3.1.5
      - NIST-800-53-AC-6(5)
      - NIST-800-53-IA-2
      - NIST-800-53-IA-4(b)
      - PCI-DSS-Req-8.5
      - PCI-DSSv4-8.2.1
      - accounts_no_uid_except_zero
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - restrict_strategy

    - name: Lock the password of the user accounts other than root with uid 0
      command: passwd -l {{ item.key }}
      loop: '{{ getent_passwd | dict2items | rejectattr(''key'', ''search'', ''root'')
        | list }}'
      when: item.value.1  == '0'
      tags:
      - DISA-STIG-RHEL-08-040200
      - NIST-800-171-3.1.1
      - NIST-800-171-3.1.5
      - NIST-800-53-AC-6(5)
      - NIST-800-53-IA-2
      - NIST-800-53-IA-4(b)
      - PCI-DSS-Req-8.5
      - PCI-DSSv4-8.2.1
      - accounts_no_uid_except_zero
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - restrict_strategy

#### Rule Ensure that System Accounts Do Not Run a Shell Upon Login [ref]

Some accounts are not associated with a human user of the system, and exist to perform some administrative functions. Should an attacker be able to log into these accounts, they should not be granted access to a shell.
The login shell for each local account is stored in the last field of each line in `/etc/passwd`. System accounts are those user accounts with a user ID less than `1000`. The user ID is stored in the third field. If any system account other than `root` has a login shell, disable it with the command:

    $ sudo usermod -s /sbin/nologin account

Warning: Do not perform the steps in this section on the root account. Doing so might cause the system to become inaccessible.

Rationale: | Ensuring shells are not given to system accounts upon login makes it more difficult for attackers to make use of system accounts.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_no_shelllogin_for_systemaccounts

Identifiers and References

References: 1, 12, 13, 14, 15, 16, 18, 3, 5, 7, 8, DSS01.03, DSS03.05, DSS05.04, DSS05.05, DSS05.07, DSS06.03, CCI-000366, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, SR 1.1, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 6.2, 1491, A.12.4.1, A.12.4.3, A.6.1.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, AC-6, CM-6(a), CM-6(b), CM-6.1(iv), DE.CM-1, DE.CM-3, PR.AC-1, PR.AC-4, PR.AC-6, 8.2.2, SRG-OS-000480-GPOS-00227, 5.6.2

Remediation Shell script ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | restrict

    readarray -t systemaccounts < <(awk -F: '($3 < 1000 && $3 != root \
      && $7 != "\/sbin\/shutdown" && $7 != "\/sbin\/halt" && $7 != "\/bin\/sync") \
      { print $1 }' /etc/passwd)

    for systemaccount in "${systemaccounts[@]}"; do
        usermod -s /sbin/nologin "$systemaccount"
    done

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | restrict

    - name: Ensure that System Accounts Do Not Run a Shell Upon Login - Get All Local
        Users From /etc/passwd
      ansible.builtin.getent:
        database: passwd
        split: ':'
      tags:
      - NIST-800-53-AC-6
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - PCI-DSSv4-8.2.2
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - no_shelllogin_for_systemaccounts
      - restrict_strategy

    - name: Ensure that System Accounts Do Not Run a Shell Upon Login - Create local_users
        Variable From getent_passwd Facts
      ansible.builtin.set_fact:
        local_users: '{{ ansible_facts.getent_passwd | dict2items }}'
      tags:
      - NIST-800-53-AC-6
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - PCI-DSSv4-8.2.2
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - no_shelllogin_for_systemaccounts
      - restrict_strategy

    - name: Ensure that System Accounts Do Not Run a Shell Upon Login -  Disable Login
        Shell for System Accounts
      ansible.builtin.user:
        name: '{{ item.key }}'
        shell: /sbin/nologin
      loop: '{{ local_users }}'
      when:
      - item.key not in ['root']
      - item.value[1]|int < 1000
      - item.value[5] not in ['/sbin/shutdown', '/sbin/halt', '/bin/sync']
      tags:
      - NIST-800-53-AC-6
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-6.1(iv)
      - PCI-DSSv4-8.2.2
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - no_shelllogin_for_systemaccounts
      - restrict_strategy

#### Rule Enable authselect [ref]

Configure user authentication setup to use the `authselect` tool. If authselect profile is selected, the rule will enable the sssd profile.
Warning: If the `sudo authselect select` command returns an error informing that the chosen profile cannot be selected, it is probably because PAM files have already been modified by the administrator. If this is the case, in order to not overwrite the desired changes made by the administrator, the current PAM settings should be investigated before forcing the selection of the chosen authselect profile.

Rationale: | Authselect is a successor to authconfig. It is a tool to select system authentication and identity sources from a list of supported profiles instead of letting the administrator manually build the PAM stack. That way, it avoids potential breakage of configuration, as it ships several tested profiles that are well tested and supported to solve different use-cases.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_enable_authselect
Identifiers and References | References: BP28(R31), CCI-000213, 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii), AC-3, FIA_UAU.1, FIA_AFL.1, SRG-OS-000480-GPOS-00227, 1.2.3

Remediation Shell script ⇲

    var_authselect_profile='sssd'

    authselect select "$var_authselect_profile"

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

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure

    - name: XCCDF Value var_authselect_profile # promote to variable
      set_fact:
        var_authselect_profile: !!str sssd
      tags:
        - always

    - name: Enable authselect - Select authselect profile
      ansible.builtin.command:
        cmd: authselect select "{{ var_authselect_profile }}"
      register: result_authselect_select
      failed_when: false
      tags:
      - NIST-800-53-AC-3
      - configure_strategy
      - enable_authselect
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Enable authselect - Verify if PAM has been altered
      ansible.builtin.command:
        cmd: rpm -qV pam
      register: result_altered_authselect
      failed_when: false
      when: result_authselect_select.rc != 0
      tags:
      - NIST-800-53-AC-3
      - configure_strategy
      - enable_authselect
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Enable authselect - Informative message based on the authselect integrity
        check
      ansible.builtin.assert:
        that:
        - result_altered_authselect is skipped or result_altered_authselect.rc == 0
        fail_msg:
        - Files in the 'pam' package have been altered, so the authselect configuration
          won't be forced.
      tags:
      - NIST-800-53-AC-3
      - configure_strategy
      - enable_authselect
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Enable authselect - Force authselect profile select
      ansible.builtin.command:
        cmd: authselect select --force "{{ var_authselect_profile }}"
      when:
      - result_authselect_select.rc != 0
      - result_altered_authselect is skipped or result_altered_authselect.rc == 0
      tags:
      - NIST-800-53-AC-3
      - configure_strategy
      - enable_authselect
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

Group System Accounting with auditd Group contains 10 groups and 33 rules

[ref] The audit service provides substantial capabilities for recording system activities. By default, the service audits about SELinux AVC denials and certain types of security-relevant events such as system logins, account modifications, and authentication events performed by programs such as sudo. Under its default configuration, `auditd` has modest disk space requirements, and should not noticeably impact system performance.
NOTE: The Linux Audit daemon `auditd` can be configured to use the `augenrules` program to read audit rules files (`*.rules`) located in `/etc/audit/rules.d` location and compile them to create the resulting form of the `/etc/audit/audit.rules` configuration file during the daemon startup (default configuration). Alternatively, the `auditd` daemon can use the `auditctl` utility to read audit rules from the `/etc/audit/audit.rules` configuration file during daemon startup, and load them into the kernel. The expected behavior is configured via the appropriate `ExecStartPost` directive setting in the `/usr/lib/systemd/system/auditd.service` configuration file. To instruct the `auditd` daemon to use the `augenrules` program to read audit rules (default configuration), use the following setting:

    ExecStartPost=-/sbin/augenrules --load

in the `/usr/lib/systemd/system/auditd.service` configuration file. In order to instruct the `auditd` daemon to use the `auditctl` utility to read audit rules, use the following setting:

    ExecStartPost=-/sbin/auditctl -R /etc/audit/audit.rules

in the `/usr/lib/systemd/system/auditd.service` configuration file. Refer to `[Service]` section of the `/usr/lib/systemd/system/auditd.service` configuration file for further details.
Government networks often have substantial auditing requirements and `auditd` can be configured to meet these requirements. Examining some example audit records demonstrates how the Linux audit system satisfies common requirements. The following example from Red Hat Enterprise Linux 7 Documentation available at `https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html-single/selinux_users_and_administrators_guide/index#sect-Security-Enhanced_Linux-Fixing_Problems-Raw_Audit_Messages` shows the substantial amount of information captured in a two typical "raw" audit messages, followed by a breakdown of the most important fields. In this example the message is SELinux-related and reports an AVC denial (and the associated system call) that occurred when the Apache HTTP Server attempted to access the `/var/www/html/file1` file (labeled with the `samba_share_t` type):

    type=AVC msg=audit(1226874073.147:96): avc:  denied  { getattr } for pid=2465 comm="httpd"
    path="/var/www/html/file1" dev=dm-0 ino=284133 scontext=unconfined_u:system_r:httpd_t:s0
    tcontext=unconfined_u:object_r:samba_share_t:s0 tclass=file

    type=SYSCALL msg=audit(1226874073.147:96): arch=40000003 syscall=196 success=no exit=-13
    a0=b98df198 a1=bfec85dc a2=54dff4 a3=2008171 items=0 ppid=2463 pid=2465 auid=502 uid=48
    gid=48 euid=48 suid=48 fsuid=48 egid=48 sgid=48 fsgid=48 tty=(none) ses=6 comm="httpd"
    exe="/usr/sbin/httpd" subj=unconfined_u:system_r:httpd_t:s0 key=(null)

- `msg=audit(1226874073.147:96)`
  - The number in parentheses is the unformatted time stamp (Epoch time) for the event, which can be converted to standard time by using the `date` command.
- `{ getattr }`
  - The item in braces indicates the permission that was denied. `getattr` indicates the source process was trying to read the target file's status information. This occurs before reading files. This action is denied due to the file being accessed having the wrong label. Commonly seen permissions include `getattr`, `read`, and `write`.
- `comm="httpd"`
  - The executable that launched the process. The full path of the executable is found in the `exe=` section of the system call (`SYSCALL`) message, which in this case, is `exe="/usr/sbin/httpd"`.
- `path="/var/www/html/file1"`
  - The path to the object (target) the process attempted to access.
- `scontext="unconfined_u:system_r:httpd_t:s0"`
  - The SELinux context of the process that attempted the denied action. In this case, it is the SELinux context of the Apache HTTP Server, which is running in the `httpd_t` domain.
- `tcontext="unconfined_u:object_r:samba_share_t:s0"`
  - The SELinux context of the object (target) the process attempted to access. In this case, it is the SELinux context of `file1`. Note: the `samba_share_t` type is not accessible to processes running in the `httpd_t` domain.
- From the system call (`SYSCALL`) message, two items are of interest:
  - `success=no`: indicates whether the denial (AVC) was enforced or not. `success=no` indicates the system call was not successful (SELinux denied access). `success=yes` indicates the system call was successful - this can be seen for permissive domains or unconfined domains, such as `initrc_t` and `kernel_t`.
  - `exe="/usr/sbin/httpd"`: the full path to the executable that launched the process, which in this case, is `exe="/usr/sbin/httpd"`.

Group Configure auditd Rules for Comprehensive Auditing Group contains 7 groups and 24 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 2 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: BP28(R73), 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-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.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, 10.3.4, 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-APP-000091-CTR-000160, SRG-APP-000492-CTR-001220, SRG-APP-000493-CTR-001225, SRG-APP-000494-CTR-001230, SRG-APP-000500-CTR-001260, SRG-APP-000507-CTR-001295, SRG-APP-000495-CTR-001235, SRG-APP-000499-CTR-001255, 4.1.3.9, SV-230456r810462_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit && { ! ( grep -q aarch64 /proc/sys/kernel/osrelease ); }; 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-RHEL-08-030490
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3.4
      - 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: b64
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - not ( ansible_architecture == "aarch64" )
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-08-030490
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_chmod
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

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

      - name: Declare list of syscalls
        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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - not ( ansible_architecture == "aarch64" )
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-08-030490
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_chmod
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

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

      - name: Declare list of syscalls
        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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - not ( ansible_architecture == "aarch64" )
      - audit_arch == "b64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-08-030490
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_chmod
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

#### Rule Record Events that Modify the System's Discretionary Access Controls - chown [ref]

At a minimum, the audit system should collect file permission changes for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:

    -a always,exit -F arch=b32 -S chown -F auid>=1000 -F auid!=unset -F key=perm_mod

If the system is 64 bit then also add the following line:

    -a always,exit -F arch=b64 -S chown -F auid>=1000 -F auid!=unset -F key=perm_mod

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:

    -a always,exit -F arch=b32 -S chown -F auid>=1000 -F auid!=unset -F key=perm_mod

If the system is 64 bit then also add the following line:

    -a always,exit -F arch=b64 -S chown -F auid>=1000 -F auid!=unset -F key=perm_mod

Warning: Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.

Rationale: | The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_dac_modification_chown

Identifiers and References

References: BP28(R73), 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-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.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, 10.3.4, 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-APP-000091-CTR-000160, SRG-APP-000492-CTR-001220, SRG-APP-000493-CTR-001225, SRG-APP-000494-CTR-001230, SRG-APP-000500-CTR-001260, SRG-APP-000507-CTR-001295, SRG-APP-000495-CTR-001235, SRG-APP-000499-CTR-001255, 4.1.3.9, SV-230455r810459_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit && { ! ( grep -q aarch64 /proc/sys/kernel/osrelease ); }; 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-RHEL-08-030480
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3.4
      - 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: b64
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - not ( ansible_architecture == "aarch64" )
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-08-030480
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_chown
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

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

      - name: Declare list of syscalls
        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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - not ( ansible_architecture == "aarch64" )
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-08-030480
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_chown
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

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

      - name: Declare list of syscalls
        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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - not ( ansible_architecture == "aarch64" )
      - audit_arch == "b64"
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-08-030480
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.5
      - PCI-DSSv4-10.3.4
      - audit_rules_dac_modification_chown
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

Group Record Execution Attempts to Run SELinux Privileged Commands Group contains 6 rules
[ref] At a minimum, the audit system should collect the execution of SELinux privileged commands for all users and root.

#### Rule Record Any Attempts to Run chcon [ref]

At a minimum, the audit system should collect any execution attempt of the `chcon` command for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:

    -a always,exit -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules` file:

    -a always,exit -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.
Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_execution_chcon

Identifiers and References

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-000468-GPOS-00212, SRG-OS-000471-GPOS-00215, SRG-OS-000463-GPOS-00207, SRG-OS-000465-GPOS-00209, SRG-APP-000495-CTR-001235, SRG-APP-000496-CTR-001240, SRG-APP-000497-CTR-001245, SRG-APP-000498-CTR-001250, SRG-APP-000501-CTR-001265, SRG-APP-000502-CTR-001270, 4.1.3.15, SV-230419r627750_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/bin/chcon -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0640 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod o-rwx ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod o-rwx ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-030260
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - audit_rules_execution_chcon
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

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

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

      - name: Check existence of  in /etc/audit/rules.d/
        find:
          paths: /etc/audit/rules.d
          contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
            path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
          | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
        set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
        when: found_paths | length == 0

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

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

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

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

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

      - name: Check existence of  in /etc/audit/audit.rules
        find:
          paths: /etc/audit
          contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
            path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

      - name: Add the audit rule to {{ audit_file }}
        lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chcon -F perm=x
            -F auid>=1000 -F auid!=unset -F key=privileged
          create: true
          mode: o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-030260
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - audit_rules_execution_chcon
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Record Any Attempts to Run restorecon [ref]

At a minimum, the audit system should collect any execution attempt of the `restorecon` command for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:

    -a always,exit -F path=/usr/sbin/restorecon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules` file:

    -a always,exit -F path=/usr/sbin/restorecon -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.
Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_execution_restorecon

Identifiers and References

References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000392-GPOS-00172, SRG-OS-000463-GPOS-00207, SRG-OS-000465-GPOS-00209

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/sbin/restorecon -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0640 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod o-rwx ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod o-rwx ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
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_execution_restorecon
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

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

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

      - name: Check existence of  in /etc/audit/rules.d/
        find:
          paths: /etc/audit/rules.d
          contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
            path=/usr/sbin/restorecon -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
          | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
        set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
        when: found_paths | length == 0

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

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

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

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

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

      - name: Check existence of  in /etc/audit/audit.rules
        find:
          paths: /etc/audit
          contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
            path=/usr/sbin/restorecon -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

      - name: Add the audit rule to {{ audit_file }}
        lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/restorecon
            -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged
          create: true
          mode: o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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_execution_restorecon
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Record Any Attempts to Run semanage [ref]

At a minimum, the audit system should collect any execution attempt of the `semanage` command for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:

    -a always,exit -F path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules` file:

    -a always,exit -F path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.
Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_execution_semanage

Identifiers and References

References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000169, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, CIP-004-6 R2.2.2, CIP-004-6 R2.2.3, CIP-007-3 R.1.3, CIP-007-3 R5, CIP-007-3 R5.1.1, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000463-GPOS-00207, SRG-OS-000465-GPOS-00209, SRG-APP-000495-CTR-001235, SRG-APP-000496-CTR-001240, SRG-APP-000497-CTR-001245, SRG-APP-000498-CTR-001250, SV-230429r627750_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/sbin/semanage -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0640 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod o-rwx ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod o-rwx ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-030313
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - audit_rules_execution_semanage
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

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

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

      - name: Check existence of  in /etc/audit/rules.d/
        find:
          paths: /etc/audit/rules.d
          contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
            path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
          | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
        set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
        when: found_paths | length == 0

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

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

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

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

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

      - name: Check existence of  in /etc/audit/audit.rules
        find:
          paths: /etc/audit
          contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
            path=/usr/sbin/semanage -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

      - name: Add the audit rule to {{ audit_file }}
        lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/semanage -F
            perm=x -F auid>=1000 -F auid!=unset -F key=privileged
          create: true
          mode: o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-030313
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(4)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - audit_rules_execution_semanage
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Record Any Attempts to Run setfiles [ref]

At a minimum, the audit system should collect any execution attempt of the `setfiles` command for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:

    -a always,exit -F path=/usr/sbin/setfiles -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules` file:

    -a always,exit -F path=/usr/sbin/setfiles -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.
Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_execution_setfiles

Identifiers and References

References: CCI-000169, CCI-000172, CCI-002884, AU-2(d), AU-12(c), AC-6(9), CM-6(a), SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000463-GPOS-00207, SRG-OS-000465-GPOS-00209, SRG-APP-000495-CTR-001235, SRG-APP-000496-CTR-001240, SRG-APP-000497-CTR-001245, SRG-APP-000498-CTR-001250, SV-230430r627750_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/sbin/setfiles -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0640 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod o-rwx ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod o-rwx ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-030314
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - audit_rules_execution_setfiles
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/sbin/setfiles
      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/setfiles -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
          | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
        set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
        when: found_paths | length == 0

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

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

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

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

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

      - name: Check existence of  in /etc/audit/audit.rules
        find:
          paths: /etc/audit
          contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
            path=/usr/sbin/setfiles -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

      - name: Add the audit rule to {{ audit_file }}
        lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/setfiles -F
            perm=x -F auid>=1000 -F auid!=unset -F key=privileged
          create: true
          mode: o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-030314
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - audit_rules_execution_setfiles
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Record Any Attempts to Run setsebool [ref]

At a minimum, the audit system should collect any execution attempt of the `setsebool` command for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:

    -a always,exit -F path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules` file:

    -a always,exit -F path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.
Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_execution_setsebool

Identifiers and References

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-000463-GPOS-00207, SRG-OS-000465-GPOS-00209, SRG-APP-000495-CTR-001235, SRG-APP-000496-CTR-001240, SRG-APP-000497-CTR-001245, SRG-APP-000498-CTR-001250, SV-230432r627750_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/sbin/setsebool -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0640 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod o-rwx ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod o-rwx ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-030316
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - audit_rules_execution_setsebool
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

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

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

      - name: Check existence of  in /etc/audit/rules.d/
        find:
          paths: /etc/audit/rules.d
          contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
            path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
          | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
        set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
        when: found_paths | length == 0

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

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

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

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

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

      - name: Check existence of  in /etc/audit/audit.rules
        find:
          paths: /etc/audit
          contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
            path=/usr/sbin/setsebool -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

      - name: Add the audit rule to {{ audit_file }}
        lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/setsebool -F
            perm=x -F auid>=1000 -F auid!=unset -F key=privileged
          create: true
          mode: o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-030316
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - audit_rules_execution_setsebool
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Record Any Attempts to Run seunshare [ref]

At a minimum, the audit system should collect any execution attempt of the `seunshare` command for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:

    -a always,exit -F path=/usr/sbin/seunshare -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules` file:

    -a always,exit -F path=/usr/sbin/seunshare -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.
Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_execution_seunshare
Identifiers and References | References: CCI-000172, AU-2(d), AU-12(c), AC-6(9), CM-6(a), FAU_GEN.1.1.c

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    OTHER_FILTERS="-F path=/usr/sbin/seunshare -F perm=x"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""
    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'augenrules', then check if the audit rule is defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
    # If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
    default_file="/etc/audit/rules.d/$KEY.rules"
    # As other_filters may include paths, lets use a different delimiter for it
    # The "F" script expression tells sed to print the filenames where the expressions matched
    readarray -t files_to_inspect < <(sed -s -n -e "/^$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
    # Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
    if [ ${#files_to_inspect[@]} -eq "0" ]
    then
        file_to_inspect="/etc/audit/rules.d/$KEY.rules"
        files_to_inspect=("$file_to_inspect")
        if [ ! -e "$file_to_inspect" ]
        then
            touch "$file_to_inspect"
            chmod 0640 "$file_to_inspect"
        fi
    fi

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod o-rwx ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi
    unset syscall_a
    unset syscall_grouping
    unset syscall_string
    unset syscall
    unset file_to_edit
    unset rule_to_edit
    unset rule_syscalls_to_edit
    unset other_string
    unset auid_string
    unset full_rule

    # Load macro arguments into arrays
    read -a syscall_a <<< $SYSCALL
    read -a syscall_grouping <<< $SYSCALL_GROUPING

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    #  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
    # -----------------------------------------------------------------------------------------
    #        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
    # -----------------------------------------------------------------------------------------
    #        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
    #        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    #
    files_to_inspect=()

    # If audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # file to the list of files to be inspected
    default_file="/etc/audit/audit.rules"
    files_to_inspect+=('/etc/audit/audit.rules' )

    # After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
    skip=1

    for audit_file in "${files_to_inspect[@]}"
    do
        # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
        # i.e, collect rules that match:
        # * the action, list and arch, (2-nd argument)
        # * the other filters, (3-rd argument)
        # * the auid filters, (4-rd argument)
        readarray -t similar_rules < <(sed -e "/^$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

        candidate_rules=()
        # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
        for s_rule in "${similar_rules[@]}"
        do
            # Strip all the options and fields we know of,
            # than check if there was any field left over
            extra_fields=$(sed -E -e "s/^$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
            grep -q -- "-F" <<< "$extra_fields" || candidate_rules+=("$s_rule")
        done

        if [[ ${#syscall_a[@]} -ge 1 ]]
        then
            # Check if the syscall we want is present in any of the similar existing rules
            for rule in "${candidate_rules[@]}"
            do
                rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
                all_syscalls_found=0
                for syscall in "${syscall_a[@]}"
                do
                    grep -q -- "\b${syscall}\b" <<< "$rule_syscalls" || {
                       # A syscall was not found in the candidate rule
                       all_syscalls_found=1
                       }
                done
                if [[ $all_syscalls_found -eq 0 ]]
                then
                    # We found a rule with all the syscall(s) we want; skip rest of macro
                    skip=0
                    break
                fi

                # Check if this rule can be grouped with our target syscall and keep track of it
                for syscall_g in "${syscall_grouping[@]}"
                do
                    if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                    then
                        file_to_edit=${audit_file}
                        rule_to_edit=${rule}
                        rule_syscalls_to_edit=${rule_syscalls}
                    fi
                done
            done
        else
            # If there is any candidate rule, it is compliant; skip rest of macro
            if [ "${#candidate_rules[@]}" -gt 0 ]
            then
                skip=0
            fi
        fi

        if [ "$skip" -eq 0 ]; then
            break
        fi
    done

    if [ "$skip" -ne 0 ]; then
        # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
        # At this point we know if we need to either append the $full_rule or group
        # the syscall together with an exsiting rule

        # Append the full_rule if it cannot be grouped to any other rule
        if [ -z ${rule_to_edit+x} ]
        then
            # Build full_rule while avoid adding double spaces when other_filters is empty
            if [ "${#syscall_a[@]}" -gt 0 ]
            then
                syscall_string=""
                for syscall in "${syscall_a[@]}"
                do
                    syscall_string+=" -S $syscall"
                done
            fi
            other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS") || /bin/true
            auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS") || /bin/true
            full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY" || /bin/true
            echo "$full_rule" >> "$default_file"
            chmod o-rwx ${default_file}
        else
            # Check if the syscalls are declared as a comma separated list or
            # as multiple -S parameters
            if grep -q -- "," <<< "${rule_syscalls_to_edit}"
            then
                delimiter=","
            else
                delimiter=" -S "
            fi
            new_grouped_syscalls="${rule_syscalls_to_edit}"
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "${rule_syscalls_to_edit}" || {
                   # A syscall was not found in the candidate rule
                   new_grouped_syscalls+="${delimiter}${syscall}"
                   }
            done

            # Group the syscall in the rule
            sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
        fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
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_execution_seunshare
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Perform remediation of Audit rules for /usr/sbin/seunshare
      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/seunshare -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: '*.rules'
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Reset syscalls found per file
        set_fact:
          syscalls_per_file: {}
          found_paths_dict: {}

      - name: Declare syscalls found per file
        set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
          :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
        loop: '{{ find_command.results | selectattr(''matched'') | list }}'

      - name: Declare files where syscalls were found
        set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
          | map(attribute='path') | list }}"

      - name: Count occurrences of syscalls in paths
        set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
          0) }) }}"
        loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
          | list }}'

      - name: Get path with most syscalls
        set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
          | last).key }}"
        when: found_paths | length >= 1

      - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
        set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
        when: found_paths | length == 0

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

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

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

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

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

      - name: Check existence of  in /etc/audit/audit.rules
        find:
          paths: /etc/audit
          contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)* -F
            path=/usr/sbin/seunshare -F perm=x -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
          patterns: audit.rules
        register: find_command
        loop: '{{ (syscall_grouping + syscalls) | unique }}'

      - name: Set path to /etc/audit/audit.rules
        set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

      - name: Add the audit rule to {{ audit_file }}
        lineinfile:
          path: '{{ audit_file }}'
          line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/seunshare -F
            perm=x -F auid>=1000 -F auid!=unset -F key=privileged
          create: true
          mode: o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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_execution_seunshare
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

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 [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit && { ! ( grep -q aarch64 /proc/sys/kernel/osrelease ); }; 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, 4.1.15

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && 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="-F auid>=1000 -F auid!=unset"

            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: b64
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - 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 32bit 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+)* -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/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+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
        when: syscalls_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=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+)* -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=modules
          create: true
          mode: o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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 64bit 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+)* -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/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+)+)( -F auid>=1000 -F auid!=unset (?:-k
            |-F key=)\w+)
          line: \1\2\3{{ missing_syscalls | join("\3") }}\4
          backrefs: true
          state: present
        when: syscalls_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=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+)* -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=modules
          create: true
          mode: o-rwx
          state: present
        when: syscalls_found | length == 0
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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 Attempts to Alter Logon and Logout Events Group contains 4 rules

[ref] The audit system already collects login information for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d` in order to watch for attempted manual edits of files involved in storing logon events:

    -w /var/log/tallylog -p wa -k logins
    -w /var/log/faillock -p wa -k logins
    -w /var/log/lastlog -p wa -k logins

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules` file in order to watch for unattempted manual edits of files involved in storing logon events:

    -w /var/log/tallylog -p wa -k logins
    -w /var/log/faillock -p wa -k logins
    -w /var/log/lastlog -p wa -k logins

#### Rule Record Attempts to Alter Logon and Logout Events [ref]

The audit system already collects login information for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d` in order to watch for attempted manual edits of files involved in storing logon events:

    -w /var/log/tallylog -p wa -k logins
    -w /var/log/faillock -p wa -k logins
    -w /var/log/lastlog -p wa -k logins

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules` file in order to watch for unattempted manual edits of files involved in storing logon events:

    -w /var/log/tallylog -p wa -k logins
    -w /var/log/faillock -p wa -k logins
    -w /var/log/lastlog -p wa -k logins

Warning: This rule checks for multiple syscalls related to login events; 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_login_events_tallylog`
- `audit_rules_login_events_faillock`
- `audit_rules_login_events_lastlog`

Rationale: | Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_login_events

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, 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.3

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

    var_accounts_passwords_pam_faillock_dir='/var/log/faillock'

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+/var/log/tallylog" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/tallylog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+/var/log/tallylog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/tallylog -p wa -k logins" >> "$audit_rules_file"
        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/logins.rules' to list of files for inspection.
    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/tallylog" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/logins.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/logins.rules"
        # If the logins.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0640 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+/var/log/tallylog" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/tallylog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+/var/log/tallylog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/tallylog -p wa -k logins" >> "$audit_rules_file"
        fi
    done

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+${var_accounts_passwords_pam_faillock_dir}" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir} $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir}$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w ${var_accounts_passwords_pam_faillock_dir} -p wa -k logins" >> "$audit_rules_file"
        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/logins.rules' to list of files for inspection.
    readarray -t matches < <(grep -HP "[\s]*-w[\s]+${var_accounts_passwords_pam_faillock_dir}" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/logins.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/logins.rules"
        # If the logins.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0640 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+${var_accounts_passwords_pam_faillock_dir}" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir} $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir}$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w ${var_accounts_passwords_pam_faillock_dir} -p wa -k logins" >> "$audit_rules_file"
        fi
    done

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+/var/log/lastlog" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/lastlog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+/var/log/lastlog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/lastlog -p wa -k logins" >> "$audit_rules_file"
        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/logins.rules' to list of files for inspection.
    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/lastlog" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/logins.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/logins.rules"
        # If the logins.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0640 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+/var/log/lastlog" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/lastlog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+/var/log/lastlog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/lastlog -p wa -k logins" >> "$audit_rules_file"
        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

#### Rule Record Attempts to Alter Logon and Logout Events - faillock [ref]

The audit system already collects login information for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d` in order to watch for attempted manual edits of files involved in storing logon events:

    -w /var/log/faillock -p wa -k logins

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules` file in order to watch for unattempted manual edits of files involved in storing logon events:

    -w /var/log/faillock -p wa -k logins

Rationale: | Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_login_events_faillock

Identifiers and References

References: BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000126, CCI-000172, CCI-002884, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.3, 10.2.1.3, SRG-OS-000392-GPOS-00172, SRG-OS-000470-GPOS-00214, SRG-OS-000473-GPOS-00218, SRG-APP-000503-CTR-001275, SRG-APP-000506-CTR-001290, 4.1.3.12, SV-230466r627750_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

    var_accounts_passwords_pam_faillock_dir='/var/log/faillock'

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+${var_accounts_passwords_pam_faillock_dir}" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir} $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir}$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w ${var_accounts_passwords_pam_faillock_dir} -p wa -k logins" >> "$audit_rules_file"
        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/logins.rules' to list of files for inspection.
    readarray -t matches < <(grep -HP "[\s]*-w[\s]+${var_accounts_passwords_pam_faillock_dir}" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/logins.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/logins.rules"
        # If the logins.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0640 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+${var_accounts_passwords_pam_faillock_dir}" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir} $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+${var_accounts_passwords_pam_faillock_dir}$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w ${var_accounts_passwords_pam_faillock_dir} -p wa -k logins" >> "$audit_rules_file"
        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-030590
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_faillock
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy
    - name: XCCDF Value var_accounts_passwords_pam_faillock_dir # promote to variable
      set_fact:
        var_accounts_passwords_pam_faillock_dir: !!str /var/log/faillock
      tags:
        - always

    - name: Check if watch rule for {{ var_accounts_passwords_pam_faillock_dir }} already
        exists in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+{{ var_accounts_passwords_pam_faillock_dir }}\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-030590
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_faillock
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key logins
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)logins$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - DISA-STIG-RHEL-08-030590
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_faillock
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Use /etc/audit/rules.d/logins.rules as the recipient for the rule
      set_fact:
        all_files:
        - /etc/audit/rules.d/logins.rules
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - DISA-STIG-RHEL-08-030590
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_faillock
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - DISA-STIG-RHEL-08-030590
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_faillock
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Add watch rule for {{ var_accounts_passwords_pam_faillock_dir }} in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w {{ var_accounts_passwords_pam_faillock_dir }} -p wa -k logins
        create: true
        mode: '0640'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - DISA-STIG-RHEL-08-030590
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_faillock
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Check if watch rule for {{ var_accounts_passwords_pam_faillock_dir }} already
        exists in /etc/audit/audit.rules
      find:
        paths: /etc/audit/
        contains: ^\s*-w\s+{{ var_accounts_passwords_pam_faillock_dir }}\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-030590
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_faillock
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Add watch rule for {{ var_accounts_passwords_pam_faillock_dir }} in /etc/audit/audit.rules
      lineinfile:
        line: -w {{ var_accounts_passwords_pam_faillock_dir }} -p wa -k logins
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - DISA-STIG-RHEL-08-030590
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_faillock
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

#### Rule Record Attempts to Alter Logon and Logout Events - lastlog [ref]

The audit system already collects login information for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d` in order to watch for attempted manual edits of files involved in storing logon events:

    -w /var/log/lastlog -p wa -k logins

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules` file in order to watch for unattempted manual edits of files involved in storing logon events:

    -w /var/log/lastlog -p wa -k logins

Rationale: | Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_login_events_lastlog

Identifiers and References

References: BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000126, CCI-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.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.3, 10.2.1.3, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000471-GPOS-00215, SRG-OS-000473-GPOS-00218, SRG-OS-000470-GPOS-00214, SRG-APP-000495-CTR-001235, SRG-APP-000503-CTR-001275, SRG-APP-000506-CTR-001290, 4.1.3.12, SV-230467r627750_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+/var/log/lastlog" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/lastlog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+/var/log/lastlog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/lastlog -p wa -k logins" >> "$audit_rules_file"
        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/logins.rules' to list of files for inspection.
    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/lastlog" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/logins.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/logins.rules"
        # If the logins.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0640 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+/var/log/lastlog" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/lastlog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+/var/log/lastlog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/lastlog -p wa -k logins" >> "$audit_rules_file"
        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-030600
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_lastlog
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Check if watch rule for /var/log/lastlog already exists in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/var/log/lastlog\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-030600
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_lastlog
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key logins
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)logins$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - DISA-STIG-RHEL-08-030600
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_lastlog
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Use /etc/audit/rules.d/logins.rules as the recipient for the rule
      set_fact:
        all_files:
        - /etc/audit/rules.d/logins.rules
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - DISA-STIG-RHEL-08-030600
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_lastlog
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - DISA-STIG-RHEL-08-030600
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_lastlog
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Add watch rule for /var/log/lastlog in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /var/log/lastlog -p wa -k logins
        create: true
        mode: '0640'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - DISA-STIG-RHEL-08-030600
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_lastlog
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Check if watch rule for /var/log/lastlog already exists in /etc/audit/audit.rules
      find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/var/log/lastlog\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-030600
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_lastlog
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Add watch rule for /var/log/lastlog in /etc/audit/audit.rules
      lineinfile:
        line: -w /var/log/lastlog -p wa -k logins
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - DISA-STIG-RHEL-08-030600
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_lastlog
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

#### Rule Record Attempts to Alter Logon and Logout Events - tallylog [ref]

The audit system already collects login information for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d` in order to watch for attempted manual edits of files involved in storing logon events:

    -w /var/log/tallylog -p wa -k logins

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules` file in order to watch for unattempted manual edits of files involved in storing logon events:

    -w /var/log/tallylog -p wa -k logins

Rationale: | Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_login_events_tallylog

Identifiers and References

References: 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, CCI-000172, CCI-002884, CCI-000126, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), AC-6(9), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.3, 10.2.1.3, SRG-OS-000392-GPOS-00172, SRG-OS-000470-GPOS-00214, SRG-OS-000473-GPOS-00218, SRG-APP-000503-CTR-001275

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+/var/log/tallylog" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/tallylog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+/var/log/tallylog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/tallylog -p wa -k logins" >> "$audit_rules_file"
        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/logins.rules' to list of files for inspection.
    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/tallylog" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/logins.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/logins.rules"
        # If the logins.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0640 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+/var/log/tallylog" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/tallylog $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+/var/log/tallylog$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/tallylog -p wa -k logins" >> "$audit_rules_file"
        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_tallylog
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Check if watch rule for /var/log/tallylog already exists in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/var/log/tallylog\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_tallylog
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key logins
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)logins$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      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)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_tallylog
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Use /etc/audit/rules.d/logins.rules as the recipient for the rule
      set_fact:
        all_files:
        - /etc/audit/rules.d/logins.rules
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_tallylog
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_tallylog
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Add watch rule for /var/log/tallylog in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /var/log/tallylog -p wa -k logins
        create: true
        mode: '0640'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      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)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_tallylog
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Check if watch rule for /var/log/tallylog already exists in /etc/audit/audit.rules
      find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/var/log/tallylog\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_tallylog
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Add watch rule for /var/log/tallylog in /etc/audit/audit.rules
      lineinfile:
        line: -w /var/log/tallylog -p wa -k logins
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      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)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_login_events_tallylog
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

Group Record Information on the Use of Privileged Commands Group contains 1 rule
[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. These are commands with suid or sgid bits on and they are specially risky in local block device partitions not mounted with noexec and nosuid options. Therefore, these partitions should be first identified by the following command:

    findmnt -n -l -k -it $(awk '/nodev/ { print $2 }' /proc/filesystems | paste -sd,) | grep -Pv "noexec|nosuid"

For all partitions listed by the previous command, it is necessary to search for setuid / setgid programs using the following command:

    $ sudo find PARTITION -xdev -perm /6000 -type f 2>/dev/null

For each setuid / setgid program identified by the previous command, an audit rule must be present in the appropriate place using the following line structure:

    -a always,exit -F path=PROG_PATH -F perm=x -F auid>=1000 -F auid!=unset -F key=privileged

If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup, add the line to a file with suffix `.rules` in the `/etc/audit/rules.d` directory, replacing the PROG_PATH part with the full path of that setuid / setgid identified program. If the `auditd` daemon is configured to use the `auditctl` utility instead, add the line to the `/etc/audit/audit.rules` file, also replacing the PROG_PATH part with the full path of that setuid / setgid identified program.
Warning: This rule checks for multiple syscalls related to privileged commands. If needed to check specific privileged commands, other more specific rules should be considered. For example:

- `audit_rules_privileged_commands_su`
- `audit_rules_privileged_commands_umount`
- `audit_rules_privileged_commands_passwd`

Warning: Note that OVAL check and Bash / Ansible remediation of this rule explicitly excludes file systems mounted at

    /proc

directory and its subdirectories. It is a virtual file system and it doesn't contain executable applications. At the same time, interacting with this file system during check or remediation caused undesirable errors.

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 that 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: BP28(R73), 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, 4.1.3.6

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    ACTION_ARCH_FILTERS="-a always,exit"
    AUID_FILTERS="-F auid>=1000 -F auid!=unset"
    SYSCALL=""
    KEY="privileged"
    SYSCALL_GROUPING=""

    FILTER_NODEV=$(awk '/nodev/ { print $2 }' /proc/filesystems | paste -sd,)
    PARTITIONS=$(findmnt -n -l -k -it $FILTER_NODEV | grep -Pv "noexec|nosuid|/proc($|/.*$)" | awk '{ print $1 }')
    for PARTITION in $PARTITIONS; do
      PRIV_CMDS=$(find "${PARTITION}" -xdev -perm /6000 -type f 2>/dev/null)
      for PRIV_CMD in $PRIV_CMDS; do
        OTHER_FILTERS="-F path=$PRIV_CMD -F perm=x"
        # 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
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.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
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure auditd Collects Information on the Use of Privileged Commands - Set
        List of Mount Points Which Permits Execution of Privileged Commands
      ansible.builtin.set_fact:
        privileged_mount_points: '{{(ansible_facts.mounts | rejectattr(''options'', ''search'',
          ''noexec|nosuid'') | rejectattr(''mount'', ''match'', ''/proc($|/.*$)'') | map(attribute=''mount'')
          | list ) }}'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure auditd Collects Information on the Use of Privileged Commands - Search
        for Privileged Commands in Eligible Mount Points
      ansible.builtin.shell:
        cmd: find {{ item }} -xdev -perm /6000 -type f 2>/dev/null
      register: result_privileged_commands_search
      changed_when: false
      failed_when: false
      with_items: '{{ privileged_mount_points }}'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure auditd Collects Information on the Use of Privileged Commands - Set
        List of Privileged Commands Found in Eligible Mount Points
      ansible.builtin.set_fact:
        privileged_commands: '{{( result_privileged_commands_search.results | map(attribute=''stdout_lines'')
          | select() | list ) | sum(start=[]) }}'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure auditd Collects Information on the Use of Privileged Commands - Privileged
        Commands are Present in the System
      block:

      - name: Ensure auditd Collects Information on the Use of Privileged Commands - Ensure
          Rules for All Privileged Commands in augenrules Format
        ansible.builtin.lineinfile:
          path: /etc/audit/rules.d/privileged.rules
          line: -a always,exit -F path={{ item }} -F perm=x -F auid>=1000 -F auid!=unset
            -F key=privileged
          regexp: ^.*path={{ item | regex_escape() }} .*$
          create: true
        with_items:
        - '{{ privileged_commands }}'

      - name: Ensure auditd Collects Information on the Use of Privileged Commands - Ensure
          Rules for All Privileged Commands in auditctl Format
        ansible.builtin.lineinfile:
          path: /etc/audit/audit.rules
          line: -a always,exit -F path={{ item }} -F perm=x -F auid>=1000 -F auid!=unset
            -F key=privileged
          regexp: ^.*path={{ item | regex_escape() }} .*$
          create: true
        with_items:
        - '{{ privileged_commands }}'

      - name: Ensure auditd Collects Information on the Use of Privileged Commands - Search
          for Duplicated Rules in Other Files
        ansible.builtin.find:
          paths: /etc/audit/rules.d
          recurse: false
          contains: ^-a always,exit -F path={{ item }} .*$
          patterns: '*.rules'
        with_items:
        - '{{ privileged_commands }}'
        register: result_augenrules_files

      - name: Ensure auditd Collects Information on the Use of Privileged Commands - Ensure
          Rules for Privileged Commands are Defined Only in One File
        ansible.builtin.lineinfile:
          path: '{{ item.1.path }}'
          regexp: ^-a always,exit -F path={{ item.0.item }} .*$
          state: absent
        with_subelements:
        - '{{ result_augenrules_files.results }}'
        - files
        when:
        - item.1.path != '/etc/audit/rules.d/privileged.rules'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - privileged_commands is defined
      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
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

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: BP28(R73), 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, 10.6.3, 4.1.3.4

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && 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
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-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
      - PCI-DSSv4-10.6.3
      - 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: b64
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - 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
      - PCI-DSSv4-10.6.3
      - audit_rules_time_adjtimex
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Perform remediation of Audit rules for adjtimex for 32bit 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - PCI-DSSv4-10.6.3
      - audit_rules_time_adjtimex
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Perform remediation of Audit rules for adjtimex for 64bit 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.6.3
      - audit_rules_time_adjtimex
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ -a%20always%2Cexit%20-F%20arch%3Db64%20-S%20adjtimex%20-k%20audit_time_rules%0A-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20adjtimex%20-k%20audit_time_rules%0A }}
            mode: 0600
            path: /etc/audit/rules.d/75-syscall-adjtimex.rules
            overwrite: true

#### Rule Record Attempts to Alter Time Through clock_settime [ref]

If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:

    -a always,exit -F arch=b32 -S clock_settime -F a0=0x0 -F key=time-change

If the system is 64 bit then also add the following line:

    -a always,exit -F arch=b64 -S clock_settime -F a0=0x0 -F key=time-change

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:

    -a always,exit -F arch=b32 -S clock_settime -F a0=0x0 -F key=time-change

If the system is 64 bit then also add the following line:

    -a always,exit -F arch=b64 -S clock_settime -F a0=0x0 -F key=time-change

The -k option allows for the specification of a key in string form that can be used for better reporting capability through ausearch and aureport. Multiple system calls can be defined on the same line to save space if desired, but is not required. See an example of multiple combined syscalls:

    -a always,exit -F arch=b64 -S adjtimex,settimeofday -F key=audit_time_rules

Rationale: | Arbitrary changes to the system time can be used to obfuscate nefarious activities in log files, as well as to confuse network services that are highly dependent upon an accurate system time (such as sshd). All changes to the system time should be audited.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_time_clock_settime

Identifiers and References

References: BP28(R73), 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, 10.6.3, 4.1.3.4

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && 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
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-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
      - PCI-DSSv4-10.6.3
      - 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: b64
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - 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
      - PCI-DSSv4-10.6.3
      - 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 32bit 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - PCI-DSSv4-10.6.3
      - 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 64bit 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.6.3
      - audit_rules_time_clock_settime
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ -a%20always%2Cexit%20-F%20arch%3Db64%20-S%20clock_settime%20-F%20a0%3D0x0%20-k%20time-change%0A-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20clock_settime%20-F%20a0%3D0x0%20-k%20time-change%0A }}
            mode: 0600
            path: /etc/audit/rules.d/75-syscall-clock-settime.rules
            overwrite: true

#### Rule Record attempts to alter time through settimeofday [ref]

If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:

    -a always,exit -F arch=b32 -S settimeofday -F key=audit_time_rules

If the system is 64 bit then also add the following line:

    -a always,exit -F arch=b64 -S settimeofday -F key=audit_time_rules

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:

    -a always,exit -F arch=b32 -S settimeofday -F key=audit_time_rules

If the system is 64 bit then also add the following line:

    -a always,exit -F arch=b64 -S settimeofday -F key=audit_time_rules

The -k option allows for the specification of a key in string form that can be used for better reporting capability through ausearch and aureport. Multiple system calls can be defined on the same line to save space if desired, but is not required. See an example of multiple combined syscalls:

    -a always,exit -F arch=b64 -S adjtimex,settimeofday -F key=audit_time_rules

Rationale: | Arbitrary changes to the system time can be used to obfuscate nefarious activities in log files, as well as to confuse network services that are highly dependent upon an accurate system time (such as sshd). All changes to the system time should be audited.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_time_settimeofday

Identifiers and References

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, 10.6.3, 4.1.3.4

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && 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
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-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
      - PCI-DSSv4-10.6.3
      - 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: b64
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - 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
      - PCI-DSSv4-10.6.3
      - audit_rules_time_settimeofday
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Perform remediation of Audit rules for settimeofday for 32bit 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - PCI-DSSv4-10.6.3
      - audit_rules_time_settimeofday
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Perform remediation of Audit rules for settimeofday for 64bit 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.6.3
      - audit_rules_time_settimeofday
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ -a%20always%2Cexit%20-F%20arch%3Db64%20-S%20settimeofday%20-k%20audit_time_rules%0A-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20settimeofday%20-k%20audit_time_rules%0A }}
            mode: 0600
            path: /etc/audit/rules.d/75-syscall-settimeofday.rules
            overwrite: true

#### Rule Record Attempts to Alter Time Through stime [ref]

If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d` for both 32 bit and 64 bit systems:

    -a always,exit -F arch=b32 -S stime -F key=audit_time_rules

Since the 64 bit version of the "stime" system call is not defined in the audit lookup table, the corresponding "-F arch=b64" form of this rule is not expected to be defined on 64 bit systems (the aforementioned "-F arch=b32" stime rule form itself is sufficient for both 32 bit and 64 bit systems). If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file for both 32 bit and 64 bit systems:

    -a always,exit -F arch=b32 -S stime -F key=audit_time_rules

Since the 64 bit version of the "stime" system call is not defined in the audit lookup table, the corresponding "-F arch=b64" form of this rule is not expected to be defined on 64 bit systems (the aforementioned "-F arch=b32" stime rule form itself is sufficient for both 32 bit and 64 bit systems). The -k option allows for the specification of a key in string form that can be used for better reporting capability through ausearch and aureport. Multiple system calls can be defined on the same line to save space if desired, but is not required. See an example of multiple combined system calls:

    -a always,exit -F arch=b64 -S adjtimex,settimeofday -F key=audit_time_rules

Rationale: | Arbitrary changes to the system time can be used to obfuscate nefarious activities in log files, as well as to confuse network services that are highly dependent upon an accurate system time (such as sshd). All changes to the system time should be audited.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_time_stime

Identifiers and References

References: BP28(R73), 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, 10.6.3, 4.1.3.4

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit && { ( ! ( grep -q aarch64 /proc/sys/kernel/osrelease ) && ! ( grep -q s390x /proc/sys/kernel/osrelease ) ); }; 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
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-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
      - PCI-DSSv4-10.6.3
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - ( not ( ansible_architecture == "aarch64" ) and not ( ansible_architecture ==
        "s390x" ) )
      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
      - PCI-DSSv4-10.6.3
      - audit_rules_time_stime
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ -a%20always%2Cexit%20-F%20arch%3Db64%20-S%20stime%20-k%20audit_time_rules%0A-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20stime%20-k%20audit_time_rules%0A }}
            mode: 0600
            path: /etc/audit/rules.d/75-syscall-stime.rules
            overwrite: true

#### Rule Record Attempts to Alter the localtime File [ref]

If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:

    -w /etc/localtime -p wa -k audit_time_rules

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:

    -w /etc/localtime -p wa -k audit_time_rules

The -k option allows for the specification of a key in string form that can be used for better reporting capability through ausearch and aureport and should always be used.

Rationale: | Arbitrary changes to the system time can be used to obfuscate nefarious activities in log files, as well as to confuse network services that are highly dependent upon an accurate system time (such as sshd). All changes to the system time should be audited.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_time_watch_localtime

Identifiers and References

References: BP28(R73), 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, 10.6.3, 10.6.3, 4.1.3.4

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && 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
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-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
      - PCI-DSSv4-10.6.3
      - PCI-DSSv4-10.6.3
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - PCI-DSSv4-10.6.3
      - PCI-DSSv4-10.6.3
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.6.3
      - PCI-DSSv4-10.6.3
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.6.3
      - PCI-DSSv4-10.6.3
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.6.3
      - PCI-DSSv4-10.6.3
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.6.3
      - PCI-DSSv4-10.6.3
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - PCI-DSSv4-10.6.3
      - PCI-DSSv4-10.6.3
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.6.3
      - PCI-DSSv4-10.6.3
      - audit_rules_time_watch_localtime
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ -w%20/etc/localtime%20-p%20wa%20-k%20audit_time_rules%0A }}
            mode: 0600
            path: /etc/audit/rules.d/75-etclocaltime-wa-audit_time_rules.rules
            overwrite: true

#### Rule 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: BP28(R73), 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, 10.3.4, 4.1.3.5

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && 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
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-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
      - PCI-DSSv4-10.3.4
      - 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: b64
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - ansible_architecture == "aarch64" or ansible_architecture == "ppc64" or ansible_architecture
        == "ppc64le" or ansible_architecture == "s390x" or ansible_architecture == "x86_64"
      tags:
      - CJIS-5.4.1.1
      - 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
      - PCI-DSSv4-10.3.4
      - audit_rules_networkconfig_modification
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Remediate audit rules for network configuration for 32bit platform
      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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - PCI-DSSv4-10.3.4
      - audit_rules_networkconfig_modification
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Remediate audit rules for network configuration for 64bit platform
      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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      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
      - PCI-DSSv4-10.3.4
      - 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'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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
      - PCI-DSSv4-10.3.4
      - audit_rules_networkconfig_modification
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Record Attempts to Alter Process and Session Initiation Information [ref]

The audit system already collects process information for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following lines to a file with suffix `.rules` in the directory `/etc/audit/rules.d` in order to watch for attempted manual edits of files involved in storing such process information:

    -w /var/run/utmp -p wa -k session
    -w /var/log/btmp -p wa -k session
    -w /var/log/wtmp -p wa -k session

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following lines to `/etc/audit/audit.rules` file in order to watch for attempted manual edits of files involved in storing such process information:

    -w /var/run/utmp -p wa -k session
    -w /var/log/btmp -p wa -k session
    -w /var/log/wtmp -p wa -k session

Rationale: | Manual editing of these files may indicate nefarious activity, such as an attacker attempting to remove evidence of an intrusion.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_session_events

Identifiers and References

References: BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.1.7, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, 0582, 0584, 05885, 0586, 0846, 0957, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, AU-2(d), AU-12(c), CM-6(a), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1.1.c, Req-10.2.3, 10.2.1.3, SRG-APP-000505-CTR-001285, 4.1.3.11

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    # Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+/var/run/utmp" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/run/utmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+/var/run/utmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/run/utmp -p wa -k session" >> "$audit_rules_file"
        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/session.rules' to list of files for inspection.
    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/run/utmp" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/session.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/session.rules"
        # If the session.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0640 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+/var/run/utmp" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/run/utmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+/var/run/utmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/run/utmp -p wa -k session" >> "$audit_rules_file"
        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+/var/log/btmp" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/btmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+/var/log/btmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/btmp -p wa -k session" >> "$audit_rules_file"
        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/session.rules' to list of files for inspection.
    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/btmp" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/session.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/session.rules"
        # If the session.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0640 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+/var/log/btmp" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/btmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+/var/log/btmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/btmp -p wa -k session" >> "$audit_rules_file"
        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
    # into the list of files to be inspected
    files_to_inspect+=('/etc/audit/audit.rules')

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+/var/log/wtmp" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/wtmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+/var/log/wtmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/wtmp -p wa -k session" >> "$audit_rules_file"
        fi
    done
    # Create a list of audit *.rules files that should be inspected for presence and correctness
    # of a particular audit rule. The scheme is as follows:
    #
    # -----------------------------------------------------------------------------------------
    # Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
    # -----------------------------------------------------------------------------------------
    #	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
    # -----------------------------------------------------------------------------------------
    # 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
    # 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
    # -----------------------------------------------------------------------------------------
    files_to_inspect=()

    # If the audit is 'augenrules', then check if rule is already defined
    # If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
    # If rule isn't defined, add '/etc/audit/rules.d/session.rules' to list of files for inspection.
    readarray -t matches < <(grep -HP "[\s]*-w[\s]+/var/log/wtmp" /etc/audit/rules.d/*.rules)

    # For each of the matched entries
    for match in "${matches[@]}"
    do
        # Extract filepath from the match
        rulesd_audit_file=$(echo $match | cut -f1 -d ':')
        # Append that path into list of files for inspection
        files_to_inspect+=("$rulesd_audit_file")
    done
    # Case when particular audit rule isn't defined yet
    if [ "${#files_to_inspect[@]}" -eq "0" ]
    then
        # Append '/etc/audit/rules.d/session.rules' into list of files for inspection
        key_rule_file="/etc/audit/rules.d/session.rules"
        # If the session.rules file doesn't exist yet, create it with correct permissions
        if [ ! -e "$key_rule_file" ]
        then
            touch "$key_rule_file"
            chmod 0640 "$key_rule_file"
        fi
        files_to_inspect+=("$key_rule_file")
    fi

    # Finally perform the inspection and possible subsequent audit rule
    # correction for each of the files previously identified for inspection
    for audit_rules_file in "${files_to_inspect[@]}"
    do
        # Check if audit watch file system object rule for given path already present
        if grep -q -P -- "^[\s]*-w[\s]+/var/log/wtmp" "$audit_rules_file"
        then
            # Rule is found => verify yet if existing rule definition contains
            # all of the required access type bits

            # Define BRE whitespace class shortcut
            sp="[[:space:]]"
            # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
            current_access_bits=$(sed -ne "s#$sp*-w$sp\+/var/log/wtmp $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
            # Split required access bits string into characters array
            # (to check bit's presence for one bit at a time)
            for access_bit in $(echo "wa" | grep -o .)
            do
                # For each from the required access bits (e.g. 'w', 'a') check
                # if they are already present in current access bits for rule.
                # If not, append that bit at the end
                if ! grep -q "$access_bit" <<< "$current_access_bits"
                then
                    # Concatenate the existing mask with the missing bit
                    current_access_bits="$current_access_bits$access_bit"
                fi
            done
            # Propagate the updated rule's access bits (original + the required
            # ones) back into the /etc/audit/audit.rules file for that rule
            sed -i "s#\($sp*-w$sp\+/var/log/wtmp$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
        else
            # Rule isn't present yet. Append it at the end of $audit_rules_file file
            # with proper key

            echo "-w /var/log/wtmp -p wa -k session" >> "$audit_rules_file"
        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Check if watch rule for /var/run/utmp already exists in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/var/run/utmp\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key session
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)session$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Use /etc/audit/rules.d/session.rules as the recipient for the rule
      set_fact:
        all_files:
        - /etc/audit/rules.d/session.rules
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Add watch rule for /var/run/utmp in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /var/run/utmp -p wa -k session
        create: true
        mode: '0640'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Check if watch rule for /var/run/utmp already exists in /etc/audit/audit.rules
      find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/var/run/utmp\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Add watch rule for /var/run/utmp in /etc/audit/audit.rules
      lineinfile:
        line: -w /var/run/utmp -p wa -k session
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Check if watch rule for /var/log/btmp already exists in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/var/log/btmp\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key session
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)session$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Use /etc/audit/rules.d/session.rules as the recipient for the rule
      set_fact:
        all_files:
        - /etc/audit/rules.d/session.rules
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Add watch rule for /var/log/btmp in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /var/log/btmp -p wa -k session
        create: true
        mode: '0640'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Check if watch rule for /var/log/btmp already exists in /etc/audit/audit.rules
      find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/var/log/btmp\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Add watch rule for /var/log/btmp in /etc/audit/audit.rules
      lineinfile:
        line: -w /var/log/btmp -p wa -k session
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Check if watch rule for /var/log/wtmp already exists in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/var/log/wtmp\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key session
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)session$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Use /etc/audit/rules.d/session.rules as the recipient for the rule
      set_fact:
        all_files:
        - /etc/audit/rules.d/session.rules
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Use matched file as the recipient for the rule
      set_fact:
        all_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Add watch rule for /var/log/wtmp in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /var/log/wtmp -p wa -k session
        create: true
        mode: '0640'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Check if watch rule for /var/log/wtmp already exists in /etc/audit/audit.rules
      find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/var/log/wtmp\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

    - name: Add watch rule for /var/log/wtmp in /etc/audit/audit.rules
      lineinfile:
        line: -w /var/log/wtmp -p wa -k session
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - 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-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.3
      - PCI-DSSv4-10.2.1.3
      - audit_rules_session_events
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    ---

    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ %0A-w%20/var/run/utmp%20-p%20wa%20-k%20session%0A-w%20/var/log/btmp%20-p%20wa%20-k%20session%0A-w%20/var/log/wtmp%20-p%20wa%20-k%20session%0A }}
            mode: 0600
            path: /etc/audit/rules.d/75-audit-session-events.rules
            overwrite: true

#### Rule Ensure auditd Collects System Administrator Actions [ref]

At a minimum, the audit system should collect administrator actions for all users and root. If the `auditd` daemon is configured to use the `augenrules` program to read audit rules during daemon startup (the default), add the following line to a file with suffix `.rules` in the directory `/etc/audit/rules.d`:

    -w /etc/sudoers -p wa -k actions
    -w /etc/sudoers.d/ -p wa -k actions

If the `auditd` daemon is configured to use the `auditctl` utility to read audit rules during daemon startup, add the following line to `/etc/audit/audit.rules` file:

    -w /etc/sudoers -p wa -k actions
    -w /etc/sudoers.d/ -p wa -k actions

Rationale: | The actions taken by system administrators should be audited to keep a record of what was executed on the system, as well as, for accountability purposes.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_rules_sysadmin_actions

Identifiers and References

References: BP28(R73), 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, 10.2.1.5, SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000304-GPOS-00121, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221, SRG-APP-000026-CTR-000070, SRG-APP-000027-CTR-000075, SRG-APP-000028-CTR-000080, SRG-APP-000291-CTR-000675, SRG-APP-000292-CTR-000680, SRG-APP-000293-CTR-000685, SRG-APP-000294-CTR-000690, SRG-APP-000319-CTR-000745, SRG-APP-000320-CTR-000750, SRG-APP-000509-CTR-001305, 4.1.3.1

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && 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
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Check if watch rule for /etc/sudoers already exists in /etc/audit/audit.rules
      find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/etc/sudoers\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Add watch rule for /etc/sudoers in /etc/audit/audit.rules
      lineinfile:
        line: -w /etc/sudoers -p wa -k actions
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Check if watch rule for /etc/sudoers already exists in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/etc/sudoers\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key actions
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)actions$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2.1.5
      - 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_files:
        - /etc/audit/rules.d/actions.rules
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2.1.5
      - 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_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Add watch rule for /etc/sudoers in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/sudoers -p wa -k actions
        create: true
        mode: '0640'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Check if watch rule for /etc/sudoers.d/ already exists in /etc/audit/audit.rules
      find:
        paths: /etc/audit/
        contains: ^\s*-w\s+/etc/sudoers.d/\s+-p\s+wa(\s|$)+
        patterns: audit.rules
      register: find_existing_watch_audit_rules
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Add watch rule for /etc/sudoers.d/ in /etc/audit/audit.rules
      lineinfile:
        line: -w /etc/sudoers.d/ -p wa -k actions
        state: present
        dest: /etc/audit/audit.rules
        create: true
        mode: '0640'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_existing_watch_audit_rules.matched is defined and find_existing_watch_audit_rules.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Check if watch rule for /etc/sudoers.d/ already exists in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: ^\s*-w\s+/etc/sudoers.d/\s+-p\s+wa(\s|$)+
        patterns: '*.rules'
      register: find_existing_watch_rules_d
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Search /etc/audit/rules.d for other rules with specified key actions
      find:
        paths: /etc/audit/rules.d
        contains: ^.*(?:-F key=|-k\s+)actions$
        patterns: '*.rules'
      register: find_watch_key
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2.1.5
      - 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_files:
        - /etc/audit/rules.d/actions.rules
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_watch_key.matched is defined and find_watch_key.matched == 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2.1.5
      - 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_files:
        - '{{ find_watch_key.files | map(attribute=''path'') | list | first }}'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_watch_key.matched is defined and find_watch_key.matched > 0 and find_existing_watch_rules_d.matched
        is defined and find_existing_watch_rules_d.matched == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Add watch rule for /etc/sudoers.d/ in /etc/audit/rules.d/
      lineinfile:
        path: '{{ all_files[0] }}'
        line: -w /etc/sudoers.d/ -p wa -k actions
        create: true
        mode: '0640'
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - find_existing_watch_rules_d.matched is defined and find_existing_watch_rules_d.matched
        == 0
      tags:
      - CJIS-5.4.1.1
      - NIST-800-171-3.1.7
      - NIST-800-53-AC-2(7)(b)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.2.2
      - PCI-DSS-Req-10.2.5.b
      - PCI-DSSv4-10.2.1.5
      - audit_rules_sysadmin_actions
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ -w%20/etc/sudoers.d/%20-p%20wa%20-k%20actions%0A-w%20/etc/sudoers%20-p%20wa%20-k%20actions%0A }}
            mode: 0600
            path: /etc/audit/rules.d/75-audit-sysadmin-actions.rules
            overwrite: true

#### Rule 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, SRG-APP-000026-CTR-000070, SRG-APP-000027-CTR-000075, SRG-APP-000028-CTR-000080, SRG-APP-000291-CTR-000675, SRG-APP-000292-CTR-000680, SRG-APP-000293-CTR-000685, SRG-APP-000294-CTR-000690, SRG-APP-000319-CTR-000745, SRG-APP-000320-CTR-000750, SRG-APP-000509-CTR-001305

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && 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 auditd Data Retention Group contains 6 rules

[ref] The audit system writes data to `/var/log/audit/audit.log`. By default, `auditd` rotates 5 logs by size (6MB), retaining a maximum of 30MB of data in total, and refuses to write entries when the disk is too full. This minimizes the risk of audit data filling its partition and impacting other services. This also minimizes the risk of the audit daemon temporarily disabling the system if it cannot write audit log (which it can be configured to do). For a busy system or a system which is thoroughly auditing system activity, the default settings for data retention may be insufficient. The log file size needed will depend heavily on what types of events are being audited. First configure auditing to log all the events of interest. Then monitor the log size manually for awhile to determine what file size will allow you to keep the required data for the correct time period.
Using a dedicated partition for `/var/log/audit` prevents the `auditd` logs from disrupting system functionality if they fill, and, more importantly, prevents other activity in `/var` from filling the partition and stopping the audit trail. (The audit logs are size-limited and therefore unlikely to grow without bound unless configured to do so.) Some machines may have requirements that no actions occur which cannot be audited. If this is the case, then `auditd` can be configured to halt the machine if it runs out of space. Note: Since older logs are rotated, configuring `auditd` this way does not prevent older logs from being rotated away before they can be viewed. If your system is configured to halt when logging cannot be performed, make sure this can never happen under normal circumstances! Ensure that `/var/log/audit` is on its own partition, and that this partition is larger than the maximum amount of data `auditd` will retain normally.

#### Rule Configure auditd flush priority [ref]

The `auditd` service can be configured to synchronously write audit event data to disk. Add or correct the following line in `/etc/audit/auditd.conf` to ensure that audit event data is fully synchronized with the log files on the disk:

    flush = incremental_async

Rationale: | Audit data should be synchronously written to disk to ensure log integrity. These parameters assure that all audit event data is fully synchronized with the log files on the disk.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_auditd_data_retention_flush

Identifiers and References

References: 1, 12, 13, 14, 15, 16, 2, 3, 5, 6, 7, 8, 9, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS01.03, DSS03.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.3.1, CCI-001576, 164.308(a)(1)(ii)(D), 164.308(a)(3)(ii)(A), 164.308(a)(5)(ii)(C), 164.312(a)(2)(i), 164.312(b), 164.312(d), 164.312(e), 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 6.1, SR 6.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.14.2.7, A.15.2.1, A.15.2.2, CIP-004-6 R2.2.3, CIP-004-6 R3.3, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3, CIP-007-3 R6.5, AU-11, CM-6(a), DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.PT-1, SRG-OS-000480-GPOS-00227

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    var_auditd_flush='incremental_async'

    AUDITCONFIG=/etc/audit/auditd.conf

    # if flush is present, flush param edited to var_auditd_flush
    # else flush param is defined by var_auditd_flush
    #
    # the freq param is only used for values 'incremental' and 'incremental_async' and will be
    # commented out if flush != incremental or flush != incremental_async
    #
    # if flush == incremental or flush == incremental_async && freq param is not defined, it
    # will be defined as the package-default value of 20

    grep -q ^flush $AUDITCONFIG && \
      sed -i 's/^flush.*/flush = '"$var_auditd_flush"'/g' $AUDITCONFIG
    if ! [ $? -eq 0 ]; then
      echo "flush = $var_auditd_flush" >> $AUDITCONFIG
    fi

    if ! [ "$var_auditd_flush" == "incremental" ] && ! [ "$var_auditd_flush" == "incremental_async" ]; then
      sed -i 's/^freq/##freq/g' $AUDITCONFIG
    elif [ "$var_auditd_flush" == "incremental" ] || [ "$var_auditd_flush" == "incremental_async" ]; then
      grep -q freq $AUDITCONFIG && \
        sed -i 's/^#\+freq/freq/g' $AUDITCONFIG
      if ! [ $? -eq 0 ]; then
        echo "freq = 20" >> $AUDITCONFIG
      fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-171-3.3.1
      - NIST-800-53-AU-11
      - NIST-800-53-CM-6(a)
      - auditd_data_retention_flush
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_auditd_flush # promote to variable
      set_fact:
        var_auditd_flush: !!str incremental_async
      tags:
        - always

    - name: Configure auditd Flush Priority
      lineinfile:
        dest: /etc/audit/auditd.conf
        regexp: ^\s*flush\s*=\s*.*$
        line: flush = {{ var_auditd_flush }}
        state: present
        create: true
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-171-3.3.1
      - NIST-800-53-AU-11
      - NIST-800-53-CM-6(a)
      - auditd_data_retention_flush
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }}
            mode: 0640
            path: /etc/audit/auditd.conf
            overwrite: true

#### Rule Set number of records to cause an explicit flush to audit logs [ref]

To configure Audit daemon to issue an explicit flush to disk command after writing 50 records, set `freq` to `50` in `/etc/audit/auditd.conf`.
Rationale: | If option `freq` isn't set to `50`, the flush to disk may happen after higher number of records, increasing the danger of audit loss.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_auditd_freq
Identifiers and References | References: CM-6, FAU_GEN.1, SRG-OS-000051-GPOS-00024

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    if [ -e "/etc/audit/auditd.conf" ] ; then

        LC_ALL=C sed -i "/^\s*freq\s*=\s*/Id" "/etc/audit/auditd.conf"
    else
        touch "/etc/audit/auditd.conf"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/audit/auditd.conf"

    cp "/etc/audit/auditd.conf" "/etc/audit/auditd.conf.bak"
    # Insert at the end of the file
    printf '%s\n' "freq = 50" >> "/etc/audit/auditd.conf"
    # Clean up after ourselves.
    rm "/etc/audit/auditd.conf.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-53-CM-6
      - auditd_freq
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set number of records to cause an explicit flush to audit logs
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/audit/auditd.conf
          create: true
          regexp: (?i)^\s*freq\s*=\s*
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/audit/auditd.conf
        lineinfile:
          path: /etc/audit/auditd.conf
          create: true
          regexp: (?i)^\s*freq\s*=\s*
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/audit/auditd.conf
        lineinfile:
          path: /etc/audit/auditd.conf
          create: true
          regexp: (?i)^\s*freq\s*=\s*
          line: freq = 50
          state: present
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-CM-6
      - auditd_freq
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }}
            mode: 0640
            path: /etc/audit/auditd.conf
            overwrite: true

#### Rule Include Local Events in Audit Logs [ref]

To configure Audit daemon to include local events in Audit logs, set `local_events` to `yes` in `/etc/audit/auditd.conf`. This is the default setting.
Rationale: | If option `local_events` isn't set to `yes` only events from network will be aggregated.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_auditd_local_events
Identifiers and References | References: CCI-000366, CM-6, FAU_GEN.1, SRG-OS-000062-GPOS-00031, SRG-OS-000480-GPOS-00227, SV-230393r627750_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    if [ -e "/etc/audit/auditd.conf" ] ; then

        LC_ALL=C sed -i "/^\s*local_events\s*=\s*/Id" "/etc/audit/auditd.conf"
    else
        touch "/etc/audit/auditd.conf"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/audit/auditd.conf"

    cp "/etc/audit/auditd.conf" "/etc/audit/auditd.conf.bak"
    # Insert at the end of the file
    printf '%s\n' "local_events = yes" >> "/etc/audit/auditd.conf"
    # Clean up after ourselves.
    rm "/etc/audit/auditd.conf.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-030061
      - NIST-800-53-CM-6
      - auditd_local_events
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Include Local Events in Audit Logs
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/audit/auditd.conf
          create: true
          regexp: (?i)^\s*local_events\s*=\s*
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/audit/auditd.conf
        lineinfile:
          path: /etc/audit/auditd.conf
          create: true
          regexp: (?i)^\s*local_events\s*=\s*
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/audit/auditd.conf
        lineinfile:
          path: /etc/audit/auditd.conf
          create: true
          regexp: (?i)^\s*local_events\s*=\s*
          line: local_events = yes
          state: present
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-030061
      - NIST-800-53-CM-6
      - auditd_local_events
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }}
            mode: 0640
            path: /etc/audit/auditd.conf
            overwrite: true

#### Rule Resolve information before writing to audit logs [ref]

To configure Audit daemon to resolve all uid, gid, syscall, architecture, and socket address information before writing the events to disk, set `log_format` to `ENRICHED` in `/etc/audit/auditd.conf`.
Rationale: | If option `log_format` isn't set to `ENRICHED`, the audit records will be stored in a format exactly as the kernel sends them.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_auditd_log_format
Identifiers and References | References: CCI-000366, CM-6, AU-3, FAU_GEN.1.2, SRG-OS-000255-GPOS-00096, SRG-OS-000480-GPOS-00227, SRG-APP-000096-CTR-000175, SRG-APP-000097-CTR-000180, SRG-APP-000098-CTR-000185, SRG-APP-000099-CTR-000190, SRG-APP-000100-CTR-000195, SRG-APP-000100-CTR-000200, SRG-APP-000109-CTR-000215, SRG-APP-000290-CTR-000670, SRG-APP-000357-CTR-000800, SV-230395r627750_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    if [ -e "/etc/audit/auditd.conf" ] ; then

        LC_ALL=C sed -i "/^\s*log_format\s*=\s*/Id" "/etc/audit/auditd.conf"
    else
        touch "/etc/audit/auditd.conf"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/audit/auditd.conf"

    cp "/etc/audit/auditd.conf" "/etc/audit/auditd.conf.bak"
    # Insert at the end of the file
    printf '%s\n' "log_format = ENRICHED" >> "/etc/audit/auditd.conf"
    # Clean up after ourselves.
    rm "/etc/audit/auditd.conf.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-030063
      - NIST-800-53-AU-3
      - NIST-800-53-CM-6
      - auditd_log_format
      - low_complexity
      - low_disruption
      - low_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Resolve information before writing to audit logs
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/audit/auditd.conf
          create: true
          regexp: (?i)^\s*log_format\s*=\s*
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/audit/auditd.conf
        lineinfile:
          path: /etc/audit/auditd.conf
          create: true
          regexp: (?i)^\s*log_format\s*=\s*
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/audit/auditd.conf
        lineinfile:
          path: /etc/audit/auditd.conf
          create: true
          regexp: (?i)^\s*log_format\s*=\s*
          line: log_format = ENRICHED
          state: present
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-030063
      - NIST-800-53-AU-3
      - NIST-800-53-CM-6
      - auditd_log_format
      - low_complexity
      - low_disruption
      - low_severity
      - no_reboot_needed
      - restrict_strategy

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }}
            mode: 0640
            path: /etc/audit/auditd.conf
            overwrite: true

#### Rule Set type of computer node name logging in audit logs [ref]

To configure Audit daemon to use a unique identifier as computer node name in the audit events, set `name_format` to `hostname` in `/etc/audit/auditd.conf`.
Warning: Whenever the variable

    var_auditd_name_format

uses a multiple value option, for example

    A|B|C

, the first value will be used when remediating this rule.

Rationale: | If option `name_format` is left at its default value of `none`, audit events from different computers may be hard to distinguish.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_auditd_name_format
Identifiers and References | References: CCI-001851, CM-6, AU-3, FAU_GEN.1.2, SRG-OS-000039-GPOS-00017, SRG-OS-000342-GPOS-00133, SRG-OS-000479-GPOS-00224, SV-230394r877390_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    var_auditd_name_format='hostname'

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

    if [ -e "/etc/audit/auditd.conf" ] ; then

        LC_ALL=C sed -i "/^\s*name_format\s*=\s*/Id" "/etc/audit/auditd.conf"
    else
        touch "/etc/audit/auditd.conf"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/audit/auditd.conf"

    cp "/etc/audit/auditd.conf" "/etc/audit/auditd.conf.bak"
    # Insert at the end of the file
    printf '%s\n' "name_format = $var_auditd_name_format" >> "/etc/audit/auditd.conf"
    # Clean up after ourselves.
    rm "/etc/audit/auditd.conf.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-030062
      - NIST-800-53-AU-3
      - NIST-800-53-CM-6
      - auditd_name_format
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_auditd_name_format # promote to variable
      set_fact:
        var_auditd_name_format: !!str hostname
      tags:
        - always

    - name: Set type of computer node name logging in audit logs - Define Value to Be
        Used in the Remediation
      ansible.builtin.set_fact: auditd_name_format_split="{{ var_auditd_name_format.split('|')[0]
        }}"
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-030062
      - NIST-800-53-AU-3
      - NIST-800-53-CM-6
      - auditd_name_format
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set type of computer node name logging in audit logs
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/audit/auditd.conf
          create: true
          regexp: (?i)^\s*name_format\s*=\s*
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/audit/auditd.conf
        lineinfile:
          path: /etc/audit/auditd.conf
          create: true
          regexp: (?i)^\s*name_format\s*=\s*
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/audit/auditd.conf
        lineinfile:
          path: /etc/audit/auditd.conf
          create: true
          regexp: (?i)^\s*name_format\s*=\s*
          line: name_format = {{ auditd_name_format_split }}
          state: present
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-030062
      - NIST-800-53-AU-3
      - NIST-800-53-CM-6
      - auditd_name_format
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }}
            mode: 0640
            path: /etc/audit/auditd.conf
            overwrite: true

#### Rule Write Audit Logs to the Disk [ref]

To configure Audit daemon to write Audit logs to the disk, set `write_logs` to `yes` in `/etc/audit/auditd.conf`. This is the default setting.
Rationale: | If `write_logs` isn't set to `yes`, the Audit logs will not be written to the disk.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_auditd_write_logs
Identifiers and References | References: CM-6, FAU_STG.1, SRG-OS-000480-GPOS-00227

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && rpm --quiet -q audit; then

    if [ -e "/etc/audit/auditd.conf" ] ; then

        LC_ALL=C sed -i "/^\s*write_logs\s*=\s*/Id" "/etc/audit/auditd.conf"
    else
        touch "/etc/audit/auditd.conf"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/audit/auditd.conf"

    cp "/etc/audit/auditd.conf" "/etc/audit/auditd.conf.bak"
    # Insert at the end of the file
    printf '%s\n' "write_logs = yes" >> "/etc/audit/auditd.conf"
    # Clean up after ourselves.
    rm "/etc/audit/auditd.conf.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-53-CM-6
      - auditd_write_logs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Write Audit Logs to the Disk
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/audit/auditd.conf
          create: true
          regexp: (?i)^\s*write_logs\s*=\s*
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/audit/auditd.conf
        lineinfile:
          path: /etc/audit/auditd.conf
          create: true
          regexp: (?i)^\s*write_logs\s*=\s*
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/audit/auditd.conf
        lineinfile:
          path: /etc/audit/auditd.conf
          create: true
          regexp: (?i)^\s*write_logs\s*=\s*
          line: write_logs = yes
          state: present
      when:
      - '"audit" in ansible_facts.packages'
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-CM-6
      - auditd_write_logs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20audit%20daemon%0A%23%0A%0Alocal_events%20%3D%20yes%0Awrite_logs%20%3D%20yes%0Alog_file%20%3D%20/var/log/audit/audit.log%0Alog_group%20%3D%20root%0Alog_format%20%3D%20ENRICHED%0Aflush%20%3D%20%7B%7B.var_auditd_flush%7D%7D%0Afreq%20%3D%2050%0Amax_log_file%20%3D%20%7B%7B.var_auditd_max_log_file%7D%7D%0Anum_logs%20%3D%20%7B%7B.var_auditd_num_logs%7D%7D%0Apriority_boost%20%3D%204%0Aname_format%20%3D%20hostname%0A%23%23name%20%3D%20mydomain%0Amax_log_file_action%20%3D%20%7B%7B.var_auditd_max_log_file_action%7D%7D%0Aspace_left%20%3D%20%7B%7B.var_auditd_space_left%7D%7D%0Aspace_left_action%20%3D%20%7B%7B.var_auditd_space_left_action%7D%7D%0Averify_email%20%3D%20yes%0Aaction_mail_acct%20%3D%20%7B%7B.var_auditd_action_mail_acct%7D%7D%0Aadmin_space_left%20%3D%2050%0Aadmin_space_left_action%20%3D%20syslog%0Adisk_full_action%20%3D%20%7B%7B.var_auditd_disk_full_action%7D%7D%0Adisk_error_action%20%3D%20%7B%7B.var_auditd_disk_error_action%7D%7D%0Ause_libwrap%20%3D%20yes%0A%23%23tcp_listen_port%20%3D%2060%0Atcp_listen_queue%20%3D%205%0Atcp_max_per_addr%20%3D%201%0A%23%23tcp_client_ports%20%3D%201024-65535%0Atcp_client_max_idle%20%3D%200%0Atransport%20%3D%20TCP%0Akrb5_principal%20%3D%20auditd%0A%23%23krb5_key_file%20%3D%20/etc/audit/audit.key%0Adistribute_network%20%3D%20no%0Aq_depth%20%3D%20400%0Aoverflow_action%20%3D%20syslog%0Amax_restarts%20%3D%2010%0Aplugin_dir%20%3D%20/etc/audit/plugins.d }}
            mode: 0640
            path: /etc/audit/auditd.conf
            overwrite: true

Group System Accounting with auditd Group contains 2 rules
[ref] The `auditd` program can perform comprehensive monitoring of system activity. This section makes use of recommended configuration settings for specific policies or use cases. The rules in this section make use of rules defined in `/usr/share/doc/audit-VERSION/rules`.

#### Rule Configure auditing of unsuccessful file accesses [ref]

Ensure that unsuccessful attempts to access a file are audited. The following rules configure audit as described above:

    ## Unsuccessful file access (any other opens) This has to go last.
    -a always,exit -F arch=b32 -S open,openat,openat2,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccessful-access
    -a always,exit -F arch=b64 -S open,openat,openat2,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccessful-access
    -a always,exit -F arch=b32 -S open,openat,openat2,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccessful-access
    -a always,exit -F arch=b64 -S open,openat,openat2,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccessful-access

Load new Audit rules into kernel by running:

    augenrules --load

Note: This rule uses a special set of Audit rules to comply with OSPP 4.2.1. You may reuse this rule in different profiles. If you decide to do so, it is recommended that you inspect contents of the file closely and make sure that they are alligned with your needs.

Rationale: | Unsuccessful attempts to access a file might be signs of malicious activity happening within the system. Auditing of such activities helps in their monitoring and investigation.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_access_failed

Identifiers and References

References: 0582, 0584, 05885, 0586, 0846, 0957, AU-2(a), FAU_GEN.1.1.c, SRG-OS-000458-GPOS-00203, SRG-OS-000474-GPOS-00219, SRG-OS-000475-GPOS-00220, SRG-OS-000463-GPOS-00207, SRG-OS-000465-GPOS-00209, SRG-OS-000461-GPOS-00205, SRG-APP-000091-CTR-000160, SRG-APP-000492-CTR-001220, SRG-APP-000493-CTR-001225, SRG-APP-000494-CTR-001230, SRG-APP-000500-CTR-001260, SRG-APP-000507-CTR-001295

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    cat << 'EOF' > /etc/audit/rules.d/30-ospp-v42-3-access-failed.rules
    ## Unsuccessful file access (any other opens) This has to go last.
    -a always,exit -F arch=b32 -S open,openat,openat2,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccessful-access
    -a always,exit -F arch=b64 -S open,openat,openat2,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccessful-access
    -a always,exit -F arch=b32 -S open,openat,openat2,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccessful-access
    -a always,exit -F arch=b64 -S open,openat,openat2,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccessful-access
    EOF

    chmod o-rwx /etc/audit/rules.d/30-ospp-v42-3-access-failed.rules

    augenrules --load

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Put contents into /etc/audit/rules.d/30-ospp-v42-3-access-failed.rules according
        to policy
      copy:
        dest: /etc/audit/rules.d/30-ospp-v42-3-access-failed.rules
        content: |
          ## Unsuccessful file access (any other opens) This has to go last.
          -a always,exit -F arch=b32 -S open,openat,openat2,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccessful-access
          -a always,exit -F arch=b64 -S open,openat,openat2,open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=unsuccessful-access
          -a always,exit -F arch=b32 -S open,openat,openat2,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccessful-access
          -a always,exit -F arch=b64 -S open,openat,openat2,open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=unsuccessful-access
        force: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AU-2(a)
      - audit_access_failed
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Remove any permissions from other group
      file:
        path: /etc/audit/rules.d/30-ospp-v42-3-access-failed.rules
        mode: o-rwx
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AU-2(a)
      - audit_access_failed
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Configure auditing of successful file accesses [ref]

Ensure that successful attempts to access a file are audited. The following rules configure audit as described above:

    ## Successful file access (any other opens) This has to go last.
    ## These next two are likely to result in a whole lot of events
    -a always,exit -F arch=b32 -S open,openat,openat2,open_by_handle_at -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access
    -a always,exit -F arch=b64 -S open,openat,openat2,open_by_handle_at -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access

Load new Audit rules into kernel by running:

    augenrules --load

Note: This rule uses a special set of Audit rules to comply with OSPP 4.2.1. You may reuse this rule in different profiles. If you decide to do so, it is recommended that you inspect contents of the file closely and make sure that they are alligned with your needs.

Rationale: | Auditing of successful attempts to access a file helps in investigation of activities performed on the system.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_audit_access_success
Identifiers and References | References: 0582, 0584, 05885, 0586, 0846, 0957, AU-2(a), FAU_GEN.1.1.c, SRG-OS-000458-GPOS-00203, SRG-OS-000474-GPOS-00219, SRG-OS-000475-GPOS-00220, SRG-OS-000463-GPOS-00207, SRG-OS-000465-GPOS-00209, SRG-OS-000461-GPOS-00205

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    cat << 'EOF' > /etc/audit/rules.d/30-ospp-v42-3-access-success.rules
    ## Successful file access (any other opens) This has to go last.
    ## These next two are likely to result in a whole lot of events
    -a always,exit -F arch=b32 -S open,openat,openat2,open_by_handle_at -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access
    -a always,exit -F arch=b64 -S open,openat,openat2,open_by_handle_at -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access
    EOF

    chmod o-rwx /etc/audit/rules.d/30-ospp-v42-3-access-success.rules

    augenrules --load

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Put contents into /etc/audit/rules.d/30-ospp-v42-3-access-success.rules according
        to policy
      copy:
        dest: /etc/audit/rules.d/30-ospp-v42-3-access-success.rules
        content: |
          ## Successful file access (any other opens) This has to go last.
          ## These next two are likely to result in a whole lot of events
          -a always,exit -F arch=b32 -S open,openat,openat2,open_by_handle_at -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access
          -a always,exit -F arch=b64 -S open,openat,openat2,open_by_handle_at -F success=1 -F auid>=1000 -F auid!=unset -F key=successful-access
        force: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AU-2(a)
      - audit_access_success
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Remove any permissions from other group
      file:
        path: /etc/audit/rules.d/30-ospp-v42-3-access-success.rules
        mode: o-rwx
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AU-2(a)
      - audit_access_success
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Remediation Kubernetes snippet ⇲

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,%23%23%20Successful%20file%20access%20%28any%20other%20opens%29%20This%20has%20to%20go%20last.%0A%23%23%20These%20next%20two%20are%20likely%20to%20result%20in%20a%20whole%20lot%20of%20events%0A-a%20always%2Cexit%20-F%20arch%3Db32%20-S%20open%2Copenat%2Copen_by_handle_at%20-F%20success%3D1%20-F%20auid%26gt%3B%3D1000%20-F%20auid%21%3Dunset%20-F%20key%3Dsuccessful-access%0A-a%20always%2Cexit%20-F%20arch%3Db64%20-S%20open%2Copenat%2Copen_by_handle_at%20-F%20success%3D1%20-F%20auid%26gt%3B%3D1000%20-F%20auid%21%3Dunset%20-F%20key%3Dsuccessful-access
            mode: 0600
            path: /etc/audit/rules.d/30-ospp-v42-3-access-success.rules
            overwrite: true

#### Rule Enable auditd Service [ref]

The `auditd` service is an essential userspace component of the Linux Auditing System, as it is responsible for writing audit records to disk. The `auditd` service can be enabled with the following command:

    $ sudo systemctl enable auditd.service

Rationale:

Without establishing what type of events occurred, it would be difficult to establish, correlate, and investigate the events leading up to an outage or attack. Ensuring the `auditd` service is active ensures audit records generated by the kernel are appropriately recorded.
Additionally, a properly configured audit subsystem ensures that actions of individual system users can be uniquely traced to those users so they can be held accountable for their actions.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_auditd_enabled

Identifiers and References

References: BP28(R33), BP28(R73), 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, 5.4.1.1, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, APO12.06, APO13.01, BAI03.05, BAI08.02, DSS01.03, DSS01.04, DSS02.02, DSS02.04, DSS02.07, DSS03.01, DSS03.05, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, 3.3.1, 3.3.2, 3.3.6, CCI-000126, CCI-000130, CCI-000131, CCI-000132, CCI-000133, CCI-000134, CCI-000135, CCI-000154, CCI-000158, CCI-000172, CCI-000366, CCI-001464, CCI-001487, CCI-001814, CCI-001875, CCI-001876, CCI-001877, CCI-002884, CCI-001878, CCI-001879, CCI-001880, CCI-001881, CCI-001882, CCI-001889, CCI-001914, CCI-000169, 164.308(a)(1)(ii)(D), 164.308(a)(5)(ii)(C), 164.310(a)(2)(iv), 164.310(d)(2)(iii), 164.312(b), 4.2.3.10, 4.3.2.6.7, 4.3.3.3.9, 4.3.3.5.8, 4.3.3.6.6, 4.3.4.4.7, 4.3.4.5.6, 4.3.4.5.7, 4.3.4.5.8, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 1.13, SR 2.10, SR 2.11, SR 2.12, SR 2.6, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 6.1, SR 6.2, SR 7.1, SR 7.6, A.11.2.6, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.7, A.15.2.1, A.15.2.2, A.16.1.4, A.16.1.5, A.16.1.7, A.6.2.1, A.6.2.2, CIP-004-6 R3.3, CIP-007-3 R6.5, AC-2(g), AU-3, AU-10, AU-2(d), AU-12(c), AU-14(1), AC-6(9), CM-6(a), SI-4(23), DE.AE-3, DE.AE-5, DE.CM-1, DE.CM-3, DE.CM-7, ID.SC-4, PR.AC-3, PR.PT-1, PR.PT-4, RS.AN-1, RS.AN-4, FAU_GEN.1, Req-10.1, 10.2.1, SRG-OS-000062-GPOS-00031, SRG-OS-000037-GPOS-00015, SRG-OS-000038-GPOS-00016, SRG-OS-000039-GPOS-00017, SRG-OS-000040-GPOS-00018, SRG-OS-000041-GPOS-00019, SRG-OS-000042-GPOS-00021, SRG-OS-000051-GPOS-00024, SRG-OS-000054-GPOS-00025, SRG-OS-000122-GPOS-00063, SRG-OS-000254-GPOS-00095, SRG-OS-000255-GPOS-00096, SRG-OS-000337-GPOS-00129, SRG-OS-000348-GPOS-00136, SRG-OS-000349-GPOS-00137, SRG-OS-000350-GPOS-00138, SRG-OS-000351-GPOS-00139, SRG-OS-000352-GPOS-00140, SRG-OS-000353-GPOS-00141, SRG-OS-000354-GPOS-00142, SRG-OS-000358-GPOS-00145, SRG-OS-000365-GPOS-00152, SRG-OS-000392-GPOS-00172, SRG-OS-000475-GPOS-00220, SRG-APP-000095-CTR-000170, SRG-APP-000409-CTR-000990, SRG-APP-000508-CTR-001300, SRG-APP-000510-CTR-001310, 4.1.1.2, SV-244542r818838_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && { rpm --quiet -q audit; }; then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" unmask 'auditd.service'
    "$SYSTEMCTL_EXEC" start 'auditd.service'
    "$SYSTEMCTL_EXEC" enable 'auditd.service'

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-08-030181
      - NIST-800-171-3.3.1
      - NIST-800-171-3.3.2
      - NIST-800-171-3.3.6
      - NIST-800-53-AC-2(g)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-10
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-14(1)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-AU-3
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SI-4(23)
      - PCI-DSS-Req-10.1
      - PCI-DSSv4-10.2.1
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_auditd_enabled

    - name: Enable service auditd
      block:

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

      - name: Enable service auditd
        systemd:
          name: auditd
          enabled: 'yes'
          state: started
          masked: 'no'
        when:
        - '"audit" in ansible_facts.packages'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - '"audit" in ansible_facts.packages'
      tags:
      - CJIS-5.4.1.1
      - DISA-STIG-RHEL-08-030181
      - NIST-800-171-3.3.1
      - NIST-800-171-3.3.2
      - NIST-800-171-3.3.6
      - NIST-800-53-AC-2(g)
      - NIST-800-53-AC-6(9)
      - NIST-800-53-AU-10
      - NIST-800-53-AU-12(c)
      - NIST-800-53-AU-14(1)
      - NIST-800-53-AU-2(d)
      - NIST-800-53-AU-3
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SI-4(23)
      - PCI-DSS-Req-10.1
      - PCI-DSSv4-10.2.1
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_auditd_enabled

Remediation OSBuild Blueprint snippet ⇲

    [customizations.services]
    enabled = ["auditd"]

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include enable_auditd

    class enable_auditd {
      service {'auditd':
        enable => true,
        ensure => 'running',
      }
    }

Remediation Kubernetes snippet ⇲

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        systemd:
          units:
          - name: auditd.service
            enabled: true

Group Configure Syslog Group contains 3 groups and 10 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 Red Hat Enterprise Linux 8, rsyslog has replaced ksyslogd as the syslog daemon of choice, and it includes some additional security features such as reliable, connection-oriented (i.e. TCP) transmission of logs, the option to log to database formats, and the encryption of log data en route to a central logging server. This section discusses how to configure rsyslog for best effect, and how to use tools provided with the system to maintain and monitor logs.

Group Ensure Proper Configuration of Log Files Group contains 4 rules

[ref] The file `/etc/rsyslog.conf` controls where log message are written. These are controlled by lines called rules, which consist of a selector and an action. These rules are often customized depending on the role of the system, the requirements of the environment, and whatever may enable the administrator to most effectively make use of log data. The default rules in Red Hat Enterprise Linux 8 are:

    *.info;mail.none;authpriv.none;cron.none                /var/log/messages
    authpriv.*                                              /var/log/secure
    mail.*                                                  -/var/log/maillog
    cron.*                                                  /var/log/cron
    *.emerg                                                 *
    uucp,news.crit                                          /var/log/spooler
    local7.*                                                /var/log/boot.log

See the man page `rsyslog.conf(5)` for more information. Note that the `rsyslog` daemon can be configured to use a timestamp format that some log processing programs may not understand. If this occurs, edit the file `/etc/rsyslog.conf` and add or edit the following line:

    $ ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat

#### Rule Ensure cron Is Logging To Rsyslog [ref]

Cron logging must be implemented to spot intrusions or trace cron job status. If `cron` is not logging to `rsyslog`, it can be implemented by adding the following to the RULES section of `/etc/rsyslog.conf`:

    cron.*                                                  /var/log/cron

Rationale: | Cron logging can be used to trace the successful or unsuccessful execution of cron jobs. It can also be used to spot intrusions into the use of the cron facility by unauthorized and malicious users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_rsyslog_cron_logging

Identifiers and References

References: 1, 14, 15, 16, 3, 5, 6, APO10.01, APO10.03, APO10.04, APO10.05, APO11.04, BAI03.05, DSS05.04, DSS05.07, MEA01.01, MEA01.02, MEA01.03, MEA01.04, MEA01.05, MEA02.01, CCI-000366, 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, 0988, 1405, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.15.2.1, A.15.2.2, CM-6(a), ID.SC-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000480-GPOS-00227, SV-230387r743996_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if ! grep -s "^\s*cron\.\*\s*/var/log/cron$" /etc/rsyslog.conf /etc/rsyslog.d/*.conf; then
    	mkdir -p /etc/rsyslog.d
    	echo "cron.*	/var/log/cron" >> /etc/rsyslog.d/cron.conf
    fi

    systemctl restart rsyslog.service

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

#### Rule Ensure Log Files Are Owned By Appropriate Group [ref]

The group-owner of all log files written by `rsyslog` should be `root`. These log files are determined by the second part of each Rule line in `/etc/rsyslog.conf` and typically all appear in `/var/log`. For each log file LOGFILE referenced in `/etc/rsyslog.conf`, run the following command to inspect the file's group owner:

    $ ls -l LOGFILE

If the owner is not `root`, run the following command to correct this:

    $ sudo chgrp root LOGFILE

Rationale: | The log files generated by rsyslog contain valuable information regarding system configuration, user authentication, and other such information. Log files should be protected from unauthorized access.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_rsyslog_files_groupownership

Identifiers and References

References: BP28(R46), BP28(R5), 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-001314, 4.3.3.7.3, SR 2.1, SR 5.2, 0988, 1405, 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, Req-10.5.1, Req-10.5.2, 10.3.2

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    # List of log file paths to be inspected for correct permissions
    # * Primarily inspect log file paths listed in /etc/rsyslog.conf
    RSYSLOG_ETC_CONFIG="/etc/rsyslog.conf"
    # * And also the log file paths listed after rsyslog's $IncludeConfig directive
    #   (store the result into array for the case there's shell glob used as value of IncludeConfig)
    readarray -t OLD_INC < <(grep -e "\$IncludeConfig[[:space:]]\+[^[:space:];]\+" /etc/rsyslog.conf | cut -d ' ' -f 2)
    readarray -t RSYSLOG_INCLUDE_CONFIG < <(for INCPATH in "${OLD_INC[@]}"; do eval printf '%s\\n' "${INCPATH}"; done)
    readarray -t NEW_INC < <(sed -n '/^\s*include(/,/)/Ip' /etc/rsyslog.conf | sed -n 's@.*file\s*=\s*"\([/[:alnum:][:punct:]]*\)".*@\1@Ip')
    readarray -t RSYSLOG_INCLUDE < <(for INCPATH in "${NEW_INC[@]}"; do eval printf '%s\\n' "${INCPATH}"; done)

    # Declare an array to hold the final list of different log file paths
    declare -a LOG_FILE_PATHS

    # Array to hold all rsyslog config entries
    RSYSLOG_CONFIGS=()
    RSYSLOG_CONFIGS=("${RSYSLOG_ETC_CONFIG}" "${RSYSLOG_INCLUDE_CONFIG[@]}" "${RSYSLOG_INCLUDE[@]}")

    # Get full list of files to be checked
    # RSYSLOG_CONFIGS may contain globs such as
    # /etc/rsyslog.d/*.conf /etc/rsyslog.d/*.frule
    # So, loop over the entries in RSYSLOG_CONFIGS and use find to get the list of included files.
    RSYSLOG_CONFIG_FILES=()
    for ENTRY in "${RSYSLOG_CONFIGS[@]}"
    do
    	# If directory, rsyslog will search for config files in recursively.
    	# However, files in hidden sub-directories or hidden files will be ignored.
    	if [ -d "${ENTRY}" ]
    	then
    		readarray -t FINDOUT < <(find "${ENTRY}" -not -path '*/.*' -type f)
    		RSYSLOG_CONFIG_FILES+=("${FINDOUT[@]}")
    	elif [ -f "${ENTRY}" ]
    	then
    		RSYSLOG_CONFIG_FILES+=("${ENTRY}")
    	else
    		echo "Invalid include object: ${ENTRY}"
    	fi
    done

    # Browse each file selected above as containing paths of log files
    # ('/etc/rsyslog.conf' and '/etc/rsyslog.d/*.conf' in the default configuration)
    for LOG_FILE in "${RSYSLOG_CONFIG_FILES[@]}"
    do
    	# From each of these files extract just particular log file path(s), thus:
    	# * Ignore lines starting with space (' '), comment ('#"), or variable syntax ('$') characters,
    	# * Ignore empty lines,
    	# * Strip quotes and closing brackets from paths.
    	# * Ignore paths that match /dev|/etc.*\.conf, as those are paths, but likely not log files
    	# * From the remaining valid rows select only fields constituting a log file path
    	# Text file column is understood to represent a log file path if and only if all of the
    	# following are met:
    	# * it contains at least one slash '/' character,
    	# * it is preceded by space
    	# * it doesn't contain space (' '), colon (':'), and semicolon (';') characters
    	# Search log file for path(s) only in case it exists!
    	if [[ -f "${LOG_FILE}" ]]
    	then
    		NORMALIZED_CONFIG_FILE_LINES=$(sed -e "/^[#|$]/d" "${LOG_FILE}")
    		LINES_WITH_PATHS=$(grep '[^/]*\s\+\S*/\S\+$' <<< "${NORMALIZED_CONFIG_FILE_LINES}")
    		FILTERED_PATHS=$(awk '{if(NF>=2&&($NF~/^\//||$NF~/^-\//)){sub(/^-\//,"/",$NF);print $NF}}' <<< "${LINES_WITH_PATHS}")
    		CLEANED_PATHS=$(sed -e "s/[\"')]//g; /\\/etc.*\.conf/d; /\\/dev\\//d" <<< "${FILTERED_PATHS}")
    		MATCHED_ITEMS=$(sed -e "/^$/d" <<< "${CLEANED_PATHS}")
    		# Since above sed command might return more than one item (delimited by newline), split
    		# the particular matches entries into new array specific for this log file
    		readarray -t ARRAY_FOR_LOG_FILE <<< "$MATCHED_ITEMS"
    		# Concatenate the two arrays - previous content of $LOG_FILE_PATHS array with
    		# items from newly created array for this log file
    		LOG_FILE_PATHS+=("${ARRAY_FOR_LOG_FILE[@]}")
    		# Delete the temporary array
    		unset ARRAY_FOR_LOG_FILE
    	fi
    done

    # Check for RainerScript action log format which might be also multiline so grep regex is a bit
    # curly:
    # extract possibly multiline action omfile expressions
    # extract File="logfile" expression
    # match only "logfile" expression
    for LOG_FILE in "${RSYSLOG_CONFIG_FILES[@]}"
    do
    	ACTION_OMFILE_LINES=$(grep -iozP "action\s*\(\s*type\s*=\s*\"omfile\"[^\)]*\)" "${LOG_FILE}")
    	OMFILE_LINES=$(echo "${ACTION_OMFILE_LINES}"| grep -iaoP "File\s*=\s*\"([/[:alnum:][:punct:]]*)\"\s*\)")
    	LOG_FILE_PATHS+=("$(echo "${OMFILE_LINES}"| grep -oE "\"([/[:alnum:][:punct:]]*)\""|tr -d "\"")")
    done

    # Ensure the correct attribute if file exists
    FILE_CMD="chgrp"
    for LOG_FILE_PATH in "${LOG_FILE_PATHS[@]}"
    do
    	# Sanity check - if particular $LOG_FILE_PATH is empty string, skip it from further processing
    	if [ -z "$LOG_FILE_PATH" ]
    	then
    		continue
    	fi
    	$FILE_CMD "root" "$LOG_FILE_PATH"
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure

    - name: Ensure Log Files Are Owned By Appropriate Group - Set rsyslog logfile configuration
        facts
      ansible.builtin.set_fact:
        rsyslog_etc_config: /etc/rsyslog.conf
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_groupownership

    - name: Ensure Log Files Are Owned By Appropriate Group - Get IncludeConfig directive
      ansible.builtin.shell: |
        set -o pipefail
        grep -e '$IncludeConfig' {{ rsyslog_etc_config }} | cut -d ' ' -f 2 || true
      register: rsyslog_old_inc
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_groupownership

    - name: Ensure Log Files Are Owned By Appropriate Group - Get include files directives
      ansible.builtin.shell: |
        set -o pipefail
        awk '/)/{f=0} /include\(/{f=1} f{nf=gensub("^(include\\(|\\s*)file=\"(\\S+)\".*","\\2",1); if($0!=nf){print nf}}' {{ rsyslog_etc_config }} || true
      register: rsyslog_new_inc
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_groupownership

    - name: Ensure Log Files Are Owned By Appropriate Group - Aggregate rsyslog includes
      ansible.builtin.set_fact:
        include_config_output: '{{ rsyslog_old_inc.stdout_lines + rsyslog_new_inc.stdout_lines
          }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_groupownership

    - name: Ensure Log Files Are Owned By Appropriate Group - List all config files
      ansible.builtin.find:
        paths: '{{ item | dirname }}'
        patterns: '{{ item | basename }}'
        hidden: false
        follow: true
      loop: '{{ include_config_output | list + [rsyslog_etc_config] }}'
      register: rsyslog_config_files
      failed_when: false
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_groupownership

    - name: Ensure Log Files Are Owned By Appropriate Group - Extract log files old format
      ansible.builtin.shell: |
        set -o pipefail
        grep -oP '^[^(\s|#|\$)]+[\s]+.*[\s]+-?(/+[^:;\s]+);*\.*$' {{ item.1.path }} | \
        awk '{print $NF}' | \
        sed -e 's/^-//' || true
      loop: '{{ rsyslog_config_files.results | subelements(''files'') }}'
      register: log_files_old
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_groupownership

    - name: Ensure Log Files Are Owned By Appropriate Group - Extract log files new format
      ansible.builtin.shell: |
        set -o pipefail
        grep -ozP "action\s*\(\s*type\s*=\s*\"omfile\"[^\)]*\)" {{ item.1.path }} | \
        grep -aoP "File\s*=\s*\"([/[:alnum:][:punct:]]*)\"\s*\)" | \
        grep -oE "\"([/[:alnum:][:punct:]]*)\"" | \
        tr -d "\""|| true
      loop: '{{ rsyslog_config_files.results | subelements(''files'') }}'
      register: log_files_new
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_groupownership

    - name: Ensure Log Files Are Owned By Appropriate Group - Sum all log files found
      ansible.builtin.set_fact:
        log_files: '{{ log_files_new.results | map(attribute=''stdout_lines'') | list
          | flatten | unique + log_files_old.results | map(attribute=''stdout_lines'')
          | list | flatten | unique }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_groupownership

    - name: Ensure Log Files Are Owned By Appropriate Group -Setup log files attribute
      ansible.builtin.file:
        path: '{{ item }}'
        group: root
        state: file
      loop: '{{ log_files | list | flatten | unique }}'
      failed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_groupownership

#### Rule Ensure Log Files Are Owned By Appropriate User [ref]

The owner of all log files written by `rsyslog` should be `root`. These log files are determined by the second part of each Rule line in `/etc/rsyslog.conf` and typically all appear in `/var/log`. For each log file LOGFILE referenced in `/etc/rsyslog.conf`, run the following command to inspect the file's owner:

    $ ls -l LOGFILE

If the owner is not `root`, run the following command to correct this:

    $ sudo chown root LOGFILE

Rationale: | The log files generated by rsyslog contain valuable information regarding system configuration, user authentication, and other such information. Log files should be protected from unauthorized access.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_rsyslog_files_ownership

Identifiers and References

References: BP28(R46), BP28(R5), 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-001314, 4.3.3.7.3, SR 2.1, SR 5.2, 0988, 1405, 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, Req-10.5.1, Req-10.5.2, 10.3.2

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    # List of log file paths to be inspected for correct permissions
    # * Primarily inspect log file paths listed in /etc/rsyslog.conf
    RSYSLOG_ETC_CONFIG="/etc/rsyslog.conf"
    # * And also the log file paths listed after rsyslog's $IncludeConfig directive
    #   (store the result into array for the case there's shell glob used as value of IncludeConfig)
    readarray -t OLD_INC < <(grep -e "\$IncludeConfig[[:space:]]\+[^[:space:];]\+" /etc/rsyslog.conf | cut -d ' ' -f 2)
    readarray -t RSYSLOG_INCLUDE_CONFIG < <(for INCPATH in "${OLD_INC[@]}"; do eval printf '%s\\n' "${INCPATH}"; done)
    readarray -t NEW_INC < <(sed -n '/^\s*include(/,/)/Ip' /etc/rsyslog.conf | sed -n 's@.*file\s*=\s*"\([/[:alnum:][:punct:]]*\)".*@\1@Ip')
    readarray -t RSYSLOG_INCLUDE < <(for INCPATH in "${NEW_INC[@]}"; do eval printf '%s\\n' "${INCPATH}"; done)

    # Declare an array to hold the final list of different log file paths
    declare -a LOG_FILE_PATHS

    # Array to hold all rsyslog config entries
    RSYSLOG_CONFIGS=()
    RSYSLOG_CONFIGS=("${RSYSLOG_ETC_CONFIG}" "${RSYSLOG_INCLUDE_CONFIG[@]}" "${RSYSLOG_INCLUDE[@]}")

    # Get full list of files to be checked
    # RSYSLOG_CONFIGS may contain globs such as
    # /etc/rsyslog.d/*.conf /etc/rsyslog.d/*.frule
    # So, loop over the entries in RSYSLOG_CONFIGS and use find to get the list of included files.
    RSYSLOG_CONFIG_FILES=()
    for ENTRY in "${RSYSLOG_CONFIGS[@]}"
    do
    	# If directory, rsyslog will search for config files in recursively.
    	# However, files in hidden sub-directories or hidden files will be ignored.
    	if [ -d "${ENTRY}" ]
    	then
    		readarray -t FINDOUT < <(find "${ENTRY}" -not -path '*/.*' -type f)
    		RSYSLOG_CONFIG_FILES+=("${FINDOUT[@]}")
    	elif [ -f "${ENTRY}" ]
    	then
    		RSYSLOG_CONFIG_FILES+=("${ENTRY}")
    	else
    		echo "Invalid include object: ${ENTRY}"
    	fi
    done

    # Browse each file selected above as containing paths of log files
    # ('/etc/rsyslog.conf' and '/etc/rsyslog.d/*.conf' in the default configuration)
    for LOG_FILE in "${RSYSLOG_CONFIG_FILES[@]}"
    do
    	# From each of these files extract just particular log file path(s), thus:
    	# * Ignore lines starting with space (' '), comment ('#"), or variable syntax ('$') characters,
    	# * Ignore empty lines,
    	# * Strip quotes and closing brackets from paths.
    	# * Ignore paths that match /dev|/etc.*\.conf, as those are paths, but likely not log files
    	# * From the remaining valid rows select only fields constituting a log file path
    	# Text file column is understood to represent a log file path if and only if all of the
    	# following are met:
    	# * it contains at least one slash '/' character,
    	# * it is preceded by space
    	# * it doesn't contain space (' '), colon (':'), and semicolon (';') characters
    	# Search log file for path(s) only in case it exists!
    	if [[ -f "${LOG_FILE}" ]]
    	then
    		NORMALIZED_CONFIG_FILE_LINES=$(sed -e "/^[#|$]/d" "${LOG_FILE}")
    		LINES_WITH_PATHS=$(grep '[^/]*\s\+\S*/\S\+$' <<< "${NORMALIZED_CONFIG_FILE_LINES}")
    		FILTERED_PATHS=$(awk '{if(NF>=2&&($NF~/^\//||$NF~/^-\//)){sub(/^-\//,"/",$NF);print $NF}}' <<< "${LINES_WITH_PATHS}")
    		CLEANED_PATHS=$(sed -e "s/[\"')]//g; /\\/etc.*\.conf/d; /\\/dev\\//d" <<< "${FILTERED_PATHS}")
    		MATCHED_ITEMS=$(sed -e "/^$/d" <<< "${CLEANED_PATHS}")
    		# Since above sed command might return more than one item (delimited by newline), split
    		# the particular matches entries into new array specific for this log file
    		readarray -t ARRAY_FOR_LOG_FILE <<< "$MATCHED_ITEMS"
    		# Concatenate the two arrays - previous content of $LOG_FILE_PATHS array with
    		# items from newly created array for this log file
    		LOG_FILE_PATHS+=("${ARRAY_FOR_LOG_FILE[@]}")
    		# Delete the temporary array
    		unset ARRAY_FOR_LOG_FILE
    	fi
    done

    # Check for RainerScript action log format which might be also multiline so grep regex is a bit
    # curly:
    # extract possibly multiline action omfile expressions
    # extract File="logfile" expression
    # match only "logfile" expression
    for LOG_FILE in "${RSYSLOG_CONFIG_FILES[@]}"
    do
    	ACTION_OMFILE_LINES=$(grep -iozP "action\s*\(\s*type\s*=\s*\"omfile\"[^\)]*\)" "${LOG_FILE}")
    	OMFILE_LINES=$(echo "${ACTION_OMFILE_LINES}"| grep -iaoP "File\s*=\s*\"([/[:alnum:][:punct:]]*)\"\s*\)")
    	LOG_FILE_PATHS+=("$(echo "${OMFILE_LINES}"| grep -oE "\"([/[:alnum:][:punct:]]*)\""|tr -d "\"")")
    done

    # Ensure the correct attribute if file exists
    FILE_CMD="chown"
    for LOG_FILE_PATH in "${LOG_FILE_PATHS[@]}"
    do
    	# Sanity check - if particular $LOG_FILE_PATH is empty string, skip it from further processing
    	if [ -z "$LOG_FILE_PATH" ]
    	then
    		continue
    	fi
    	$FILE_CMD "root" "$LOG_FILE_PATH"
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure

    - name: Ensure Log Files Are Owned By Appropriate User - Set rsyslog logfile configuration
        facts
      ansible.builtin.set_fact:
        rsyslog_etc_config: /etc/rsyslog.conf
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_ownership

    - name: Ensure Log Files Are Owned By Appropriate User - Get IncludeConfig directive
      ansible.builtin.shell: |
        set -o pipefail
        grep -e '$IncludeConfig' {{ rsyslog_etc_config }} | cut -d ' ' -f 2 || true
      register: rsyslog_old_inc
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_ownership

    - name: Ensure Log Files Are Owned By Appropriate User - Get include files directives
      ansible.builtin.shell: |
        set -o pipefail
        awk '/)/{f=0} /include\(/{f=1} f{nf=gensub("^(include\\(|\\s*)file=\"(\\S+)\".*","\\2",1); if($0!=nf){print nf}}' {{ rsyslog_etc_config }} || true
      register: rsyslog_new_inc
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_ownership

    - name: Ensure Log Files Are Owned By Appropriate User - Aggregate rsyslog includes
      ansible.builtin.set_fact:
        include_config_output: '{{ rsyslog_old_inc.stdout_lines + rsyslog_new_inc.stdout_lines
          }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_ownership

    - name: Ensure Log Files Are Owned By Appropriate User - List all config files
      ansible.builtin.find:
        paths: '{{ item | dirname }}'
        patterns: '{{ item | basename }}'
        hidden: false
        follow: true
      loop: '{{ include_config_output | list + [rsyslog_etc_config] }}'
      register: rsyslog_config_files
      failed_when: false
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_ownership

    - name: Ensure Log Files Are Owned By Appropriate User - Extract log files old format
      ansible.builtin.shell: |
        set -o pipefail
        grep -oP '^[^(\s|#|\$)]+[\s]+.*[\s]+-?(/+[^:;\s]+);*\.*$' {{ item.1.path }} | \
        awk '{print $NF}' | \
        sed -e 's/^-//' || true
      loop: '{{ rsyslog_config_files.results | subelements(''files'') }}'
      register: log_files_old
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_ownership

    - name: Ensure Log Files Are Owned By Appropriate User - Extract log files new format
      ansible.builtin.shell: |
        set -o pipefail
        grep -ozP "action\s*\(\s*type\s*=\s*\"omfile\"[^\)]*\)" {{ item.1.path }} | \
        grep -aoP "File\s*=\s*\"([/[:alnum:][:punct:]]*)\"\s*\)" | \
        grep -oE "\"([/[:alnum:][:punct:]]*)\"" | \
        tr -d "\""|| true
      loop: '{{ rsyslog_config_files.results | subelements(''files'') }}'
      register: log_files_new
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_ownership

    - name: Ensure Log Files Are Owned By Appropriate User - Sum all log files found
      ansible.builtin.set_fact:
        log_files: '{{ log_files_new.results | map(attribute=''stdout_lines'') | list
          | flatten | unique + log_files_old.results | map(attribute=''stdout_lines'')
          | list | flatten | unique }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_ownership

    - name: Ensure Log Files Are Owned By Appropriate User -Setup log files attribute
      ansible.builtin.file:
        path: '{{ item }}'
        owner: root
        state: file
      loop: '{{ log_files | list | flatten | unique }}'
      failed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_ownership

#### Rule Ensure System Log Files Have Correct Permissions [ref]

The file permissions for all log files written by `rsyslog` should be set to 640, or more restrictive. These log files are determined by the second part of each Rule line in `/etc/rsyslog.conf` and typically all appear in `/var/log`. For each log file LOGFILE referenced in `/etc/rsyslog.conf`, run the following command to inspect the file's permissions:

    $ ls -l LOGFILE

If the permissions are not 640 or more restrictive, run the following command to correct this:

    $ sudo chmod 640 LOGFILE

"

Rationale: | Log files can contain valuable information regarding system configuration. If the system log files are not protected unauthorized users could change the logged data, eliminating their forensic value.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_rsyslog_files_permissions
Identifiers and References | References: BP28(R36), CCI-001314, 0988, 1405, 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), Req-10.5.1, Req-10.5.2, 10.3.1, 4.2.3

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    # List of log file paths to be inspected for correct permissions
    # * Primarily inspect log file paths listed in /etc/rsyslog.conf
    RSYSLOG_ETC_CONFIG="/etc/rsyslog.conf"
    # * And also the log file paths listed after rsyslog's $IncludeConfig directive
    #   (store the result into array for the case there's shell glob used as value of IncludeConfig)
    readarray -t OLD_INC < <(grep -e "\$IncludeConfig[[:space:]]\+[^[:space:];]\+" /etc/rsyslog.conf | cut -d ' ' -f 2)
    readarray -t RSYSLOG_INCLUDE_CONFIG < <(for INCPATH in "${OLD_INC[@]}"; do eval printf '%s\\n' "${INCPATH}"; done)
    readarray -t NEW_INC < <(sed -n '/^\s*include(/,/)/Ip' /etc/rsyslog.conf | sed -n 's@.*file\s*=\s*"\([/[:alnum:][:punct:]]*\)".*@\1@Ip')
    readarray -t RSYSLOG_INCLUDE < <(for INCPATH in "${NEW_INC[@]}"; do eval printf '%s\\n' "${INCPATH}"; done)

    # Declare an array to hold the final list of different log file paths
    declare -a LOG_FILE_PATHS

    # Array to hold all rsyslog config entries
    RSYSLOG_CONFIGS=()
    RSYSLOG_CONFIGS=("${RSYSLOG_ETC_CONFIG}" "${RSYSLOG_INCLUDE_CONFIG[@]}" "${RSYSLOG_INCLUDE[@]}")

    # Get full list of files to be checked
    # RSYSLOG_CONFIGS may contain globs such as
    # /etc/rsyslog.d/*.conf /etc/rsyslog.d/*.frule
    # So, loop over the entries in RSYSLOG_CONFIGS and use find to get the list of included files.
    RSYSLOG_CONFIG_FILES=()
    for ENTRY in "${RSYSLOG_CONFIGS[@]}"
    do
    	# If directory, rsyslog will search for config files in recursively.
    	# However, files in hidden sub-directories or hidden files will be ignored.
    	if [ -d "${ENTRY}" ]
    	then
    		readarray -t FINDOUT < <(find "${ENTRY}" -not -path '*/.*' -type f)
    		RSYSLOG_CONFIG_FILES+=("${FINDOUT[@]}")
    	elif [ -f "${ENTRY}" ]
    	then
    		RSYSLOG_CONFIG_FILES+=("${ENTRY}")
    	else
    		echo "Invalid include object: ${ENTRY}"
    	fi
    done

    # Browse each file selected above as containing paths of log files
    # ('/etc/rsyslog.conf' and '/etc/rsyslog.d/*.conf' in the default configuration)
    for LOG_FILE in "${RSYSLOG_CONFIG_FILES[@]}"
    do
    	# From each of these files extract just particular log file path(s), thus:
    	# * Ignore lines starting with space (' '), comment ('#"), or variable syntax ('$') characters,
    	# * Ignore empty lines,
    	# * Strip quotes and closing brackets from paths.
    	# * Ignore paths that match /dev|/etc.*\.conf, as those are paths, but likely not log files
    	# * From the remaining valid rows select only fields constituting a log file path
    	# Text file column is understood to represent a log file path if and only if all of the
    	# following are met:
    	# * it contains at least one slash '/' character,
    	# * it is preceded by space
    	# * it doesn't contain space (' '), colon (':'), and semicolon (';') characters
    	# Search log file for path(s) only in case it exists!
    	if [[ -f "${LOG_FILE}" ]]
    	then
    		NORMALIZED_CONFIG_FILE_LINES=$(sed -e "/^[#|$]/d" "${LOG_FILE}")
    		LINES_WITH_PATHS=$(grep '[^/]*\s\+\S*/\S\+$' <<< "${NORMALIZED_CONFIG_FILE_LINES}")
    		FILTERED_PATHS=$(awk '{if(NF>=2&&($NF~/^\//||$NF~/^-\//)){sub(/^-\//,"/",$NF);print $NF}}' <<< "${LINES_WITH_PATHS}")
    		CLEANED_PATHS=$(sed -e "s/[\"')]//g; /\\/etc.*\.conf/d; /\\/dev\\//d" <<< "${FILTERED_PATHS}")
    		MATCHED_ITEMS=$(sed -e "/^$/d" <<< "${CLEANED_PATHS}")
    		# Since above sed command might return more than one item (delimited by newline), split
    		# the particular matches entries into new array specific for this log file
    		readarray -t ARRAY_FOR_LOG_FILE <<< "$MATCHED_ITEMS"
    		# Concatenate the two arrays - previous content of $LOG_FILE_PATHS array with
    		# items from newly created array for this log file
    		LOG_FILE_PATHS+=("${ARRAY_FOR_LOG_FILE[@]}")
    		# Delete the temporary array
    		unset ARRAY_FOR_LOG_FILE
    	fi
    done

    # Check for RainerScript action log format which might be also multiline so grep regex is a bit
    # curly:
    # extract possibly multiline action omfile expressions
    # extract File="logfile" expression
    # match only "logfile" expression
    for LOG_FILE in "${RSYSLOG_CONFIG_FILES[@]}"
    do
    	ACTION_OMFILE_LINES=$(grep -iozP "action\s*\(\s*type\s*=\s*\"omfile\"[^\)]*\)" "${LOG_FILE}")
    	OMFILE_LINES=$(echo "${ACTION_OMFILE_LINES}"| grep -iaoP "File\s*=\s*\"([/[:alnum:][:punct:]]*)\"\s*\)")
    	LOG_FILE_PATHS+=("$(echo "${OMFILE_LINES}"| grep -oE "\"([/[:alnum:][:punct:]]*)\""|tr -d "\"")")
    done

    # Ensure the correct attribute if file exists
    FILE_CMD="chmod"
    for LOG_FILE_PATH in "${LOG_FILE_PATHS[@]}"
    do
    	# Sanity check - if particular $LOG_FILE_PATH is empty string, skip it from further processing
    	if [ -z "$LOG_FILE_PATH" ]
    	then
    		continue
    	fi
    	$FILE_CMD "0640" "$LOG_FILE_PATH"
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure

    - name: Ensure System Log Files Have Correct Permissions - Set rsyslog logfile configuration
        facts
      ansible.builtin.set_fact:
        rsyslog_etc_config: /etc/rsyslog.conf
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.1
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_permissions

    - name: Ensure System Log Files Have Correct Permissions - Get IncludeConfig directive
      ansible.builtin.shell: |
        set -o pipefail
        grep -e '$IncludeConfig' {{ rsyslog_etc_config }} | cut -d ' ' -f 2 || true
      register: rsyslog_old_inc
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.1
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_permissions

    - name: Ensure System Log Files Have Correct Permissions - Get include files directives
      ansible.builtin.shell: |
        set -o pipefail
        awk '/)/{f=0} /include\(/{f=1} f{nf=gensub("^(include\\(|\\s*)file=\"(\\S+)\".*","\\2",1); if($0!=nf){print nf}}' {{ rsyslog_etc_config }} || true
      register: rsyslog_new_inc
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.1
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_permissions

    - name: Ensure System Log Files Have Correct Permissions - Aggregate rsyslog includes
      ansible.builtin.set_fact:
        include_config_output: '{{ rsyslog_old_inc.stdout_lines + rsyslog_new_inc.stdout_lines
          }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.1
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_permissions

    - name: Ensure System Log Files Have Correct Permissions - List all config files
      ansible.builtin.find:
        paths: '{{ item | dirname }}'
        patterns: '{{ item | basename }}'
        hidden: false
        follow: true
      loop: '{{ include_config_output | list + [rsyslog_etc_config] }}'
      register: rsyslog_config_files
      failed_when: false
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.1
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_permissions

    - name: Ensure System Log Files Have Correct Permissions - Extract log files old format
      ansible.builtin.shell: |
        set -o pipefail
        grep -oP '^[^(\s|#|\$)]+[\s]+.*[\s]+-?(/+[^:;\s]+);*\.*$' {{ item.1.path }} | \
        awk '{print $NF}' | \
        sed -e 's/^-//' || true
      loop: '{{ rsyslog_config_files.results | subelements(''files'') }}'
      register: log_files_old
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.1
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_permissions

    - name: Ensure System Log Files Have Correct Permissions - Extract log files new format
      ansible.builtin.shell: |
        set -o pipefail
        grep -ozP "action\s*\(\s*type\s*=\s*\"omfile\"[^\)]*\)" {{ item.1.path }} | \
        grep -aoP "File\s*=\s*\"([/[:alnum:][:punct:]]*)\"\s*\)" | \
        grep -oE "\"([/[:alnum:][:punct:]]*)\"" | \
        tr -d "\""|| true
      loop: '{{ rsyslog_config_files.results | subelements(''files'') }}'
      register: log_files_new
      changed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.1
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_permissions

    - name: Ensure System Log Files Have Correct Permissions - Sum all log files found
      ansible.builtin.set_fact:
        log_files: '{{ log_files_new.results | map(attribute=''stdout_lines'') | list
          | flatten | unique + log_files_old.results | map(attribute=''stdout_lines'')
          | list | flatten | unique }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.1
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_permissions

    - name: Ensure System Log Files Have Correct Permissions -Setup log files attribute
      ansible.builtin.file:
        path: '{{ item }}'
        mode: '0640'
        state: file
      loop: '{{ log_files | list | flatten | unique }}'
      failed_when: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.5.1
      - PCI-DSS-Req-10.5.2
      - PCI-DSSv4-10.3.1
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_files_permissions

Group Configure rsyslogd to Accept Remote Messages If Acting as a Log Server Group contains 1 rule
[ref] By default, `rsyslog` does not listen over the network for log messages. If needed, modules can be enabled to allow the rsyslog daemon to receive messages from other systems and for the system thus to act as a log server. If the system is not a log server, then lines concerning these modules should remain commented out.

#### Rule Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server [ref]

The `rsyslog` daemon should not accept remote messages unless the system acts as a log server. To ensure that it is not listening on the network, ensure any of the following lines are not found in `rsyslog` configuration files. If using legacy syntax:

    $ModLoad imtcp
    $InputTCPServerRun port
    $ModLoad imudp
    $UDPServerRun port
    $ModLoad imrelp
    $InputRELPServerRun port

If using RainerScript syntax:

    module(load="imtcp")
    module(load="imudp")
    input(type="imtcp" port="514")
    input(type="imudp" port="514")

Rationale: | Any process which receives messages from the network incurs some risk of receiving malicious messages. This risk can be eliminated for rsyslog by configuring it not to listen on the network.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_rsyslog_nolisten

Identifiers and References

References: 1, 11, 12, 13, 14, 15, 16, 18, 3, 4, 5, 6, 8, 9, APO01.06, APO11.04, APO13.01, BAI03.05, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.05, DSS03.01, DSS05.02, DSS05.04, DSS05.07, DSS06.02, MEA02.01, CCI-000318, CCI-000366, CCI-000368, CCI-001812, CCI-001813, CCI-001814, 4.2.3.4, 4.3.3.3.9, 4.3.3.4, 4.3.3.5.8, 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, 4.4.3.3, 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, 0988, 1405, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.5.1, A.12.6.2, A.12.7.1, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.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, CM-7(a), CM-7(b), CM-6(a), DE.AE-1, ID.AM-3, PR.AC-5, PR.DS-5, PR.IP-1, PR.PT-1, PR.PT-4, SRG-OS-000480-GPOS-00227, 4.2.1.7

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    legacy_regex='^\s*\$(((Input(TCP|RELP)|UDP)ServerRun)|ModLoad\s+(imtcp|imudp|imrelp))'
    rainer_regex='^\s*(module|input)\((load|type)="(imtcp|imudp)".*$'

    readarray -t legacy_targets < <(grep -l -E -r "${legacy_regex[@]}" /etc/rsyslog.conf /etc/rsyslog.d/)
    readarray -t rainer_targets < <(grep -l -E -r "${rainer_regex[@]}" /etc/rsyslog.conf /etc/rsyslog.d/)

    config_changed=false
    if [ ${#legacy_targets[@]} -gt 0 ]; then
        for target in "${legacy_targets[@]}"; do
            sed -E -i "/$legacy_regex/ s/^/# /" "$target"
        done
        config_changed=true
    fi

    if [ ${#rainer_targets[@]} -gt 0 ]; then
        for target in "${rainer_targets[@]}"; do
            sed -E -i "/$rainer_regex/ s/^/# /" "$target"
        done
        config_changed=true
    fi

    if $config_changed; then
        systemctl restart rsyslog.service
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server
        - Define Rsyslog Config Lines Regex in Legacy Syntax
      ansible.builtin.set_fact:
        rsyslog_listen_legacy_regex: ^\s*\$(((Input(TCP|RELP)|UDP)ServerRun)|ModLoad\s+(imtcp|imudp|imrelp))
      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)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_nolisten

    - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server
        - Search for Legacy Config Lines in Rsyslog Main Config File
      ansible.builtin.find:
        paths: /etc
        pattern: rsyslog.conf
        contains: '{{ rsyslog_listen_legacy_regex }}'
      register: rsyslog_listen_legacy_main_file
      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)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_nolisten

    - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server
        - Search for Legacy Config Lines in Rsyslog Include Files
      ansible.builtin.find:
        paths: /etc/rsyslog.d/
        pattern: '*.conf'
        contains: '{{ rsyslog_listen_legacy_regex }}'
      register: rsyslog_listen_legacy_include_files
      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)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_nolisten

    - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server
        - Assemble List of Config Files With Listen Lines in Legacy Syntax
      ansible.builtin.set_fact:
        rsyslog_legacy_remote_listen_files: '{{ rsyslog_listen_legacy_main_file.files
          | map(attribute=''path'') | list + rsyslog_listen_legacy_include_files.files
          | map(attribute=''path'') | list }}'
      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)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_nolisten

    - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server
        - Comment Listen Config Lines Wherever Defined Using Legacy Syntax
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: '{{ rsyslog_listen_legacy_regex }}'
        replace: '# \1'
      loop: '{{ rsyslog_legacy_remote_listen_files }}'
      register: rsyslog_listen_legacy_comment
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - rsyslog_legacy_remote_listen_files | length > 0
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_nolisten

    - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server
        - Define Rsyslog Config Lines Regex in RainerScript Syntax
      ansible.builtin.set_fact:
        rsyslog_listen_rainer_regex: ^\s*(module|input)\((load|type)="(imtcp|imudp)".*$
      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)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_nolisten

    - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server
        - Search for RainerScript Config Lines in Rsyslog Main Config File
      ansible.builtin.find:
        paths: /etc
        pattern: rsyslog.conf
        contains: '{{ rsyslog_listen_rainer_regex }}'
      register: rsyslog_rainer_remote_main_file
      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)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_nolisten

    - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server
        - Search for RainerScript Config Lines in Rsyslog Include Files
      ansible.builtin.find:
        paths: /etc/rsyslog.d/
        pattern: '*.conf'
        contains: '{{ rsyslog_listen_rainer_regex }}'
      register: rsyslog_rainer_remote_include_files
      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)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_nolisten

    - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server
        - Assemble List of Config Files With Listen Lines in RainerScript
      ansible.builtin.set_fact:
        rsyslog_rainer_remote_listen_files: '{{ rsyslog_rainer_remote_main_file.files
          | map(attribute=''path'') | list + rsyslog_rainer_remote_include_files.files
          | map(attribute=''path'') | list }}'
      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)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_nolisten

    - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server
        - Comment Listen Config Lines Wherever Defined Using RainerScript
      ansible.builtin.replace:
        path: '{{ item }}'
        regexp: '{{ rsyslog_listen_rainer_regex }}'
        replace: '# \1'
      loop: '{{ rsyslog_rainer_remote_listen_files }}'
      register: rsyslog_listen_rainer_comment
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - rsyslog_rainer_remote_listen_files | length > 0
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_nolisten

    - name: Ensure rsyslog Does Not Accept Remote Messages Unless Acting As Log Server
        - Restart Rsyslog if Any Line Were Commented Out
      ansible.builtin.service:
        name: rsyslog
        state: restarted
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - rsyslog_listen_legacy_comment is changed or rsyslog_listen_rainer_comment is changed
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_nolisten

Group Rsyslog Logs Sent To Remote Host Group contains 3 rules

[ref] If system logs are to be useful in detecting malicious activities, it is necessary to send logs to a remote server. An intruder who has compromised the root account on a system may delete the log entries which indicate that the system was attacked before they are seen by an administrator.
However, it is recommended that logs be stored on the local host in addition to being sent to the loghost, especially if `rsyslog` has been configured to use the UDP protocol to send messages over a network. UDP does not guarantee reliable delivery, and moderately busy sites will lose log messages occasionally, especially in periods of high traffic which may be the result of an attack. In addition, remote `rsyslog` messages are not authenticated in any way by default, so it is easy for an attacker to introduce spurious messages to the central log server. Also, some problems cause loss of network connectivity, which will prevent the sending of messages to the central server. For all of these reasons, it is better to store log messages both centrally and on each host, so that they can be correlated if necessary.

#### Rule Ensure Logs Sent To Remote Host [ref]

To configure rsyslog to send logs to a remote log server, open `/etc/rsyslog.conf` and read and understand the last section of the file, which describes the multiple directives necessary to activate remote logging. Along with these other directives, the system can be configured to forward its logs to a particular log server by adding or correcting one of the following lines, substituting `logcollector` appropriately. The choice of protocol depends on the environment of the system; although TCP and RELP provide more reliable message delivery, they may not be supported in all environments.
To use UDP for log message delivery:

    *.* @logcollector

To use TCP for log message delivery:

    *.* @@logcollector

To use RELP for log message delivery:

    *.* :omrelp:logcollector

There must be a resolvable DNS CNAME or Alias record set to "logcollector" for logs to be sent correctly to the centralized logging utility.
Warning: It is important to configure queues in case the client is sending log messages to a remote server. If queues are not configured, the system will stop functioning when the connection to the remote server is not available. Please consult Rsyslog documentation for more information about configuration of queues. The example configuration which should go into `/etc/rsyslog.conf` can look like the following lines:

    $ActionQueueType LinkedList
    $ActionQueueFileName queuefilename
    $ActionQueueMaxDiskSpace 1g
    $ActionQueueSaveOnShutdown on
    $ActionResumeRetryCount -1

Rationale: | A log server (loghost) receives syslog messages from one or more systems. This data can be used as an additional log source in the event a system is compromised and its local logs are suspect. Forwarding log messages to a remote loghost also provides system administrators with a centralized place to view the status of multiple hosts within the enterprise.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_rsyslog_remote_loghost

Identifiers and References

References: BP28(R7), NT28(R43), NT12(R5), 1, 13, 14, 15, 16, 2, 3, 5, 6, APO11.04, APO13.01, BAI03.05, BAI04.04, DSS05.04, DSS05.07, MEA02.01, CCI-000366, CCI-001348, CCI-000136, CCI-001851, 164.308(a)(1)(ii)(D), 164.308(a)(5)(ii)(B), 164.308(a)(5)(ii)(C), 164.308(a)(6)(ii), 164.308(a)(8), 164.310(d)(2)(iii), 164.312(b), 164.314(a)(2)(i)(C), 164.314(a)(2)(iii), 4.3.3.3.9, 4.3.3.5.8, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, SR 2.10, SR 2.11, SR 2.12, SR 2.8, SR 2.9, SR 7.1, SR 7.2, 0988, 1405, A.12.1.3, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.17.2.1, CIP-003-8 R5.2, CIP-004-6 R3.3, CM-6(a), AU-4(1), AU-9(2), PR.DS-4, PR.PT-1, FAU_GEN.1.1.c, SRG-OS-000479-GPOS-00224, SRG-OS-000480-GPOS-00227, SRG-OS-000342-GPOS-00133, 4.2.1.6, SV-230479r917883_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    rsyslog_remote_loghost_address='logcollector'

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

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

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: XCCDF Value rsyslog_remote_loghost_address # promote to variable
      set_fact:
        rsyslog_remote_loghost_address: !!str logcollector
      tags:
        - always

    - name: Set rsyslog remote loghost
      lineinfile:
        dest: /etc/rsyslog.conf
        regexp: ^\*\.\*
        line: '*.* @@{{ rsyslog_remote_loghost_address }}'
        create: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-030690
      - NIST-800-53-AU-4(1)
      - NIST-800-53-AU-9(2)
      - NIST-800-53-CM-6(a)
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - rsyslog_remote_loghost

#### Rule Configure TLS for rsyslog remote logging [ref]

Configure `rsyslog` to use Transport Layer Security (TLS) support for logging to remote server for the Forwarding Output Module in `/etc/rsyslog.conf` using action. You can use the following command:

    echo 'action(type="omfwd" protocol="tcp" Target="<remote system>" port="6514"
        StreamDriver="gtls" StreamDriverMode="1" StreamDriverAuthMode="x509/name" streamdriver.CheckExtendedKeyPurpose="on")' >> /etc/rsyslog.conf

Replace the `<remote system>` in the above command with an IP address or a host name of the remote logging server.

Rationale: | For protection of data being logged, the connection to the remote logging server needs to be authenticated and encrypted.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_rsyslog_remote_tls
Identifiers and References | References: BP28(R43), 0988, 1405, AU-9(3), CM-6(a), FCS_TLSC_EXT.1, FTP_ITC_EXT.1.1, FIA_X509_EXT.1.1, FMT_SMF_EXT.1.1, SRG-OS-000480-GPOS-00227, SRG-OS-000120-GPOS-00061

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    rsyslog_remote_loghost_address='logcollector'

    params_to_add_if_missing=("protocol" "target" "port" "StreamDriver" "StreamDriverMode" "StreamDriverAuthMode" "streamdriver.CheckExtendedKeyPurpose")
    values_to_add_if_missing=("tcp" "$rsyslog_remote_loghost_address" "6514" "gtls" "1" "x509/name" "on")
    params_to_replace_if_wrong_value=("protocol" "StreamDriver" "StreamDriverMode" "StreamDriverAuthMode" "streamdriver.CheckExtendedKeyPurpose")
    values_to_replace_if_wrong_value=("tcp" "gtls" "1" "x509/name" "on")

    files_containing_omfwd=("$(grep -ilE '^[^#]*\s*action\s*\(\s*type\s*=\s*"omfwd".*' /etc/rsyslog.conf /etc/rsyslog.d/*.conf)")
    if [ -n "${files_containing_omfwd[*]}" ]; then
        for file in "${files_containing_omfwd[@]}"; do
            for ((i=0; i<${#params_to_replace_if_wrong_value[@]}; i++)); do
                sed -i -E -e 'H;$!d;x;s/^\n//' -e "s|(\s*action\s*\(\s*type\s*=\s*[\"]omfwd[\"].*?)${params_to_replace_if_wrong_value[$i]}\s*=\s*[\"]\S*[\"](.*\))|\1${params_to_replace_if_wrong_value[$i]}=\"${values_to_replace_if_wrong_value[$i]}\"\2|gI" "$file"
            done
            for ((i=0; i<${#params_to_add_if_missing[@]}; i++)); do
                if ! grep -qPzi "(?s)\s*action\s*\(\s*type\s*=\s*[\"]omfwd[\"].*?${params_to_add_if_missing[$i]}.*?\).*" "$file"; then
                    sed -i -E -e 'H;$!d;x;s/^\n//' -e "s|(\s*action\s*\(\s*type\s*=\s*[\"]omfwd[\"])|\1\n${params_to_add_if_missing[$i]}=\"${values_to_add_if_missing[$i]}\"|gI" "$file"
                fi
            done
        done
    else
        echo "action(type=\"omfwd\" protocol=\"tcp\" Target=\"$rsyslog_remote_loghost_address\" port=\"6514\" StreamDriver=\"gtls\" StreamDriverMode=\"1\" StreamDriverAuthMode=\"x509/name\" streamdriver.CheckExtendedKeyPurpose=\"on\")"  >> /etc/rsyslog.conf
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    - name: XCCDF Value rsyslog_remote_loghost_address # promote to variable
      set_fact:
        rsyslog_remote_loghost_address: !!str logcollector
      tags:
        - always

    - name: 'Configure TLS for rsyslog remote logging: search for omfwd action directive
        in rsyslog include files'
      ansible.builtin.find:
        paths: /etc/rsyslog.d/
        pattern: '*.conf'
        contains: ^\s*action\s*\(\s*type\s*=\s*"omfwd".*
      register: rsyslog_includes_with_directive
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AU-9(3)
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_remote_tls

    - name: 'Configure TLS for rsyslog remote logging: search for omfwd action directive
        in rsyslog main config file'
      ansible.builtin.find:
        paths: /etc
        pattern: rsyslog.conf
        contains: ^\s*action\s*\(\s*type\s*=\s*"omfwd".*
      register: rsyslog_main_file_with_directive
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AU-9(3)
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_remote_tls

    - name: 'Configure TLS for rsyslog remote logging: declare Rsyslog option parameters
        to be inserted if entirely missing'
      ansible.builtin.set_fact:
        rsyslog_parameters_to_add_if_missing:
        - protocol
        - target
        - port
        - StreamDriver
        - StreamDriverMode
        - StreamDriverAuthMode
        - streamdriver.CheckExtendedKeyPurpose
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AU-9(3)
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_remote_tls

    - name: 'Configure TLS for rsyslog remote logging: declare Rsyslog option values to
        be inserted if entirely missing'
      ansible.builtin.set_fact:
        rsyslog_values_to_add_if_missing:
        - tcp
        - '{{ rsyslog_remote_loghost_address }}'
        - '6514'
        - gtls
        - '1'
        - x509/name
        - 'on'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AU-9(3)
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_remote_tls

    - name: 'Configure TLS for rsyslog remote logging: declare Rsyslog option parameters
        to be replaced if defined with wrong values'
      ansible.builtin.set_fact:
        rsyslog_parameters_to_replace_if_wrong_value:
        - protocol
        - StreamDriver
        - StreamDriverMode
        - StreamDriverAuthMode
        - streamdriver.CheckExtendedKeyPurpose
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AU-9(3)
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_remote_tls

    - name: 'Configure TLS for rsyslog remote logging: declare Rsyslog option values to
        be replaced when having wrong value'
      ansible.builtin.set_fact:
        rsyslog_values_to_replace_if_wrong_value:
        - tcp
        - gtls
        - '1'
        - x509/name
        - 'on'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AU-9(3)
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_remote_tls

    - name: 'Configure TLS for rsyslog remote logging: assemble list of files with existing
        directives'
      ansible.builtin.set_fact:
        rsyslog_files: '{{ rsyslog_includes_with_directive.files | map(attribute=''path'')
          | list + rsyslog_main_file_with_directive.files | map(attribute=''path'') |
          list }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AU-9(3)
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_remote_tls

    - name: 'Configure TLS for rsyslog remote logging: try to fix existing directives'
      block:

      - name: 'Configure TLS for rsyslog remote logging: Fix existing omfwd directives
          by adjusting the value'
        ansible.builtin.replace:
          path: '{{ item[0] }}'
          regexp: (?i)^(\s*action\s*\(\s*type\s*=\s*"omfwd"[\s\S]*)({{ item[1][0] | regex_escape()
            }}\s*=\s*"\S*")([\s\S]*\))$
          replace: \1{{ item[1][0] }}="{{ item[1][1] }}"\3
        loop: '{{ rsyslog_files | product (rsyslog_parameters_to_replace_if_wrong_value
          | zip(rsyslog_values_to_replace_if_wrong_value)) | list }}'

      - name: 'Configure TLS for rsyslog remote logging: Fix existing omfwd directives
          by adding parameter and value'
        ansible.builtin.replace:
          path: '{{ item[0] }}'
          regexp: (?i)^(\s*action\s*\(\s*type\s*=\s*"omfwd"(?:[\s\S](?!{{ item[1][0] |
            regex_escape() }}))*.)(\))$
          replace: \1 {{ item[1][0] }}="{{ item[1][1] }}" \2
        loop: '{{ rsyslog_files | product (rsyslog_parameters_to_add_if_missing | zip(rsyslog_values_to_add_if_missing))
          | list }}'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - rsyslog_includes_with_directive.matched or rsyslog_main_file_with_directive.matched
      tags:
      - NIST-800-53-AU-9(3)
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_remote_tls

    - name: 'Configure TLS for rsyslog remote logging: Add missing rsyslog directive'
      ansible.builtin.lineinfile:
        dest: /etc/rsyslog.conf
        line: action(type="omfwd" protocol="tcp" Target="{{ rsyslog_remote_loghost_address
          }}" port="6514" StreamDriver="gtls" StreamDriverMode="1" StreamDriverAuthMode="x509/name"
          streamdriver.CheckExtendedKeyPurpose="on")
        create: true
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - not rsyslog_includes_with_directive.matched and not rsyslog_main_file_with_directive.matched
      tags:
      - NIST-800-53-AU-9(3)
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - rsyslog_remote_tls

#### Rule Configure CA certificate for rsyslog remote logging [ref]

Configure CA certificate for `rsyslog` logging to remote server using Transport Layer Security (TLS) using correct path for the `DefaultNetstreamDriverCAFile` global option in `/etc/rsyslog.conf`, for example with the following command:

    echo 'global(DefaultNetstreamDriverCAFile="/etc/pki/tls/cert.pem")' >> /etc/rsyslog.conf

Replace the `/etc/pki/tls/cert.pem` in the above command with the path to the file with CA certificate generated for the purpose of remote logging.
Warning: Automatic remediation is not available as each organization has unique requirements.

Rationale:

The CA certificate needs to be set or `rsyslog.service` fails to start with

    error: ca certificate is not set, cannot continue

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_rsyslog_remote_tls_cacert
Identifiers and References | References: BP28(R43), 0988, 1405, FCS_TLSC_EXT.1, SRG-OS-000480-GPOS-00227

#### 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: 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, 4.2.1.1, SV-230477r627750_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
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 Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    package --add=rsyslog

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: Ensure rsyslog is installed
      package:
        name: rsyslog
        state: present
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-030670
      - NIST-800-53-CM-6(a)
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_rsyslog_installed

Remediation OSBuild Blueprint snippet ⇲

    [[packages]]
    name = "rsyslog"
    version = "*"

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include install_rsyslog

    class install_rsyslog {
      package { 'rsyslog':
        ensure => 'installed',
      }
    }

#### Rule Enable rsyslog Service [ref]

The `rsyslog` service provides syslog-style logging by default on Red Hat Enterprise Linux 8. 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: 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, 4.2.1.2, SV-230298r627750_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
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
Reboot: | false
Strategy: | enable

    - name: Enable service rsyslog
      block:

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

      - name: Enable service rsyslog
        systemd:
          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:
      - DISA-STIG-RHEL-08-010561
      - 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 OSBuild Blueprint snippet ⇲

    [customizations.services]
    enabled = ["rsyslog"]

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include enable_rsyslog

    class enable_rsyslog {
      service {'rsyslog':
        enable => true,
        ensure => 'running',
      }
    }

Group Network Configuration and Firewalls Group contains 5 groups and 7 rules

[ref] Most systems must be connected to a network of some sort, and this brings with it the substantial risk of network attack. This section discusses the security impact of decisions about networking which must be made when configuring a system.
This section also discusses firewalls, network access controls, and other network security frameworks, which allow system-level rules to be written that can limit an attackers' ability to connect to your system. These rules can specify that network traffic should be allowed or denied from certain IP addresses, hosts, and networks. The rules can also specify which of the system's network services are available to particular hosts or networks.

Group firewalld Group contains 2 groups and 4 rules

[ref] The dynamic firewall daemon `firewalld` provides a dynamically managed firewall with support for network “zones” to assign a level of trust to a network and its associated connections and interfaces. It has support for IPv4 and IPv6 firewall settings. It supports Ethernet bridges and has a separation of runtime and permanent configuration options. It also has an interface for services or applications to add firewall rules directly.
A graphical configuration tool, `firewall-config`, is used to configure `firewalld`, which in turn uses `iptables` tool to communicate with `Netfilter` in the kernel which implements packet filtering.
The firewall service provided by `firewalld` is dynamic rather than static because changes to the configuration can be made at anytime and are immediately implemented. There is no need to save or apply the changes. No unintended disruption of existing network connections occurs as no part of the firewall has to be reloaded.

Group Inspect and Activate Default firewalld Rules Group contains 2 rules

[ref] Firewalls can be used to separate networks into different zones based on the level of trust the user has decided to place on the devices and traffic within that network. `NetworkManager` informs firewalld to which zone an interface belongs. An interface's assigned zone can be changed by `NetworkManager` or via the `firewall-config` tool.
The zone settings in `/etc/firewalld/` are a range of preset settings which can be quickly applied to a network interface. These are the zones provided by firewalld sorted according to the default trust level of the zones from untrusted to trusted:

- `drop`

  Any incoming network packets are dropped, there is no reply. Only outgoing network connections are possible.

- `block`

  Any incoming network connections are rejected with an `icmp-host-prohibited` message for IPv4 and `icmp6-adm-prohibited` for IPv6. Only network connections initiated from within the system are possible.

- `public`

  For use in public areas. You do not trust the other computers on the network to not harm your computer. Only selected incoming connections are accepted.

- `external`

  For use on external networks with masquerading enabled especially for routers. You do not trust the other computers on the network to not harm your computer. Only selected incoming connections are accepted.

- `dmz`

  For computers in your demilitarized zone that are publicly-accessible with limited access to your internal network. Only selected incoming connections are accepted.

- `work`

  For use in work areas. You mostly trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted.

- `home`

  For use in home areas. You mostly trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted.

- `internal`

  For use on internal networks. You mostly trust the other computers on the networks to not harm your computer. Only selected incoming connections are accepted.

- `trusted`

  All network connections are accepted.

It is possible to designate one of these zones to be the default zone. When interface connections are added to `NetworkManager`, they are assigned to the default zone. On installation, the default zone in firewalld is set to be the public zone.
To find out all the settings of a zone, for example the `public zone,` enter the following command as root:

    # firewall-cmd --zone=public --list-all

Example output of this command might look like the following:

    # firewall-cmd --zone=public --list-all
    public
      interfaces:
      services: mdns dhcpv6-client ssh
      ports:
      forward-ports:
      icmp-blocks: source-quench

To view the network zones currently active, enter the following command as root:

    # firewall-cmd --get-service

The following listing displays the result of this command on common Red Hat Enterprise Linux 8 system:

    # firewall-cmd --get-service
    amanda-client bacula bacula-client dhcp dhcpv6 dhcpv6-client dns ftp
    high-availability http https imaps ipp ipp-client ipsec kerberos kpasswd
    ldap ldaps libvirt libvirt-tls mdns mountd ms-wbt mysql nfs ntp openvpn
    pmcd pmproxy pmwebapi pmwebapis pop3s postgresql proxy-dhcp radius rpc-bind
    samba samba-client smtp ssh telnet tftp tftp-client transmission-client
    vnc-server wbem-https

Finally to view the network zones that will be active after the next firewalld service reload, enter the following command as root:

    # firewall-cmd --get-service --permanent

#### Rule Install firewalld Package [ref]

The `firewalld` package can be installed with the following command:

    $ sudo yum install firewalld

Rationale:

"Firewalld" provides an easy and effective way to block/limit remote access to the system via ports, services, and protocols. Remote access services, such as those providing remote access to network devices and information systems, which lack automated control capabilities, increase risk and make remote user access management difficult at best. Remote access is access to DoD nonpublic information systems by an authorized user (or an information system) communicating through an external, non-organization-controlled network. Remote access methods include, for example, dial-up, broadband, and wireless. Red Hat Enterprise Linux 8 functionality (e.g., SSH) must be capable of taking enforcement action if the audit reveals unauthorized activity. Automated control of remote access sessions allows organizations to ensure ongoing compliance with remote access policies by enforcing connection rules of remote access applications on a variety of information system components (e.g., servers, workstations, notebook computers, smartphones, and tablets)."

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_firewalld_installed
Identifiers and References | References: CCI-002314, CM-6(a), FMT_SMF_EXT.1, SRG-OS-000096-GPOS-00050, SRG-OS-000297-GPOS-00115, SRG-OS-000298-GPOS-00116, SRG-OS-000480-GPOS-00227, SRG-OS-000480-GPOS-00232, 3.4.1.1, SV-230505r854048_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    package --add=firewalld

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: Ensure firewalld is installed
      package:
        name: firewalld
        state: present
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040100
      - NIST-800-53-CM-6(a)
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_firewalld_installed

Remediation OSBuild Blueprint snippet ⇲

    [[packages]]
    name = "firewalld"
    version = "*"

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include install_firewalld

    class install_firewalld {
      package { 'firewalld':
        ensure => 'installed',
      }
    }

#### Rule Verify firewalld Enabled [ref]

The `firewalld` service can be enabled with the following command:

    $ sudo systemctl enable firewalld.service

Rationale: | Access control methods provide the ability to enhance system security posture by restricting services and known good IP addresses and address ranges. This prevents connections from unknown hosts and protocols.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_firewalld_enabled

Identifiers and References

References: 11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.1.3, 3.4.7, CCI-000366, CCI-000382, CCI-002314, 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, CIP-003-8 R4, CIP-003-8 R5, CIP-004-6 R3, AC-4, CM-7(b), CA-3(5), SC-7(21), CM-6(a), PR.IP-1, FMT_SMF_EXT.1, 1.2.1, SRG-OS-000096-GPOS-00050, SRG-OS-000297-GPOS-00115, SRG-OS-000480-GPOS-00227, SRG-OS-000480-GPOS-00231, SRG-OS-000480-GPOS-00232, 3.4.1.4, SV-244544r854073_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && { rpm --quiet -q firewalld; }; then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" unmask 'firewalld.service'
    "$SYSTEMCTL_EXEC" start 'firewalld.service'
    "$SYSTEMCTL_EXEC" enable 'firewalld.service'

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-040101
      - NIST-800-171-3.1.3
      - NIST-800-171-3.4.7
      - NIST-800-53-AC-4
      - NIST-800-53-CA-3(5)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(21)
      - PCI-DSSv4-1.2.1
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_firewalld_enabled

    - name: Enable service firewalld
      block:

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

      - name: Enable service firewalld
        systemd:
          name: firewalld
          enabled: 'yes'
          state: started
          masked: 'no'
        when:
        - '"firewalld" in ansible_facts.packages'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - '"firewalld" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-08-040101
      - NIST-800-171-3.1.3
      - NIST-800-171-3.4.7
      - NIST-800-53-AC-4
      - NIST-800-53-CA-3(5)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-SC-7(21)
      - PCI-DSSv4-1.2.1
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_firewalld_enabled

Remediation OSBuild Blueprint snippet ⇲

    [customizations.services]
    enabled = ["firewalld"]

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include enable_firewalld

    class enable_firewalld {
      service {'firewalld':
        enable => true,
        ensure => 'running',
      }
    }

Group Strengthen the Default Ruleset Group contains 2 rules

[ref] The default rules can be strengthened. The system scripts that activate the firewall rules expect them to be defined in configuration files under the `/etc/firewalld/services` and `/etc/firewalld/zones` directories.
The following recommendations describe how to strengthen the default ruleset configuration file. An alternative to editing this configuration file is to create a shell script that makes calls to the `firewall-cmd` program to load in rules under the `/etc/firewalld/services` and `/etc/firewalld/zones` directories.
Instructions apply to both unless otherwise noted. Language and address conventions for regular firewalld rules are used throughout this section.
Warning: The program `firewall-config` allows additional services to penetrate the default firewall rules and automatically adjusts the `firewalld` ruleset(s).

#### Rule Configure the Firewalld Ports [ref]

Configure the `firewalld` ports to allow approved services to have access to the system. To configure `firewalld` to open ports, run the following command:

    firewall-cmd --permanent --add-port=port_number/tcp

To configure `firewalld` to allow access for pre-defined services, run the following command:

    firewall-cmd --permanent --add-service=service_name

Rationale:

In order to prevent unauthorized connection of devices, unauthorized transfer of information, or unauthorized tunneling (i.e., embedding of data types within data types), organizations must disable or restrict unused or unnecessary physical and logical ports/protocols on information systems.
Operating systems are capable of providing a wide variety of functions and services. Some of the functions and services provided by default may not be necessary to support essential organizational operations. Additionally, it is sometimes convenient to provide multiple services from a single component (e.g., VPN and IPS); however, doing so increases risk over limiting the services provided by one component.
To support the requirements and principles of least functionality, the operating system must support the organizational requirements, providing only essential capabilities and limiting the use of ports, protocols, and/or services to only those required, authorized, and approved to conduct official business.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_configure_firewalld_ports

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, CCI-002314, 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, 1416, 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, AC-4, CM-7(b), CA-3(5), SC-7(21), CM-6(a), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, 1.3.1, SRG-OS-000096-GPOS-00050, SRG-OS-000297-GPOS-00115, SV-230500r627750_rule

#### Rule Set Default firewalld Zone for Incoming Packets [ref]

To set the default zone to `drop` for the built-in default zone which processes incoming IPv4 and IPv6 packets, modify the following line in `/etc/firewalld/firewalld.conf` to be:

    DefaultZone=drop

Warning: To prevent denying any access to the system, automatic remediation of this control is not available. Remediation must be automated as a component of machine provisioning, or followed manually as outlined above.

Rationale: | In `firewalld` the default zone is applied only after all the applicable rules in the table are examined for a match. Setting the default zone to `drop` implements proper design for a firewall, i.e. any packets which are not explicitly permitted should not be accepted.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_set_firewalld_default_zone

Identifiers and References

References: 11, 14, 3, 9, 5.10.1, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, 3.1.3, 3.4.7, 3.13.6, CCI-000366, 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, 1416, 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, CA-3(5), CM-7(b), SC-7(23), CM-6(a), PR.IP-1, PR.PT-3, FMT_MOF_EXT.1, Req-1.4, 1.3.1, 1.5.1, SRG-OS-000480-GPOS-00227, 3.4.1.5, SV-230504r854047_rule

Group Wireless Networking Group contains 1 group and 1 rule

[ref] Wireless networking, such as 802.11 (WiFi) and Bluetooth, can present a security risk to sensitive or classified systems and networks. Wireless networking hardware is much more likely to be included in laptop or portable systems than in desktops or servers.
Removal of hardware provides the greatest assurance that the wireless capability remains disabled. Acquisition policies often include provisions to prevent the purchase of equipment that will be used in sensitive spaces and includes wireless capabilities. If it is impractical to remove the wireless hardware, and policy permits the device to enter sensitive spaces as long as wireless is disabled, efforts should instead focus on disabling wireless capability via software.

Group Disable Wireless Through Software Configuration Group contains 1 rule
[ref] If it is impossible to remove the wireless hardware from the device in question, disable as much of it as possible through software. The following methods can disable software support for wireless networking, but note that these methods do not prevent malicious software or careless users from re-activating the devices.

#### Rule Deactivate Wireless Network Interfaces [ref]

Deactivating wireless network interfaces should prevent normal usage of the wireless capability.
Configure the system to disable all wireless network interfaces with the following command:

    $ sudo nmcli radio all off

Rationale:

The use of wireless networking can introduce many different attack vectors into the organization's network. Common attack vectors such as malicious association and ad hoc networks will allow an attacker to spoof a wireless access point (AP), allowing validated systems to connect to the malicious AP and enabling the attacker to monitor and record network traffic. These malicious APs can also serve to create a man-in-the-middle attack or be used to create a denial of service to valid network resources.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_wireless_disable_interfaces

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, 3.1.16, CCI-000085, CCI-002418, CCI-002421, CCI-001443, CCI-001444, 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, 1315, 1319, 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, AC-18(a), AC-18(3), CM-7(a), CM-7(b), CM-6(a), MP-7, PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, Req-1.3.3, 1.3.3, 2.3, SRG-OS-000299-GPOS-00117, SRG-OS-000300-GPOS-00118, SRG-OS-000424-GPOS-00188, SRG-OS-000481-GPOS-000481, 3.1.4, SV-230506r627750_rule

Remediation Shell script ⇲

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

    nmcli radio all off

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | unknown

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-040110
      - NIST-800-171-3.1.16
      - NIST-800-53-AC-18(3)
      - NIST-800-53-AC-18(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MP-7
      - PCI-DSS-Req-1.3.3
      - PCI-DSSv4-1.3.3
      - PCI-DSSv4-2.3
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy
      - wireless_disable_interfaces

    - name: Ensure NetworkManager is installed
      ansible.builtin.package:
        name: '{{ item }}'
        state: present
      with_items:
      - NetworkManager
      tags:
      - DISA-STIG-RHEL-08-040110
      - NIST-800-171-3.1.16
      - NIST-800-53-AC-18(3)
      - NIST-800-53-AC-18(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MP-7
      - PCI-DSS-Req-1.3.3
      - PCI-DSSv4-1.3.3
      - PCI-DSSv4-2.3
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy
      - wireless_disable_interfaces

    - name: Deactivate Wireless Network Interfaces
      command: nmcli radio wifi off
      when: '''NetworkManager'' in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-08-040110
      - NIST-800-171-3.1.16
      - NIST-800-53-AC-18(3)
      - NIST-800-53-AC-18(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MP-7
      - PCI-DSS-Req-1.3.3
      - PCI-DSSv4-1.3.3
      - PCI-DSSv4-2.3
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy
      - wireless_disable_interfaces

#### Rule Prevent non-Privileged Users from Modifying Network Interfaces using nmcli [ref]

By default, non-privileged users are given permissions to modify networking interfaces and configurations using the `nmcli` command. Non-privileged users should not be making configuration changes to network configurations. To ensure that non-privileged users do not have permissions to make changes to the network configuration using `nmcli`, create the following configuration in `/etc/polkit-1/localauthority/20-org.d/10-nm-harden-access.pkla`:

    [Disable General User Access to NetworkManager]
    Identity=default
    Action=org.freedesktop.NetworkManager.*
    ResultAny=no
    ResultInactive=no
    ResultActive=auth_admin

Rationale: | Allowing non-privileged users to make changes to network settings can allow untrusted access, prevent system availability, and/or can lead to a compromise or attack.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_network_nmcli_permissions
Identifiers and References | References: 3.1.16, 0418, 1055, 1402, AC-18(4), CM-6(a), 1.2.8

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q polkit; then

    printf "[Disable General User Access to NetworkManager]\nIdentity=default\nAction=org.freedesktop.NetworkManager.*\nResultAny=no\nResultInactive=no\nResultActive=auth_admin\n" > /etc/polkit-1/localauthority/20-org.d/10-nm-harden-access.pkla

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-171-3.1.16
      - NIST-800-53-AC-18(4)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-1.2.8
      - low_complexity
      - low_disruption
      - medium_severity
      - network_nmcli_permissions
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure non-privileged users do not have access to nmcli
      ini_file:
        path: /etc/polkit-1/localauthority/20-org.d/10-nm-harden-access.pkla
        section: Disable General User Access to NetworkManager
        option: '{{ item.option }}'
        value: '{{ item.value }}'
        no_extra_spaces: true
        create: true
      loop:
      - option: Identity
        value: default
      - option: Action
        value: org.freedesktop.NetworkManager.*
      - option: ResultAny
        value: 'no'
      - option: ResultInactive
        value: 'no'
      - option: ResultActive
        value: auth_admin
      when: '"polkit" in ansible_facts.packages'
      tags:
      - NIST-800-171-3.1.16
      - NIST-800-53-AC-18(4)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-1.2.8
      - low_complexity
      - low_disruption
      - medium_severity
      - network_nmcli_permissions
      - no_reboot_needed
      - restrict_strategy

#### Rule Ensure System is Not Acting as a Network Sniffer [ref]

The system should not be acting as a network sniffer, which can capture all traffic on the network to which it is connected. Run the following to determine if any interface is running in promiscuous mode:

    $ ip link | grep PROMISC

Promiscuous mode of an interface can be disabled with the following command:

    $ sudo ip link set dev device_name multicast off promisc off

Rationale:

Network interfaces in promiscuous mode allow for the capture of all network traffic visible to the system. If unauthorized individuals can access these applications, it may allow them to collect information such as logon IDs, passwords, and key exchanges between systems.
If the system is being used to perform a network troubleshooting function, the use of these tools must be documented with the Information Systems Security Manager (ISSM) and restricted to only authorized personnel.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_network_sniffer_disabled

Identifiers and References

References: 1, 11, 14, 3, 9, APO11.06, APO12.06, BAI03.10, BAI09.01, BAI09.02, BAI09.03, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.05, DSS04.05, DSS05.02, DSS05.05, DSS06.06, CCI-000366, 4.2.3.4, 4.3.3.3.7, 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.4, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, SR 7.8, A.11.1.2, A.11.2.4, A.11.2.5, A.11.2.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.16.1.6, A.8.1.1, A.8.1.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), CM-7(2), MA-3, DE.DP-5, ID.AM-1, PR.IP-1, PR.MA-1, PR.PT-3, 1.4.5, SRG-OS-000480-GPOS-00227, SV-230554r627750_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    for interface in $(ip link show | grep -E '^[0-9]' | cut -d ":" -f 2); do
        ip link set dev $interface multicast off promisc off
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Ensure System is Not Acting as a Network Sniffer - Gather network interfaces
      ansible.builtin.command:
        cmd: ip link show
      register: network_interfaces
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040330
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(2)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MA-3
      - PCI-DSSv4-1.4.5
      - low_complexity
      - low_disruption
      - medium_severity
      - network_sniffer_disabled
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure System is Not Acting as a Network Sniffer - Disable promiscuous mode
      ansible.builtin.command:
        cmd: ip link set dev {{ item.split(':')[1] }} multicast off promisc off
      loop: '{{ network_interfaces.stdout_lines }}'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - item.split(':') | length == 3
      tags:
      - DISA-STIG-RHEL-08-040330
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(2)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-MA-3
      - PCI-DSSv4-1.4.5
      - low_complexity
      - low_disruption
      - medium_severity
      - network_sniffer_disabled
      - no_reboot_needed
      - restrict_strategy

Group File Permissions and Masks Group contains 5 groups and 19 rules

[ref] Traditional Unix security relies heavily on file and directory permissions to prevent unauthorized users from reading or modifying files to which they should not have access.
Several of the commands in this section search filesystems for files or directories with certain characteristics, and are intended to be run on every local partition on a given system. When the variable PART appears in one of the commands below, it means that the command is intended to be run repeatedly, with the name of each local partition substituted for PART in turn.
The following command prints a list of all xfs partitions on the local system, which is the default filesystem for Red Hat Enterprise Linux 8 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 1 group and 8 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.
Group Verify File Permissions Within Some Important Directories Group contains 4 rules
[ref] Some directories contain files whose confidentiality or integrity is notably important and may also be susceptible to misconfiguration over time, particularly if unpackaged software is installed. As such, an argument exists to verify that files' permissions within these directories remain configured correctly and restrictively.

#### Rule Verify that System Executables Have Root Ownership [ref]

System executables are stored in the following directories by default:

    /bin
    /sbin
    /usr/bin
    /usr/libexec
    /usr/local/bin
    /usr/local/sbin
    /usr/sbin

All files in these directories should be owned by the `root` user. If any file FILE in these directories is found to be owned by a user other than root, correct its ownership with the following command:

    $ sudo chown root FILE

Rationale: | System binaries are executed by privileged users as well as system services, and restrictive permissions are necessary to ensure that their execution of these programs cannot be co-opted.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_ownership_binary_dirs

Identifiers and References

References: 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-001499, 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-5(6), CM-5(6).1, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, SRG-OS-000259-GPOS-00100, SV-230258r627750_rule

Remediation Shell script ⇲

    find /bin/ \
    /usr/bin/ \
    /usr/local/bin/ \
    /sbin/ \
    /usr/sbin/ \
    /usr/local/sbin/ \
    /usr/libexec \
    \! -user root -execdir chown root {} \;

Remediation Ansible snippet ⇲

Complexity: | medium
Disruption: | medium
Reboot: | false
Strategy: | restrict

    - name: Read list of system executables without root ownership
      command: find /bin/ /usr/bin/ /usr/local/bin/ /sbin/ /usr/sbin/ /usr/local/sbin/
        /usr/libexec \! -user root
      register: no_root_system_executables
      changed_when: false
      failed_when: false
      check_mode: false
      tags:
      - DISA-STIG-RHEL-08-010310
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - file_ownership_binary_dirs
      - medium_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set ownership to root of system executables
      file:
        path: '{{ item }}'
        owner: root
      with_items: '{{ no_root_system_executables.stdout_lines }}'
      when: no_root_system_executables.stdout_lines | length > 0
      tags:
      - DISA-STIG-RHEL-08-010310
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - file_ownership_binary_dirs
      - medium_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Verify that Shared Library Files Have Root Ownership [ref]

System-wide shared library files, which are linked to executables during process load time or run time, are stored in the following directories by default:

    /lib
    /lib64
    /usr/lib
    /usr/lib64

Kernel modules, which can be added to the kernel during runtime, are also stored in `/lib/modules`. All files in these directories should be owned by the `root` user. If the directory, or any file in these directories, is found to be owned by a user other than root correct its ownership with the following command:

    $ sudo chown root FILE

Rationale: | Files from shared library directories are loaded into the address space of processes (including privileged ones) or of the kernel itself at runtime. Proper ownership is necessary to protect the integrity of the system.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_ownership_library_dirs

Identifiers and References

References: 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-001499, 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-5(6), CM-5(6).1, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, SRG-OS-000259-GPOS-00100, SV-230261r627750_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    find /lib/  -type f ! -uid 0 -regex '^.*$' -exec chown 0 {} \;

    find /lib64/  -type f ! -uid 0 -regex '^.*$' -exec chown 0 {} \;

    find /usr/lib/  -type f ! -uid 0 -regex '^.*$' -exec chown 0 {} \;

    find /usr/lib64/  -type f ! -uid 0 -regex '^.*$' -exec chown 0 {} \;

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    - name: Find /lib/ file(s) matching ^.*$ recursively
      command: find -H /lib/  -type f ! -uid 0 -regex "^.*$"
      register: files_found
      changed_when: false
      failed_when: false
      check_mode: false
      tags:
      - DISA-STIG-RHEL-08-010340
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - file_ownership_library_dirs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure owner on /lib/ file(s) matching ^.*$
      file:
        path: '{{ item }}'
        owner: '0'
        state: file
      with_items:
      - '{{ files_found.stdout_lines }}'
      tags:
      - DISA-STIG-RHEL-08-010340
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - file_ownership_library_dirs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Find /lib64/ file(s) matching ^.*$ recursively
      command: find -H /lib64/  -type f ! -uid 0 -regex "^.*$"
      register: files_found
      changed_when: false
      failed_when: false
      check_mode: false
      tags:
      - DISA-STIG-RHEL-08-010340
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - file_ownership_library_dirs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure owner on /lib64/ file(s) matching ^.*$
      file:
        path: '{{ item }}'
        owner: '0'
        state: file
      with_items:
      - '{{ files_found.stdout_lines }}'
      tags:
      - DISA-STIG-RHEL-08-010340
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - file_ownership_library_dirs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Find /usr/lib/ file(s) matching ^.*$ recursively
      command: find -H /usr/lib/  -type f ! -uid 0 -regex "^.*$"
      register: files_found
      changed_when: false
      failed_when: false
      check_mode: false
      tags:
      - DISA-STIG-RHEL-08-010340
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - file_ownership_library_dirs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure owner on /usr/lib/ file(s) matching ^.*$
      file:
        path: '{{ item }}'
        owner: '0'
        state: file
      with_items:
      - '{{ files_found.stdout_lines }}'
      tags:
      - DISA-STIG-RHEL-08-010340
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - file_ownership_library_dirs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Find /usr/lib64/ file(s) matching ^.*$ recursively
      command: find -H /usr/lib64/  -type f ! -uid 0 -regex "^.*$"
      register: files_found
      changed_when: false
      failed_when: false
      check_mode: false
      tags:
      - DISA-STIG-RHEL-08-010340
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - file_ownership_library_dirs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Ensure owner on /usr/lib64/ file(s) matching ^.*$
      file:
        path: '{{ item }}'
        owner: '0'
        state: file
      with_items:
      - '{{ files_found.stdout_lines }}'
      tags:
      - DISA-STIG-RHEL-08-010340
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - file_ownership_library_dirs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

#### Rule Verify that System Executables Have Restrictive Permissions [ref]

System executables are stored in the following directories by default:

    /bin
    /sbin
    /usr/bin
    /usr/libexec
    /usr/local/bin
    /usr/local/sbin
    /usr/sbin

All files in these directories should not be group-writable or world-writable. If any file FILE in these directories is found to be group-writable or world-writable, correct its permission with the following command:

    $ sudo chmod go-w FILE

Rationale: | System binaries are executed by privileged users, as well as system services, and restrictive permissions are necessary to ensure execution of these programs cannot be co-opted.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_permissions_binary_dirs

Identifiers and References

References: 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-001499, 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-5(6), CM-5(6).1, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, SRG-OS-000259-GPOS-00100, SV-230257r792862_rule

Remediation Shell script ⇲

    DIRS="/bin /usr/bin /usr/local/bin /sbin /usr/sbin /usr/local/sbin /usr/libexec"
    for dirPath in $DIRS; do
    	find "$dirPath" -perm /022 -exec chmod go-w '{}' \;
    done

Remediation Ansible snippet ⇲

Complexity: | medium
Disruption: | medium
Reboot: | false
Strategy: | restrict

    - name: Read list of world and group writable system executables
      ansible.builtin.command: find /bin /usr/bin /usr/local/bin /sbin /usr/sbin /usr/local/sbin
        /usr/libexec -perm /022 -type f
      register: world_writable_library_files
      changed_when: false
      failed_when: false
      check_mode: false
      tags:
      - DISA-STIG-RHEL-08-010300
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - file_permissions_binary_dirs
      - medium_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Remove world/group writability of system executables
      ansible.builtin.file:
        path: '{{ item }}'
        mode: go-w
        state: file
      with_items: '{{ world_writable_library_files.stdout_lines }}'
      when: world_writable_library_files.stdout_lines | length > 0
      tags:
      - DISA-STIG-RHEL-08-010300
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - file_permissions_binary_dirs
      - medium_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Verify that Shared Library Files Have Restrictive Permissions [ref]

System-wide shared library files, which are linked to executables during process load time or run time, are stored in the following directories by default:

    /lib
    /lib64
    /usr/lib
    /usr/lib64

Kernel modules, which can be added to the kernel during runtime, are stored in `/lib/modules`. All files in these directories should not be group-writable or world-writable. If any file in these directories is found to be group-writable or world-writable, correct its permission with the following command:

    $ sudo chmod go-w FILE

Rationale: | Files from shared library directories are loaded into the address space of processes (including privileged ones) or of the kernel itself at runtime. Restrictive permissions are necessary to protect the integrity of the system.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_permissions_library_dirs

Identifiers and References

References: 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-001499, 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), CM-5(6), CM-5(6).1, AC-6(1), PR.AC-4, PR.DS-5, SRG-OS-000259-GPOS-00100, SV-230260r792867_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    find -H /lib/  -perm /g+w,o+w  -type f -regex '^.*$' -exec chmod g-w,o-w {} \;

    find -H /lib64/  -perm /g+w,o+w  -type f -regex '^.*$' -exec chmod g-w,o-w {} \;

    find -H /usr/lib/  -perm /g+w,o+w  -type f -regex '^.*$' -exec chmod g-w,o-w {} \;

    find -H /usr/lib64/  -perm /g+w,o+w  -type f -regex '^.*$' -exec chmod g-w,o-w {} \;

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    - name: Find /lib/ file(s) recursively
      command: find -H /lib/  -perm /g+w,o+w  -type f -regex "^.*$"
      register: files_found
      changed_when: false
      failed_when: false
      check_mode: false
      tags:
      - DISA-STIG-RHEL-08-010330
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - file_permissions_library_dirs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Set permissions for /lib/ file(s)
      file:
        path: '{{ item }}'
        mode: g-w,o-w
        state: file
      with_items:
      - '{{ files_found.stdout_lines }}'
      tags:
      - DISA-STIG-RHEL-08-010330
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - file_permissions_library_dirs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Find /lib64/ file(s) recursively
      command: find -H /lib64/  -perm /g+w,o+w  -type f -regex "^.*$"
      register: files_found
      changed_when: false
      failed_when: false
      check_mode: false
      tags:
      - DISA-STIG-RHEL-08-010330
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - file_permissions_library_dirs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Set permissions for /lib64/ file(s)
      file:
        path: '{{ item }}'
        mode: g-w,o-w
        state: file
      with_items:
      - '{{ files_found.stdout_lines }}'
      tags:
      - DISA-STIG-RHEL-08-010330
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - file_permissions_library_dirs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Find /usr/lib/ file(s) recursively
      command: find -H /usr/lib/  -perm /g+w,o+w  -type f -regex "^.*$"
      register: files_found
      changed_when: false
      failed_when: false
      check_mode: false
      tags:
      - DISA-STIG-RHEL-08-010330
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - file_permissions_library_dirs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Set permissions for /usr/lib/ file(s)
      file:
        path: '{{ item }}'
        mode: g-w,o-w
        state: file
      with_items:
      - '{{ files_found.stdout_lines }}'
      tags:
      - DISA-STIG-RHEL-08-010330
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - file_permissions_library_dirs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Find /usr/lib64/ file(s) recursively
      command: find -H /usr/lib64/  -perm /g+w,o+w  -type f -regex "^.*$"
      register: files_found
      changed_when: false
      failed_when: false
      check_mode: false
      tags:
      - DISA-STIG-RHEL-08-010330
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - file_permissions_library_dirs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Set permissions for /usr/lib64/ file(s)
      file:
        path: '{{ item }}'
        mode: g-w,o-w
        state: file
      with_items:
      - '{{ files_found.stdout_lines }}'
      tags:
      - DISA-STIG-RHEL-08-010330
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-5(6)
      - NIST-800-53-CM-5(6).1
      - NIST-800-53-CM-6(a)
      - configure_strategy
      - file_permissions_library_dirs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

#### 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, 2.2.6, SRG-OS-000138-GPOS-00069, 6.1.2, SV-230243r792857_rule

Remediation Shell script ⇲

    df --local -P | awk '{if (NR!=1) print $6}' \
    | xargs -I '$6' find '$6' -xdev -type d \
    \( -perm -0002 -a ! -perm -1000 \) 2>/dev/null \
    -exec chmod a+t {} +

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Define Excluded
        (Non-Local) File Systems and Paths
      ansible.builtin.set_fact:
        excluded_fstypes:
        - afs
        - ceph
        - cifs
        - smb3
        - smbfs
        - sshfs
        - ncpfs
        - ncp
        - nfs
        - nfs4
        - gfs
        - gfs2
        - glusterfs
        - gpfs
        - pvfs2
        - ocfs2
        - lustre
        - davfs
        - fuse.sshfs
        excluded_paths:
        - dev
        - proc
        - run
        - sys
        search_paths: []
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Find Relevant
        Root Directories Ignoring Pre-Defined Excluded Paths
      ansible.builtin.find:
        paths: /
        file_type: directory
        excludes: '{{ excluded_paths }}'
        hidden: true
        recurse: false
      register: result_relevant_root_dirs
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Include
        Relevant Root Directories in a List of Paths to be Searched
      ansible.builtin.set_fact:
        search_paths: '{{ search_paths | union([item.path]) }}'
      loop: '{{ result_relevant_root_dirs.files }}'
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Increment
        Search Paths List with Local Partitions Mount Points
      ansible.builtin.set_fact:
        search_paths: '{{ search_paths | union([item.mount]) }}'
      loop: '{{ ansible_mounts }}'
      when:
      - item.fstype not in excluded_fstypes
      - item.mount != '/'
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Increment
        Search Paths List with Local NFS File System Targets
      ansible.builtin.set_fact:
        search_paths: '{{ search_paths | union([item.device.split('':'')[1]]) }}'
      loop: '{{ ansible_mounts }}'
      when: item.device is search("localhost:")
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Define Rule
        Specific Facts
      ansible.builtin.set_fact:
        world_writable_dirs: []
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Find All
        Uncompliant Directories in Local File Systems
      ansible.builtin.command:
        cmd: find {{ item }} -xdev -type d ( -perm -0002 -a ! -perm -1000 )
      loop: '{{ search_paths }}'
      changed_when: false
      register: result_found_dirs
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Create List
        of World Writable Directories Without Sticky Bit
      ansible.builtin.set_fact:
        world_writable_dirs: '{{ world_writable_dirs | union(item.stdout_lines) | list
          }}'
      loop: '{{ result_found_dirs.results }}'
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Ensure Sticky
        Bit is Set on Local World Writable Directories
      ansible.builtin.file:
        path: '{{ item }}'
        mode: a+t
      loop: '{{ world_writable_dirs }}'
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - 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, 6.1.15

#### 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, 6.1.14

#### 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, 2.2.6, 6.1.11

Remediation Shell script ⇲

    find / -xdev -type f -perm -002 -exec chmod o-w {} \;

Group Restrict Partition Mount Options Group contains 3 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, 1.1.8.1, SV-230508r854049_rule

Remediation Shell script ⇲

Reboot: | false

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    function perform_remediation {

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

        # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
        if ! grep -q "$mount_point_match_regexp" /etc/fstab; then
            # runtime opts without some automatic kernel/userspace-added defaults
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                        | sed -E "s/(rw|defaults|seclabel|nodev)(,|$)//g;s/,$//")
            [ "$previous_mount_opts" ] && previous_mount_opts+=","
            # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in
            # fstab as "block".  The next variable is to satisfy shellcheck SC2050.
            fs_type="tmpfs"
            if [  "$fs_type" == "iso9660" ] ; then
                previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts")
            fi
            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 -q "nodev"; then
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
            sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nodev|" /etc/fstab
        fi

        if mkdir -p "/dev/shm"; then
            if mountpoint -q "/dev/shm"; then
                mount -o remount --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
Reboot: | false
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-RHEL-08-040120
      - 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-RHEL-08-040120
      - 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-RHEL-08-040120
      - 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-RHEL-08-040120
      - 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-RHEL-08-040120
      - 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 noexec Option to /dev/shm [ref]

The `noexec` mount option can be used to prevent binaries from being executed out of `/dev/shm`. It can be dangerous to allow the execution of binaries from world-writable temporary storage directories such as `/dev/shm`. Add the `noexec` option to the fourth column of `/etc/fstab` for the line which controls mounting of `/dev/shm`.
Rationale: | Allowing users to execute binaries from world-writable directories such as `/dev/shm` can expose the system to potential compromise.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_mount_option_dev_shm_noexec

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, 1.1.8.2, SV-230510r854051_rule

Remediation Shell script ⇲

Reboot: | false

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    function perform_remediation {

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

        # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
        if ! grep -q "$mount_point_match_regexp" /etc/fstab; then
            # runtime opts without some automatic kernel/userspace-added defaults
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                        | sed -E "s/(rw|defaults|seclabel|noexec)(,|$)//g;s/,$//")
            [ "$previous_mount_opts" ] && previous_mount_opts+=","
            # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in
            # fstab as "block".  The next variable is to satisfy shellcheck SC2050.
            fs_type="tmpfs"
            if [  "$fs_type" == "iso9660" ] ; then
                previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts")
            fi
            echo "tmpfs /dev/shm tmpfs defaults,${previous_mount_opts}noexec 0 0" >> /etc/fstab
        # If the mount_opt option is not already in the mount point's /etc/fstab entry, add it
        elif ! grep "$mount_point_match_regexp" /etc/fstab | grep -q "noexec"; then
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
            sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,noexec|" /etc/fstab
        fi

        if mkdir -p "/dev/shm"; then
            if mountpoint -q "/dev/shm"; then
                mount -o remount --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
Reboot: | false
Strategy: | configure

    - name: 'Add noexec 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-RHEL-08-040122
      - 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_noexec
      - no_reboot_needed

    - name: 'Add noexec 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-RHEL-08-040122
      - 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_noexec
      - no_reboot_needed

    - name: 'Add noexec 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-RHEL-08-040122
      - 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_noexec
      - no_reboot_needed

    - name: 'Add noexec Option to /dev/shm: Make sure noexec option is part of the to
        /dev/shm options'
      set_fact:
        mount_info: '{{ mount_info | combine( {''options'':''''~mount_info.options~'',noexec''
          }) }}'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - mount_info is defined and "noexec" not in mount_info.options
      tags:
      - DISA-STIG-RHEL-08-040122
      - 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_noexec
      - no_reboot_needed

    - name: 'Add noexec Option to /dev/shm: Ensure /dev/shm is mounted with noexec 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-RHEL-08-040122
      - 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_noexec
      - 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, 1.1.8.3, SV-230509r854050_rule

Remediation Shell script ⇲

Reboot: | false

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    function perform_remediation {

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

        # If the mount point is not in /etc/fstab, get previous mount options from /etc/mtab
        if ! grep -q "$mount_point_match_regexp" /etc/fstab; then
            # runtime opts without some automatic kernel/userspace-added defaults
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/mtab | head -1 |  awk '{print $4}' \
                        | sed -E "s/(rw|defaults|seclabel|nosuid)(,|$)//g;s/,$//")
            [ "$previous_mount_opts" ] && previous_mount_opts+=","
            # In iso9660 filesystems mtab could describe a "blocksize" value, this should be reflected in
            # fstab as "block".  The next variable is to satisfy shellcheck SC2050.
            fs_type="tmpfs"
            if [  "$fs_type" == "iso9660" ] ; then
                previous_mount_opts=$(sed 's/blocksize=/block=/' <<< "$previous_mount_opts")
            fi
            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 -q "nosuid"; then
            previous_mount_opts=$(grep "$mount_point_match_regexp" /etc/fstab | awk '{print $4}')
            sed -i "s|\(${mount_point_match_regexp}.*${previous_mount_opts}\)|\1,nosuid|" /etc/fstab
        fi

        if mkdir -p "/dev/shm"; then
            if mountpoint -q "/dev/shm"; then
                mount -o remount --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
Reboot: | false
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-RHEL-08-040121
      - 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-RHEL-08-040121
      - 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-RHEL-08-040121
      - 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-RHEL-08-040121
      - 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-RHEL-08-040121
      - 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 Restrict Programs from Dangerous Execution Patterns Group contains 1 group and 8 rules
[ref] The recommendations in this section are designed to ensure that the system's features to protect against potentially dangerous program execution are activated. These protections are applied at the system initialization or kernel level, and defend against certain types of badly-configured or compromised programs.
Group Enable ExecShield Group contains 3 rules

[ref] ExecShield describes kernel features that provide protection against exploitation of memory corruption errors such as buffer overflows. These features include random placement of the stack and other memory regions, prevention of execution in memory that should only hold data, and special handling of text buffers. These protections are enabled by default on 32-bit systems and controlled through `sysctl` variables `kernel.exec-shield` and `kernel.randomize_va_space`. On the latest 64-bit systems, `kernel.exec-shield` cannot be enabled or disabled with `sysctl`.

#### Rule Enable ExecShield via sysctl [ref]

By default on Red Hat Enterprise Linux 8 64-bit systems, ExecShield is enabled and can only be disabled if the hardware does not support ExecShield or is disabled in `/etc/default/grub`.

Rationale:

ExecShield uses the segmentation feature on all x86 systems to prevent execution in memory higher than a certain address. It writes an address as a limit in the code segment descriptor, to control where code can be executed, on a per-process basis. When the kernel places a process's memory regions such as the stack and heap higher than this address, the hardware prevents execution in that address range. This is enabled by default on the latest Red Hat and Fedora systems if supported by the hardware.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_kernel_exec_shield
Identifiers and References | References: 12, 15, 8, APO13.01, DSS05.02, 3.1.7, CCI-002530, 164.308(a)(1)(ii)(D), 164.308(a)(3), 164.308(a)(4), 164.310(b), 164.310(c), 164.312(a), 164.312(e), SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.13.1.1, A.13.2.1, A.14.1.3, SC-39, CM-6(a), PR.PT-4, SRG-OS-000433-GPOS-00192

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    grubby --update-kernel=ALL --remove-args=noexec --env=/boot/grub2/grubenv

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    - name: Update grub defaults and the bootloader menu
      command: /sbin/grubby --update-kernel=ALL --remove-args="noexec"
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-171-3.1.7
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-39
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy
      - sysctl_kernel_exec_shield

#### Rule Restrict Exposed Kernel Pointer Addresses Access [ref]

To set the runtime status of the `kernel.kptr_restrict` kernel parameter, run the following command:

    $ sudo sysctl -w kernel.kptr_restrict=1

To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:

    kernel.kptr_restrict = 1

Rationale:

Exposing kernel pointers (through procfs or `seq_printf()`) exposes kernel writeable structures which may contain functions pointers. If a write vulnerability occurs in the kernel, allowing write access to any of this structure, the kernel can be compromised. This option disallow any program without the CAP_SYSLOG capability to get the addresses of kernel pointers by replacing them with 0.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_kernel_kptr_restrict

Identifiers and References

References: BP28(R23), CCI-002824, CCI-000366, CIP-002-5 R1.1, CIP-002-5 R1.2, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 4.1, CIP-004-6 4.2, CIP-004-6 R2.2.3, CIP-004-6 R2.2.4, CIP-004-6 R2.3, CIP-004-6 R4, CIP-005-6 R1, CIP-005-6 R1.1, CIP-005-6 R1.2, CIP-007-3 R3, CIP-007-3 R3.1, CIP-007-3 R5.1, CIP-007-3 R5.1.2, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, CIP-007-3 R8.4, CIP-009-6 R.1.1, CIP-009-6 R4, SC-30, SC-30(2), SC-30(5), CM-6(a), SRG-OS-000132-GPOS-00067, SRG-OS-000433-GPOS-00192, SRG-OS-000480-GPOS-00227, SV-230547r858826_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    # Comment out any occurrences of kernel.kptr_restrict from /etc/sysctl.d/*.conf files

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

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

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

    SYSCONFIG_FILE="/etc/sysctl.conf"

    sysctl_kernel_kptr_restrict_value='1'

    #
    # Set runtime for kernel.kptr_restrict
    #
    /sbin/sysctl -q -n -w kernel.kptr_restrict="$sysctl_kernel_kptr_restrict_value"

    #
    # If kernel.kptr_restrict present in /etc/sysctl.conf, change value to appropriate value
    #	else, add "kernel.kptr_restrict = value" to /etc/sysctl.conf
    #

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

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

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    - name: List /etc/sysctl.d/*.conf files
      find:
        paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
        contains: ^[\s]*kernel.kptr_restrict.*$
        patterns: '*.conf'
        file_type: any
      register: find_sysctl_d
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040283
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-30
      - NIST-800-53-SC-30(2)
      - NIST-800-53-SC-30(5)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_kernel_kptr_restrict

    - name: Comment out any occurrences of kernel.kptr_restrict from config files
      replace:
        path: '{{ item.path }}'
        regexp: ^[\s]*kernel.kptr_restrict
        replace: '#kernel.kptr_restrict'
      loop: '{{ find_sysctl_d.files }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040283
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-30
      - NIST-800-53-SC-30(2)
      - NIST-800-53-SC-30(5)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_kernel_kptr_restrict
    - name: XCCDF Value sysctl_kernel_kptr_restrict_value # promote to variable
      set_fact:
        sysctl_kernel_kptr_restrict_value: !!str 1
      tags:
        - always

    - name: Ensure sysctl kernel.kptr_restrict is set
      sysctl:
        name: kernel.kptr_restrict
        value: '{{ sysctl_kernel_kptr_restrict_value }}'
        sysctl_file: /etc/sysctl.conf
        state: present
        reload: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040283
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-30
      - NIST-800-53-SC-30(2)
      - NIST-800-53-SC-30(5)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_kernel_kptr_restrict

Remediation Kubernetes snippet ⇲

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,kernel.kptr_restrict%3D1%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_kernel_kptr_restrict.conf
            overwrite: true

#### Rule Enable Randomized Layout of Virtual Address Space [ref]

To set the runtime status of the `kernel.randomize_va_space` kernel parameter, run the following command:

    $ sudo sysctl -w kernel.randomize_va_space=2

To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:

    kernel.randomize_va_space = 2

Rationale: | Address space layout randomization (ASLR) makes it more difficult for an attacker to predict the location of attack code they have introduced into a process's address space during an attempt at exploitation. Additionally, ASLR makes it more difficult for an attacker to know the location of existing code in order to re-purpose it using return oriented programming (ROP) techniques.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_kernel_randomize_va_space

Identifiers and References

References: BP28(R23), 3.1.7, CCI-000366, CCI-002824, 164.308(a)(1)(ii)(D), 164.308(a)(3), 164.308(a)(4), 164.310(b), 164.310(c), 164.312(a), 164.312(e), CIP-002-5 R1.1, CIP-002-5 R1.2, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 4.1, CIP-004-6 4.2, CIP-004-6 R2.2.3, CIP-004-6 R2.2.4, CIP-004-6 R2.3, CIP-004-6 R4, CIP-005-6 R1, CIP-005-6 R1.1, CIP-005-6 R1.2, CIP-007-3 R3, CIP-007-3 R3.1, CIP-007-3 R5.1, CIP-007-3 R5.1.2, CIP-007-3 R5.1.3, CIP-007-3 R5.2.1, CIP-007-3 R5.2.3, CIP-007-3 R8.4, CIP-009-6 R.1.1, CIP-009-6 R4, SC-30, SC-30(2), CM-6(a), Req-2.2.1, 3.3.1, SRG-OS-000433-GPOS-00193, SRG-OS-000480-GPOS-00227, SRG-APP-000450-CTR-001105, 1.5.3, SV-230280r858767_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    # Comment out any occurrences of kernel.randomize_va_space from /etc/sysctl.d/*.conf files

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

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

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

    SYSCONFIG_FILE="/etc/sysctl.conf"

    #
    # Set runtime for kernel.randomize_va_space
    #
    /sbin/sysctl -q -n -w kernel.randomize_va_space="2"

    #
    # If kernel.randomize_va_space present in /etc/sysctl.conf, change value to "2"
    #	else, add "kernel.randomize_va_space = 2" to /etc/sysctl.conf
    #

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

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

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    - name: List /etc/sysctl.d/*.conf files
      find:
        paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
        contains: ^[\s]*kernel.randomize_va_space.*$
        patterns: '*.conf'
        file_type: any
      register: find_sysctl_d
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010430
      - NIST-800-171-3.1.7
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-30
      - NIST-800-53-SC-30(2)
      - PCI-DSS-Req-2.2.1
      - PCI-DSSv4-3.3.1
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_kernel_randomize_va_space

    - name: Comment out any occurrences of kernel.randomize_va_space from config files
      replace:
        path: '{{ item.path }}'
        regexp: ^[\s]*kernel.randomize_va_space
        replace: '#kernel.randomize_va_space'
      loop: '{{ find_sysctl_d.files }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010430
      - NIST-800-171-3.1.7
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-30
      - NIST-800-53-SC-30(2)
      - PCI-DSS-Req-2.2.1
      - PCI-DSSv4-3.3.1
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_kernel_randomize_va_space

    - name: Ensure sysctl kernel.randomize_va_space is set to 2
      sysctl:
        name: kernel.randomize_va_space
        value: '2'
        sysctl_file: /etc/sysctl.conf
        state: present
        reload: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010430
      - NIST-800-171-3.1.7
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-30
      - NIST-800-53-SC-30(2)
      - PCI-DSS-Req-2.2.1
      - PCI-DSSv4-3.3.1
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_kernel_randomize_va_space

Remediation Kubernetes snippet ⇲

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,kernel.randomize_va_space%3D2%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_kernel_randomize_va_space.conf
            overwrite: true

#### Rule Restrict Access to Kernel Message Buffer [ref]

To set the runtime status of the `kernel.dmesg_restrict` kernel parameter, run the following command:

    $ sudo sysctl -w kernel.dmesg_restrict=1

To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:

    kernel.dmesg_restrict = 1

Rationale: | Unprivileged access to the kernel syslog can expose sensitive kernel address information.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_kernel_dmesg_restrict
Identifiers and References | References: BP28(R23), 3.1.5, CCI-001090, CCI-001314, 164.308(a)(1)(ii)(D), 164.308(a)(3), 164.308(a)(4), 164.310(b), 164.310(c), 164.312(a), 164.312(e), SI-11(a), SI-11(b), SRG-OS-000132-GPOS-00067, SRG-OS-000138-GPOS-00069, SRG-APP-000243-CTR-000600, SV-230269r858756_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    # Comment out any occurrences of kernel.dmesg_restrict from /etc/sysctl.d/*.conf files

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

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

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

    SYSCONFIG_FILE="/etc/sysctl.conf"

    #
    # Set runtime for kernel.dmesg_restrict
    #
    /sbin/sysctl -q -n -w kernel.dmesg_restrict="1"

    #
    # If kernel.dmesg_restrict present in /etc/sysctl.conf, change value to "1"
    #	else, add "kernel.dmesg_restrict = 1" to /etc/sysctl.conf
    #

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

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

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    - name: List /etc/sysctl.d/*.conf files
      find:
        paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
        contains: ^[\s]*kernel.dmesg_restrict.*$
        patterns: '*.conf'
        file_type: any
      register: find_sysctl_d
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010375
      - NIST-800-171-3.1.5
      - NIST-800-53-SI-11(a)
      - NIST-800-53-SI-11(b)
      - disable_strategy
      - low_complexity
      - low_severity
      - medium_disruption
      - reboot_required
      - sysctl_kernel_dmesg_restrict

    - name: Comment out any occurrences of kernel.dmesg_restrict from config files
      replace:
        path: '{{ item.path }}'
        regexp: ^[\s]*kernel.dmesg_restrict
        replace: '#kernel.dmesg_restrict'
      loop: '{{ find_sysctl_d.files }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010375
      - NIST-800-171-3.1.5
      - NIST-800-53-SI-11(a)
      - NIST-800-53-SI-11(b)
      - disable_strategy
      - low_complexity
      - low_severity
      - medium_disruption
      - reboot_required
      - sysctl_kernel_dmesg_restrict

    - name: Ensure sysctl kernel.dmesg_restrict is set to 1
      sysctl:
        name: kernel.dmesg_restrict
        value: '1'
        sysctl_file: /etc/sysctl.conf
        state: present
        reload: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010375
      - NIST-800-171-3.1.5
      - NIST-800-53-SI-11(a)
      - NIST-800-53-SI-11(b)
      - disable_strategy
      - low_complexity
      - low_severity
      - medium_disruption
      - reboot_required
      - sysctl_kernel_dmesg_restrict

Remediation Kubernetes snippet ⇲

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,kernel.dmesg_restrict%3D1%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_kernel_dmesg_restrict.conf
            overwrite: true

#### Rule Disable Kernel Image Loading [ref]

To set the runtime status of the `kernel.kexec_load_disabled` kernel parameter, run the following command:

    $ sudo sysctl -w kernel.kexec_load_disabled=1

To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:

    kernel.kexec_load_disabled = 1

Rationale: | Disabling kexec_load allows greater control of the kernel memory. It makes it impossible to load another kernel image after it has been disabled.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_kernel_kexec_load_disabled
Identifiers and References | References: CCI-001749, CM-6, SRG-OS-000480-GPOS-00227, SRG-OS-000366-GPOS-00153, SV-230266r877463_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    # Comment out any occurrences of kernel.kexec_load_disabled from /etc/sysctl.d/*.conf files

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

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

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

    SYSCONFIG_FILE="/etc/sysctl.conf"

    #
    # Set runtime for kernel.kexec_load_disabled
    #
    /sbin/sysctl -q -n -w kernel.kexec_load_disabled="1"

    #
    # If kernel.kexec_load_disabled present in /etc/sysctl.conf, change value to "1"
    #	else, add "kernel.kexec_load_disabled = 1" to /etc/sysctl.conf
    #

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

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

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    - name: List /etc/sysctl.d/*.conf files
      find:
        paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
        contains: ^[\s]*kernel.kexec_load_disabled.*$
        patterns: '*.conf'
        file_type: any
      register: find_sysctl_d
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010372
      - NIST-800-53-CM-6
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_kernel_kexec_load_disabled

    - name: Comment out any occurrences of kernel.kexec_load_disabled from config files
      replace:
        path: '{{ item.path }}'
        regexp: ^[\s]*kernel.kexec_load_disabled
        replace: '#kernel.kexec_load_disabled'
      loop: '{{ find_sysctl_d.files }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010372
      - NIST-800-53-CM-6
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_kernel_kexec_load_disabled

    - name: Ensure sysctl kernel.kexec_load_disabled is set to 1
      sysctl:
        name: kernel.kexec_load_disabled
        value: '1'
        sysctl_file: /etc/sysctl.conf
        state: present
        reload: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010372
      - NIST-800-53-CM-6
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_kernel_kexec_load_disabled

Remediation Kubernetes snippet ⇲

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,kernel.kexec_load_disabled%3D1%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_kernel_kexec_load_disabled.conf
            overwrite: true

#### Rule Disable Access to Network bpf() Syscall From Unprivileged Processes [ref]

To set the runtime status of the `kernel.unprivileged_bpf_disabled` kernel parameter, run the following command:

    $ sudo sysctl -w kernel.unprivileged_bpf_disabled=1

To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:

    kernel.unprivileged_bpf_disabled = 1

Rationale: | Loading and accessing the packet filters programs and maps using the bpf() syscall has the potential of revealing sensitive information about the kernel state.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_kernel_unprivileged_bpf_disabled
Identifiers and References | References: BP28(R9), CCI-000366, AC-6, SC-7(10), FMT_SMF_EXT.1, SRG-OS-000132-GPOS-00067, SRG-OS-000480-GPOS-00227, SV-230545r858822_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    # Comment out any occurrences of kernel.unprivileged_bpf_disabled from /etc/sysctl.d/*.conf files

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

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

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

    SYSCONFIG_FILE="/etc/sysctl.conf"

    #
    # Set runtime for kernel.unprivileged_bpf_disabled
    #
    /sbin/sysctl -q -n -w kernel.unprivileged_bpf_disabled="1"

    #
    # If kernel.unprivileged_bpf_disabled present in /etc/sysctl.conf, change value to "1"
    #	else, add "kernel.unprivileged_bpf_disabled = 1" to /etc/sysctl.conf
    #

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

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

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    - name: List /etc/sysctl.d/*.conf files
      find:
        paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
        contains: ^[\s]*kernel.unprivileged_bpf_disabled.*$
        patterns: '*.conf'
        file_type: any
      register: find_sysctl_d
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040281
      - NIST-800-53-AC-6
      - NIST-800-53-SC-7(10)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_kernel_unprivileged_bpf_disabled

    - name: Comment out any occurrences of kernel.unprivileged_bpf_disabled from config
        files
      replace:
        path: '{{ item.path }}'
        regexp: ^[\s]*kernel.unprivileged_bpf_disabled
        replace: '#kernel.unprivileged_bpf_disabled'
      loop: '{{ find_sysctl_d.files }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040281
      - NIST-800-53-AC-6
      - NIST-800-53-SC-7(10)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_kernel_unprivileged_bpf_disabled

    - name: Ensure sysctl kernel.unprivileged_bpf_disabled is set to 1
      sysctl:
        name: kernel.unprivileged_bpf_disabled
        value: '1'
        sysctl_file: /etc/sysctl.conf
        state: present
        reload: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040281
      - NIST-800-53-AC-6
      - NIST-800-53-SC-7(10)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_kernel_unprivileged_bpf_disabled

Remediation Kubernetes snippet ⇲

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,kernel.unprivileged_bpf_disabled%3D1%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_kernel_unprivileged_bpf_disabled.conf
            overwrite: true

#### Rule Restrict usage of ptrace to descendant processes [ref]

To set the runtime status of the `kernel.yama.ptrace_scope` kernel parameter, run the following command:

    $ sudo sysctl -w kernel.yama.ptrace_scope=1

To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:

    kernel.yama.ptrace_scope = 1

Rationale: | Unrestricted usage of ptrace allows compromised binaries to run ptrace on another processes of the user. Like this, the attacker can steal sensitive information from the target processes (e.g. SSH sessions, web browser, ...) without any additional assistance from the user (i.e. without resorting to phishing).
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_kernel_yama_ptrace_scope
Identifiers and References | References: BP28(R25), CCI-000366, SC-7(10), SRG-OS-000132-GPOS-00067, SRG-OS-000480-GPOS-00227, SV-230546r858824_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    # Comment out any occurrences of kernel.yama.ptrace_scope from /etc/sysctl.d/*.conf files

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

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

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

    SYSCONFIG_FILE="/etc/sysctl.conf"

    #
    # Set runtime for kernel.yama.ptrace_scope
    #
    /sbin/sysctl -q -n -w kernel.yama.ptrace_scope="1"

    #
    # If kernel.yama.ptrace_scope present in /etc/sysctl.conf, change value to "1"
    #	else, add "kernel.yama.ptrace_scope = 1" to /etc/sysctl.conf
    #

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

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

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    - name: List /etc/sysctl.d/*.conf files
      find:
        paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
        contains: ^[\s]*kernel.yama.ptrace_scope.*$
        patterns: '*.conf'
        file_type: any
      register: find_sysctl_d
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040282
      - NIST-800-53-SC-7(10)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_kernel_yama_ptrace_scope

    - name: Comment out any occurrences of kernel.yama.ptrace_scope from config files
      replace:
        path: '{{ item.path }}'
        regexp: ^[\s]*kernel.yama.ptrace_scope
        replace: '#kernel.yama.ptrace_scope'
      loop: '{{ find_sysctl_d.files }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040282
      - NIST-800-53-SC-7(10)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_kernel_yama_ptrace_scope

    - name: Ensure sysctl kernel.yama.ptrace_scope is set to 1
      sysctl:
        name: kernel.yama.ptrace_scope
        value: '1'
        sysctl_file: /etc/sysctl.conf
        state: present
        reload: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040282
      - NIST-800-53-SC-7(10)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_kernel_yama_ptrace_scope

Remediation Kubernetes snippet ⇲

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,kernel.yama.ptrace_scope%3D1%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_kernel_yama_ptrace_scope.conf
            overwrite: true

#### Rule Harden the operation of the BPF just-in-time compiler [ref]

To set the runtime status of the `net.core.bpf_jit_harden` kernel parameter, run the following command:

    $ sudo sysctl -w net.core.bpf_jit_harden=2

To make sure that the setting is persistent, add the following line to a file in the directory `/etc/sysctl.d`:

    net.core.bpf_jit_harden = 2

Rationale: | When hardened, the extended Berkeley Packet Filter just-in-time compiler will randomize any kernel addresses in the BPF programs and maps, and will not expose the JIT addresses in `/proc/kallsyms`.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sysctl_net_core_bpf_jit_harden
Identifiers and References | References: BP28(R12), CCI-000366, CM-6, SC-7(10), FMT_SMF_EXT.1, SRG-OS-000480-GPOS-00227, SV-244554r858832_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    # Comment out any occurrences of net.core.bpf_jit_harden from /etc/sysctl.d/*.conf files

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

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

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

    SYSCONFIG_FILE="/etc/sysctl.conf"

    #
    # Set runtime for net.core.bpf_jit_harden
    #
    /sbin/sysctl -q -n -w net.core.bpf_jit_harden="2"

    #
    # If net.core.bpf_jit_harden present in /etc/sysctl.conf, change value to "2"
    #	else, add "net.core.bpf_jit_harden = 2" to /etc/sysctl.conf
    #

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

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

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    - name: List /etc/sysctl.d/*.conf files
      find:
        paths:
        - /etc/sysctl.d/
        - /run/sysctl.d/
        - /usr/local/lib/sysctl.d/
        contains: ^[\s]*net.core.bpf_jit_harden.*$
        patterns: '*.conf'
        file_type: any
      register: find_sysctl_d
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040286
      - NIST-800-53-CM-6
      - NIST-800-53-SC-7(10)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_core_bpf_jit_harden

    - name: Comment out any occurrences of net.core.bpf_jit_harden from config files
      replace:
        path: '{{ item.path }}'
        regexp: ^[\s]*net.core.bpf_jit_harden
        replace: '#net.core.bpf_jit_harden'
      loop: '{{ find_sysctl_d.files }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040286
      - NIST-800-53-CM-6
      - NIST-800-53-SC-7(10)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_core_bpf_jit_harden

    - name: Ensure sysctl net.core.bpf_jit_harden is set to 2
      sysctl:
        name: net.core.bpf_jit_harden
        value: '2'
        sysctl_file: /etc/sysctl.conf
        state: present
        reload: true
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040286
      - NIST-800-53-CM-6
      - NIST-800-53-SC-7(10)
      - disable_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - reboot_required
      - sysctl_net_core_bpf_jit_harden

Remediation Kubernetes snippet ⇲

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,net.core.bpf_jit_harden%3D2%0A
            mode: 0644
            path: /etc/sysctl.d/75-sysctl_net_core_bpf_jit_harden.conf
            overwrite: true

Group SELinux Group contains 1 group and 3 rules

[ref] SELinux is a feature of the Linux kernel which can be used to guard against misconfigured or compromised programs. SELinux enforces the idea that programs should be limited in what files they can access and what actions they can take.
The default SELinux policy, as configured on Red Hat Enterprise Linux 8, has been sufficiently developed and debugged that it should be usable on almost any system with minimal configuration and a small amount of system administrator training. This policy prevents system services - including most of the common network-visible services such as mail servers, FTP servers, and DNS servers - from accessing files which those services have no valid reason to access. This action alone prevents a huge amount of possible damage from network attacks against services, from trojaned software, and so forth.
This guide recommends that SELinux be enabled using the default (targeted) policy on every Red Hat Enterprise Linux 8 system, unless that system has unusual requirements which make a stronger policy appropriate.
For more information on SELinux, see https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/using_selinux.

Group SELinux - Booleans Group contains 1 rule
[ref] Enable or Disable runtime customization of SELinux system policies without having to reload or recompile the SELinux policy.

#### Rule Enable the auditadm_exec_content SELinux Boolean [ref]

By default, the SELinux boolean `auditadm_exec_content` is enabled. If this setting is disabled, it should be enabled. To enable the `auditadm_exec_content` SELinux boolean, run the following command:

    $ sudo setsebool -P auditadm_exec_content on

Rationale: |
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sebool_auditadm_exec_content
Identifiers and References | References: 80424-5, 0582, 0584, 05885, 0586, 0846, 0957

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    # Remediation is applicable only in certain platforms
    if ( ! ( [ "${container:-}" == "bwrap-osbuild" ] ) ) && [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    var_auditadm_exec_content='true'

    setsebool -P auditadm_exec_content $var_auditadm_exec_content

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: XCCDF Value var_auditadm_exec_content # promote to variable
      set_fact:
        var_auditadm_exec_content: !!str true
      tags:
        - always

    - name: Ensure python3-libsemanage installed
      package:
        name: python3-libsemanage
        state: present
      when:
      - ( not ( lookup("env", "container") == "bwrap-osbuild" ) )
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-171-80424-5
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - sebool_auditadm_exec_content

    - name: Set SELinux boolean auditadm_exec_content accordingly
      seboolean:
        name: auditadm_exec_content
        state: '{{ var_auditadm_exec_content }}'
        persistent: true
      when:
      - ( not ( lookup("env", "container") == "bwrap-osbuild" ) )
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-171-80424-5
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - sebool_auditadm_exec_content

#### Rule Configure SELinux Policy [ref]

The SELinux `targeted` policy is appropriate for general-purpose desktops and servers, as well as systems in many other roles. To configure the system to use this policy, add or correct the following line in `/etc/selinux/config`:

    SELINUXTYPE=targeted

Other policies, such as `mls`, provide additional security labeling and greater confinement but are not compatible with many general-purpose use cases.

Rationale:

Setting the SELinux policy to `targeted` or a more specialized policy ensures the system will confine processes that are likely to be targeted for exploitation, such as network or system services.
Note: During the development or debugging of SELinux modules, it is common to temporarily place non-production systems in `permissive` mode. In such temporary cases, SELinux policies should be developed, and once work is completed, the system should be reconfigured to `targeted`.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_selinux_policytype

Identifiers and References

References: BP28(R66), 1, 11, 12, 13, 14, 15, 16, 18, 3, 4, 5, 6, 8, 9, APO01.06, APO11.04, APO13.01, BAI03.05, DSS01.05, DSS03.01, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.03, DSS06.06, MEA02.01, 3.1.2, 3.7.2, CCI-002165, CCI-002696, 164.308(a)(1)(ii)(D), 164.308(a)(3), 164.308(a)(4), 164.310(b), 164.310(c), 164.312(a), 164.312(e), 4.2.3.4, 4.3.3.2.2, 4.3.3.3.9, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, 4.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.2, CIP-003-8 R5.3, CIP-004-6 R2.2.3, CIP-004-6 R2.3, CIP-004-6 R3.3, CIP-007-3 R5.1, CIP-007-3 R5.1.2, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3, CIP-007-3 R6.5, AC-3, AC-3(3)(a), AU-9, SC-7(21), DE.AE-1, ID.AM-3, PR.AC-4, PR.AC-5, PR.AC-6, PR.DS-5, PR.PT-1, PR.PT-3, PR.PT-4, 1.2.6, SRG-OS-000445-GPOS-00199, SRG-APP-000233-CTR-000585, 1.6.1.3, SV-230282r854035_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    var_selinux_policy_name='targeted'

    if [ -e "/etc/selinux/config" ] ; then

        LC_ALL=C sed -i "/^SELINUXTYPE=/Id" "/etc/selinux/config"
    else
        touch "/etc/selinux/config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/selinux/config"

    cp "/etc/selinux/config" "/etc/selinux/config.bak"
    # Insert at the end of the file
    printf '%s\n' "SELINUXTYPE=$var_selinux_policy_name" >> "/etc/selinux/config"
    # Clean up after ourselves.
    rm "/etc/selinux/config.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    - name: XCCDF Value var_selinux_policy_name # promote to variable
      set_fact:
        var_selinux_policy_name: !!str targeted
      tags:
        - always

    - name: Configure SELinux Policy
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/selinux/config
          create: true
          regexp: ^SELINUXTYPE=
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/selinux/config
        lineinfile:
          path: /etc/selinux/config
          create: true
          regexp: ^SELINUXTYPE=
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/selinux/config
        lineinfile:
          path: /etc/selinux/config
          create: true
          regexp: ^SELINUXTYPE=
          line: SELINUXTYPE={{ var_selinux_policy_name }}
          state: present
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010450
      - NIST-800-171-3.1.2
      - NIST-800-171-3.7.2
      - NIST-800-53-AC-3
      - NIST-800-53-AC-3(3)(a)
      - NIST-800-53-AU-9
      - NIST-800-53-SC-7(21)
      - PCI-DSSv4-1.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - reboot_required
      - restrict_strategy
      - selinux_policytype

#### Rule Ensure SELinux State is Enforcing [ref]

The SELinux state should be set to `enforcing` at system boot time. In the file `/etc/selinux/config`, add or correct the following line to configure the system to boot into enforcing mode:

    SELINUX=enforcing

Rationale: | Setting the SELinux state to enforcing ensures SELinux is able to confine potentially compromised processes to the security policy, which is designed to prevent them from causing damage to the system or further elevating their privileges.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_selinux_state

Identifiers and References

References: BP28(R4), BP28(R66), 1, 11, 12, 13, 14, 15, 16, 18, 3, 4, 5, 6, 8, 9, APO01.06, APO11.04, APO13.01, BAI03.05, DSS01.05, DSS03.01, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.03, DSS06.06, MEA02.01, 3.1.2, 3.7.2, CCI-001084, CCI-002165, CCI-002696, 164.308(a)(1)(ii)(D), 164.308(a)(3), 164.308(a)(4), 164.310(b), 164.310(c), 164.312(a), 164.312(e), 4.2.3.4, 4.3.3.2.2, 4.3.3.3.9, 4.3.3.4, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.4.7, 4.4.2.1, 4.4.2.2, 4.4.2.4, 4.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.10, SR 2.11, SR 2.12, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 2.8, SR 2.9, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.1, A.12.1.2, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, A.13.1.1, A.13.1.2, A.13.1.3, A.13.2.1, A.13.2.2, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.2, CIP-003-8 R5.3, CIP-004-6 R2.2.3, CIP-004-6 R2.3, CIP-004-6 R3.3, CIP-007-3 R5.1, CIP-007-3 R5.1.2, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3, CIP-007-3 R6.5, AC-3, AC-3(3)(a), AU-9, SC-7(21), DE.AE-1, ID.AM-3, PR.AC-4, PR.AC-5, PR.AC-6, PR.DS-5, PR.PT-1, PR.PT-3, PR.PT-4, SRG-OS-000445-GPOS-00199, SRG-OS-000134-GPOS-00068, 1.6.1.5, SV-230240r627750_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    var_selinux_state='enforcing'

    if [ -e "/etc/selinux/config" ] ; then

        LC_ALL=C sed -i "/^SELINUX=/Id" "/etc/selinux/config"
    else
        touch "/etc/selinux/config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/selinux/config"

    cp "/etc/selinux/config" "/etc/selinux/config.bak"
    # Insert at the end of the file
    printf '%s\n' "SELINUX=$var_selinux_state" >> "/etc/selinux/config"
    # Clean up after ourselves.
    rm "/etc/selinux/config.bak"

    fixfiles onboot
    fixfiles -f relabel

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: XCCDF Value var_selinux_state # promote to variable
      set_fact:
        var_selinux_state: !!str enforcing
      tags:
        - always

    - name: Ensure SELinux State is Enforcing
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/selinux/config
          create: true
          regexp: ^SELINUX=
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/selinux/config
        lineinfile:
          path: /etc/selinux/config
          create: true
          regexp: ^SELINUX=
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/selinux/config
        lineinfile:
          path: /etc/selinux/config
          create: true
          regexp: ^SELINUX=
          line: SELINUX={{ var_selinux_state }}
          state: present
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010170
      - NIST-800-171-3.1.2
      - NIST-800-171-3.7.2
      - NIST-800-53-AC-3
      - NIST-800-53-AC-3(3)(a)
      - NIST-800-53-AU-9
      - NIST-800-53-SC-7(21)
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - restrict_strategy
      - selinux_state

Group Services Group contains 20 groups and 44 rules

[ref] The best protection against vulnerable software is running less software. This section describes how to review the software which Red Hat Enterprise Linux 8 installs on a system and disable software which is not needed. It then enumerates the software packages installed on a default Red Hat Enterprise Linux 8 system and provides guidance about which ones can be safely disabled.
Red Hat Enterprise Linux 8 provides a convenient minimal install option that essentially installs the bare necessities for a functional system. When building Red Hat Enterprise Linux 8 systems, it is highly recommended to select the minimal packages and then build up the system from there.

Group Avahi Server Group contains 1 group and 1 rule
[ref] The Avahi daemon implements the DNS Service Discovery and Multicast DNS protocols, which provide service and host discovery on a network. It allows a system to automatically identify resources on the network, such as printers or web servers. This capability is also known as mDNSresponder and is a major part of Zeroconf networking.
Group Disable Avahi Server if Possible Group contains 1 rule
[ref] Because the Avahi daemon service keeps an open network port, it is subject to network attacks. Disabling it can reduce the system's vulnerability to such attacks.

#### Rule Disable Avahi Server Software [ref]

The `avahi-daemon` service can be disabled with the following command:

    $ sudo systemctl mask --now avahi-daemon.service

Rationale: | Because the Avahi daemon service keeps an open network port, it is subject to network attacks. Its functionality is convenient but is only appropriate if the local network can be trusted.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_avahi-daemon_disabled

Identifiers and References

References: 11, 14, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, CCI-000366, 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, 2.2.4

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Block Disable service avahi-daemon
      block:

      - name: Disable service avahi-daemon
        block:

        - name: Disable service avahi-daemon
          systemd:
            name: avahi-daemon.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
        rescue:

        - name: Intentionally ignored previous 'Disable service avahi-daemon' failure,
            service was already disabled
          meta: noop
      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)
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_avahi-daemon_disabled

    - name: Unit Socket Exists - avahi-daemon.socket
      command: systemctl -q list-unit-files avahi-daemon.socket
      register: socket_file_exists
      changed_when: false
      failed_when: socket_file_exists.rc not in [0, 1]
      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)
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_avahi-daemon_disabled

    - name: Disable socket avahi-daemon
      systemd:
        name: avahi-daemon.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - socket_file_exists.stdout_lines is search("avahi-daemon.socket",multiline=True)
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_avahi-daemon_disabled

Remediation OSBuild Blueprint snippet ⇲

    [customizations.services]
    disabled = ["avahi-daemon"]

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include disable_avahi-daemon

    class disable_avahi-daemon {
      service {'avahi-daemon':
        enable => false,
        ensure => 'stopped',
      }
    }

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        systemd:
          units:
          - name: avahi-daemon.service
            enabled: false
            mask: true
          - name: avahi-daemon.socket
            enabled: false
            mask: true

Group Application Whitelisting Daemon Group contains 2 rules
[ref] Fapolicyd (File Access Policy Daemon) implements application whitelisting to decide file access rights. Applications that are known via a reputation source are allowed access while unknown applications are not. The daemon makes use of the kernel's `fanotify` interface to determine file access rights.

#### Rule Install fapolicyd Package [ref]

The `fapolicyd` package can be installed with the following command:

    $ sudo yum install fapolicyd

Rationale: | `fapolicyd` (File Access Policy Daemon) implements application whitelisting to decide file access rights.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_fapolicyd_installed
Identifiers and References | References: CCI-001764, CCI-001774, CM-6(a), SI-4(22), SRG-OS-000370-GPOS-00155, SRG-OS-000368-GPOS-00154, SRG-OS-000480-GPOS-00230, SV-230523r854064_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    package --add=fapolicyd

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: Ensure fapolicyd is installed
      package:
        name: fapolicyd
        state: present
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040135
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SI-4(22)
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_fapolicyd_installed

Remediation OSBuild Blueprint snippet ⇲

    [[packages]]
    name = "fapolicyd"
    version = "*"

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include install_fapolicyd

    class install_fapolicyd {
      package { 'fapolicyd':
        ensure => 'installed',
      }
    }

#### Rule Enable the File Access Policy Service [ref]

The File Access Policy service should be enabled. The `fapolicyd` service can be enabled with the following command:

    $ sudo systemctl enable fapolicyd.service

Rationale: | The `fapolicyd` service (File Access Policy Daemon) implements application whitelisting to decide file access rights.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_fapolicyd_enabled
Identifiers and References | References: CCI-001764, CCI-001774, CM-6(a), SI-4(22), FMT_SMF_EXT.1, SRG-OS-000370-GPOS-00155, SRG-OS-000368-GPOS-00154, SRG-OS-000480-GPOS-00230, SV-244545r854074_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" unmask 'fapolicyd.service'
    "$SYSTEMCTL_EXEC" start 'fapolicyd.service'
    "$SYSTEMCTL_EXEC" enable 'fapolicyd.service'

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: Enable service fapolicyd
      block:

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

      - name: Enable service fapolicyd
        systemd:
          name: fapolicyd
          enabled: 'yes'
          state: started
          masked: 'no'
        when:
        - '"fapolicyd" in ansible_facts.packages'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040136
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SI-4(22)
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_fapolicyd_enabled

Remediation OSBuild Blueprint snippet ⇲

    [customizations.services]
    enabled = ["fapolicyd"]

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include enable_fapolicyd

    class enable_fapolicyd {
      service {'fapolicyd':
        enable => true,
        ensure => 'running',
      }
    }

Group Network Time Protocol Group contains 5 rules

[ref] The Network Time Protocol is used to manage the system clock over a network. Computer clocks are not very accurate, so time will drift unpredictably on unmanaged systems. Central time protocols can be used both to ensure that time is consistent among a network of systems, and that their time is consistent with the outside world.
If every system on a network reliably reports the same time, then it is much easier to correlate log messages in case of an attack. In addition, a number of cryptographic protocols (such as Kerberos) use timestamps to prevent certain types of attacks. If your network does not have synchronized time, these protocols may be unreliable or even unusable.
Depending on the specifics of the network, global time accuracy may be just as important as local synchronization, or not very important at all. If your network is connected to the Internet, using a public timeserver (or one provided by your enterprise) provides globally accurate timestamps which may be essential in investigating or responding to an attack which originated outside of your network.
A typical network setup involves a small number of internal systems operating as NTP servers, and the remainder obtaining time information from those internal servers.
There is a choice between the daemons `ntpd` and `chronyd`, which are available from the repositories in the `ntp` and `chrony` packages respectively.
The default `chronyd` daemon can work well when external time references are only intermittently accesible, can perform well even when the network is congested for longer periods of time, can usually synchronize the clock faster and with better time accuracy, and quickly adapts to sudden changes in the rate of the clock, for example, due to changes in the temperature of the crystal oscillator. `Chronyd` should be considered for all systems which are frequently suspended or otherwise intermittently disconnected and reconnected to a network. Mobile and virtual systems for example.
The `ntpd` NTP daemon fully supports NTP protocol version 4 (RFC 5905), including broadcast, multicast, manycast clients and servers, and the orphan mode. It also supports extra authentication schemes based on public-key cryptography (RFC 5906). The NTP daemon (`ntpd`) should be considered for systems which are normally kept permanently on. Systems which are required to use broadcast or multicast IP, or to perform authentication of packets with the `Autokey` protocol, should consider using `ntpd`.
Refer to https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/configuring_basic_system_settings/configuring-time-synchronization_configuring-basic-system-settings for more detailed comparison of features of `chronyd` and `ntpd` daemon features respectively, and for further guidance how to choose between the two NTP daemons.
The upstream manual pages at https://chrony-project.org/documentation.html for `chronyd` and http://www.ntp.org for `ntpd` provide additional information on the capabilities and configuration of each of the NTP daemons.

#### Rule The Chrony package is installed [ref]

System time should be synchronized between all systems in an environment. This is typically done by establishing an authoritative time server or set of servers and having all systems synchronize their clocks to them. The `chrony` package can be installed with the following command:

    $ sudo yum install chrony

Rationale: | Time synchronization is important to support time sensitive security mechanisms like Kerberos and also ensures log files have consistent time records across the enterprise, which aids in forensic investigations.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_chrony_installed
Identifiers and References | References: BP28(R71), 0988, 1405, FMT_SMF_EXT.1, Req-10.4, 10.6.1, SRG-OS-000355-GPOS-00143, 2.1.1

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    package --add=chrony

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: Ensure chrony is installed
      package:
        name: chrony
        state: present
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - PCI-DSS-Req-10.4
      - PCI-DSSv4-10.6.1
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_chrony_installed

Remediation OSBuild Blueprint snippet ⇲

    [[packages]]
    name = "chrony"
    version = "*"

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include install_chrony

    class install_chrony {
      package { 'chrony':
        ensure => 'installed',
      }
    }

#### Rule The Chronyd service is enabled [ref]

chrony is a daemon which implements the Network Time Protocol (NTP) is designed to synchronize system clocks across a variety of systems and use a source that is highly accurate. More information on chrony can be found at http://chrony.tuxfamily.org/. Chrony can be configured to be a client and/or a server. To enable Chronyd service, you can run: `# systemctl enable chronyd.service` This recommendation only applies if chrony is in use on the system.

Rationale: | If chrony is in use on the system proper configuration is vital to ensuring time synchronization is working properly.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_chronyd_enabled
Identifiers and References | References: 0988, 1405, SRG-OS-000355-GPOS-00143, 2.2.1.2

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && { rpm --quiet -q chrony; }; then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" unmask 'chronyd.service'
    "$SYSTEMCTL_EXEC" start 'chronyd.service'
    "$SYSTEMCTL_EXEC" enable 'chronyd.service'

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_chronyd_enabled

    - name: Enable service chronyd
      block:

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

      - name: Enable service chronyd
        systemd:
          name: chronyd
          enabled: 'yes'
          state: started
          masked: 'no'
        when:
        - '"chrony" in ansible_facts.packages'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - '"chrony" in ansible_facts.packages'
      tags:
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_chronyd_enabled

Remediation OSBuild Blueprint snippet ⇲

    [customizations.services]
    enabled = ["chronyd"]

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include enable_chronyd

    class enable_chronyd {
      service {'chronyd':
        enable => true,
        ensure => 'running',
      }
    }

#### Rule Enable the NTP Daemon [ref]

Run the following command to determine the current status of the `chronyd` service:

    $ sudo systemctl is-active chronyd

If the service is running, it should return the following:

    active

Note: The `chronyd` daemon is enabled by default.
Run the following command to determine the current status of the `ntpd` service:

    $ sudo systemctl is-active ntpd

If the service is running, it should return the following:

    active

Note: The `ntpd` daemon is not enabled by default. Though as mentioned in the previous sections in certain environments the `ntpd` daemon might be preferred to be used rather than the `chronyd` one. Refer to: https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system_administrators_guide/ch-configuring_ntp_using_the_chrony_suite for guidance which NTP daemon to choose depending on the environment used.

Rationale:

Enabling some of `chronyd` or `ntpd` services ensures that the NTP daemon will be running and that the system will synchronize its time to any servers specified. This is important whether the system is configured to be a client (and synchronize only its own clock) or it is also acting as an NTP server to other systems. Synchronizing time is essential for authentication services such as Kerberos, but it is also important for maintaining accurate logs and auditing possible security breaches.
The `chronyd` and `ntpd` NTP daemons offer all of the functionality of `ntpdate`, which is now deprecated.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_chronyd_or_ntpd_enabled

Identifiers and References

References: BP28(R71), 1, 14, 15, 16, 3, 5, 6, APO11.04, BAI03.05, DSS05.04, DSS05.07, MEA02.01, 3.3.7, CCI-000160, 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, 0988, 1405, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, CM-6(a), AU-8(1)(a), AU-12(1), PR.PT-1, Req-10.4.1, 10.6.1, SRG-APP-000116-CTR-000235, 2.2.1.1

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if rpm --quiet -q "chrony" ; then
        if ! /usr/sbin/pidof ntpd ; then
            /usr/bin/systemctl enable "chronyd"
            /usr/bin/systemctl start "chronyd"
            # 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.
            if /usr/bin/systemctl --failed | grep -q "chronyd"; then
                /usr/bin/systemctl reset-failed "chronyd"
            fi
        fi
    elif rpm --quiet -q "ntp" ; then
        /usr/bin/systemctl enable "ntpd"
        /usr/bin/systemctl start "ntpd"
        # 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.
        if /usr/bin/systemctl --failed | grep -q "ntpd"; then
            /usr/bin/systemctl reset-failed "ntpd"
        fi
    else
        if ! rpm -q --quiet "chrony" ; then
            yum install -y "chrony"
        fi
        /usr/bin/systemctl enable "chronyd"
        /usr/bin/systemctl start "chronyd"
        # 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.
        if /usr/bin/systemctl --failed | grep -q "chronyd"; then
            /usr/bin/systemctl reset-failed "chronyd"
        fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-171-3.3.7
      - NIST-800-53-AU-12(1)
      - NIST-800-53-AU-8(1)(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.4.1
      - PCI-DSSv4-10.6.1
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_chronyd_or_ntpd_enabled

    - name: Gather the package facts
      ansible.builtin.package_facts:
        manager: auto
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-171-3.3.7
      - NIST-800-53-AU-12(1)
      - NIST-800-53-AU-8(1)(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.4.1
      - PCI-DSSv4-10.6.1
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_chronyd_or_ntpd_enabled

    - name: Start ntpd service if ntp installed
      systemd:
        name: ntpd
        enabled: 'yes'
        state: started
        masked: 'no'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - '''ntp'' in ansible_facts.packages'
      tags:
      - NIST-800-171-3.3.7
      - NIST-800-53-AU-12(1)
      - NIST-800-53-AU-8(1)(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.4.1
      - PCI-DSSv4-10.6.1
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_chronyd_or_ntpd_enabled

    - name: Start chronyd service if chrony or chronyd installed
      systemd:
        name: chronyd
        enabled: 'yes'
        state: started
        masked: 'no'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - ('chrony' in ansible_facts.packages) or ('chronyd' in ansible_facts.packages)
      tags:
      - NIST-800-171-3.3.7
      - NIST-800-53-AU-12(1)
      - NIST-800-53-AU-8(1)(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.4.1
      - PCI-DSSv4-10.6.1
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_chronyd_or_ntpd_enabled

#### Rule Specify Additional Remote NTP Servers [ref]

Depending on specific functional requirements of a concrete production environment, the Red Hat Enterprise Linux 8 system can be configured to utilize the services of the `chronyd` NTP daemon (the default), or services of the `ntpd` NTP daemon. Refer to https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system_administrators_guide/ch-configuring_ntp_using_the_chrony_suite for more detailed comparison of the features of both of the choices, and for further guidance how to choose between the two NTP daemons.
Additional NTP servers can be specified for time synchronization. To do so, perform the following:

- if the system is configured to use the `chronyd` as the NTP daemon (the default), edit the file `/etc/chrony.conf` as follows,
- if the system is configured to use the `ntpd` as the NTP daemon, edit the file `/etc/ntp.conf` as documented below.

Add additional lines of the following form, substituting the IP address or hostname of a remote NTP server for ntpserver:

    server ntpserver

Rationale: | Specifying additional NTP servers increases the availability of accurate time data, in the event that one of the specified servers becomes unavailable. This is typical for a system acting as an NTP server for other systems.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_chronyd_or_ntpd_specify_multiple_servers
Identifiers and References | References: 1, 14, 15, 16, 3, 5, 6, APO11.04, BAI03.05, DSS05.04, DSS05.07, MEA02.01, 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, 0988, 1405, A.12.4.1, A.12.4.2, A.12.4.3, A.12.4.4, A.12.7.1, CM-6(a), AU-8(1)(a), AU-8(2), AU-12(1), PR.PT-1, Req-10.4.3

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    var_multiple_time_servers='0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org,3.pool.ntp.org'

    config_file="/etc/ntp.conf"
    /usr/sbin/pidof ntpd || config_file="/etc/chrony.conf"

    if ! [ "$(grep -c '^server' "$config_file")" -gt 1 ] ; then
      if ! grep -q '#[[:space:]]*server' "$config_file" ; then
        for server in $(echo "$var_multiple_time_servers" | tr ',' '\n') ; do
          printf '\nserver %s' "$server" >> "$config_file"
        done
      else
        sed -i 's/#[ \t]*server/server/g' "$config_file"
      fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    - name: XCCDF Value var_multiple_time_servers # promote to variable
      set_fact:
        var_multiple_time_servers: !!str 0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org,3.pool.ntp.org
      tags:
        - always

    - name: Detect if chrony configuration file is present
      find:
        path: /etc
        patterns: chrony.conf
      register: chrony_server_config
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AU-12(1)
      - NIST-800-53-AU-8(1)(a)
      - NIST-800-53-AU-8(2)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.4.3
      - chronyd_or_ntpd_specify_multiple_servers
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Configure multiple time servers in chrony config
      lineinfile:
        path: /etc/chrony.conf
        line: server {{ item }}
        state: present
        create: true
      loop: '{{ var_multiple_time_servers.split(",") }}'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - chrony_server_config.matched == 1
      tags:
      - NIST-800-53-AU-12(1)
      - NIST-800-53-AU-8(1)(a)
      - NIST-800-53-AU-8(2)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.4.3
      - chronyd_or_ntpd_specify_multiple_servers
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Detect if NTP configuration file is present
      find:
        path: /etc
        patterns: ntp.conf
      register: ntp_server_config
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AU-12(1)
      - NIST-800-53-AU-8(1)(a)
      - NIST-800-53-AU-8(2)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.4.3
      - chronyd_or_ntpd_specify_multiple_servers
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Configure multiple time servers in NTP config
      lineinfile:
        path: /etc/chrony.conf
        line: pool {{ item }}
        state: present
        create: true
      loop: '{{ var_multiple_time_servers.split(",") }}'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - ntp_server_config.matched == 1
      tags:
      - NIST-800-53-AU-12(1)
      - NIST-800-53-AU-8(1)(a)
      - NIST-800-53-AU-8(2)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.4.3
      - chronyd_or_ntpd_specify_multiple_servers
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | true
Strategy: | restrict

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ %23%20Allow%20for%20extra%20configuration%20files.%20This%20is%20useful%0A%23%20for%20admins%20specifying%20their%20own%20NTP%20servers%0Ainclude%20/etc/chrony.d/%2A.conf%0A%0A%23%20Set%20chronyd%20as%20client-only.%0Aport%200%0A%0A%23%20Disable%20chronyc%20from%20the%20network%0Acmdport%200%0A%0A%23%20Record%20the%20rate%20at%20which%20the%20system%20clock%20gains/losses%20time.%0Adriftfile%20/var/lib/chrony/drift%0A%0A%23%20Allow%20the%20system%20clock%20to%20be%20stepped%20in%20the%20first%20three%20updates%0A%23%20if%20its%20offset%20is%20larger%20than%201%20second.%0Amakestep%201.0%203%0A%0A%23%20Enable%20kernel%20synchronization%20of%20the%20real-time%20clock%20%28RTC%29.%0Artcsync%0A%0A%23%20Enable%20hardware%20timestamping%20on%20all%20interfaces%20that%20support%20it.%0A%23hwtimestamp%20%2A%0A%0A%23%20Increase%20the%20minimum%20number%20of%20selectable%20sources%20required%20to%20adjust%0A%23%20the%20system%20clock.%0A%23minsources%202%0A%0A%23%20Allow%20NTP%20client%20access%20from%20local%20network.%0A%23allow%20192.168.0.0/16%0A%0A%23%20Serve%20time%20even%20if%20not%20synchronized%20to%20a%20time%20source.%0A%23local%20stratum%2010%0A%0A%23%20Require%20authentication%20%28nts%20or%20key%20option%29%20for%20all%20NTP%20sources.%0A%23authselectmode%20require%0A%0A%23%20Specify%20file%20containing%20keys%20for%20NTP%20authentication.%0Akeyfile%20/etc/chrony.keys%0A%0A%23%20Insert/delete%20leap%20seconds%20by%20slewing%20instead%20of%20stepping.%0A%23leapsecmode%20slew%0A%0A%23%20Get%20TAI-UTC%20offset%20and%20leap%20seconds%20from%20the%20system%20tz%20database.%0Aleapsectz%20right/UTC%0A%0A%23%20Specify%20directory%20for%20log%20files.%0Alogdir%20/var/log/chrony%0A%0A%23%20Select%20which%20information%20is%20logged.%0A%23log%20measurements%20statistics%20tracking }}
            mode: 420
            overwrite: true
            path: /etc/chrony.conf
          - contents:
              source: data:,
            mode: 420
            overwrite: true
            path: /etc/chrony.d/.mco-keep
          - contents:
              source: data:,{{ %23%0A%23%20This%20file%20controls%20the%20configuration%20of%20the%20ntp%20server%0A%23%20%7B%7B.var_multiple_time_servers%7D%7D%20we%20have%20to%20put%20variable%20array%20name%20here%20for%20mutilines%20remediation%0A%7B%7B%24var_time_service_set_maxpoll%3A%3D.var_time_service_set_maxpoll%7D%7D%0A%7B%7Brange%20%24element%3A%3D.var_multiple_time_servers%7CtoArrayByComma%7D%7Dserver%20%7B%7B%24element%7D%7D%20minpoll%204%20maxpoll%20%7B%7B%24var_time_service_set_maxpoll%7D%7D%0A%7B%7Bend%7D%7D }}
            mode: 420
            overwrite: true
            path: /etc/chrony.d/ntp-server.conf

#### Rule A remote time server for Chrony is configured [ref]

`Chrony` is a daemon which implements the Network Time Protocol (NTP). It is designed to synchronize system clocks across a variety of systems and use a source that is highly accurate. More information on `chrony` can be found at http://chrony.tuxfamily.org/. `Chrony` can be configured to be a client and/or a server. Add or edit server or pool lines to `/etc/chrony.conf` as appropriate:

    server <remote-server>

Multiple servers may be configured.

Rationale: | If `chrony` is in use on the system proper configuration is vital to ensuring time synchronization is working properly.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_chronyd_specify_remote_server
Identifiers and References | References: BP28(R71), CCI-000160, CCI-001891, 0988, 1405, CM-6(a), AU-8(1)(a), Req-10.4.3, 10.6.2, 2.1.2

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && { rpm --quiet -q chrony; }; then

    var_multiple_time_servers='0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org,3.pool.ntp.org'

    config_file="/etc/chrony.conf"

    if ! grep -q '^[[:space:]]*\(server\|pool\)[[:space:]]\+[[:graph:]]\+' "$config_file" ; then
      if ! grep -q '#[[:space:]]*server' "$config_file" ; then
        for server in $(echo "$var_multiple_time_servers" | tr ',' '\n') ; do
          printf '\nserver %s' "$server" >> "$config_file"
        done
      else
        sed -i 's/#[ \t]*server/server/g' "$config_file"
      fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - NIST-800-53-AU-8(1)(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.4.3
      - PCI-DSSv4-10.6.2
      - chronyd_specify_remote_server
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
    - name: XCCDF Value var_multiple_time_servers # promote to variable
      set_fact:
        var_multiple_time_servers: !!str 0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org,3.pool.ntp.org
      tags:
        - always

    - name: Detect if chrony is already configured with pools or servers
      find:
        path: /etc
        patterns: chrony.conf
        contains: ^[\s]*(?:server|pool)[\s]+[\w]+
      register: chrony_servers
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - '"chrony" in ansible_facts.packages'
      tags:
      - NIST-800-53-AU-8(1)(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.4.3
      - PCI-DSSv4-10.6.2
      - chronyd_specify_remote_server
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Configure remote time servers
      lineinfile:
        path: /etc/chrony.conf
        line: server {{ item }}
        state: present
        create: true
      loop: '{{ var_multiple_time_servers.split(",") }}'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - '"chrony" in ansible_facts.packages'
      - chrony_servers.matched == 0
      tags:
      - NIST-800-53-AU-8(1)(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-10.4.3
      - PCI-DSSv4-10.6.2
      - chronyd_specify_remote_server
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

Group Obsolete Services Group contains 5 groups and 10 rules

[ref] This section discusses a number of network-visible services which have historically caused problems for system security, and for which disabling or severely limiting the service has been the best available guidance for some time. As a result of this, many of these services are not installed as part of Red Hat Enterprise Linux 8 by default.
Organizations which are running these services should switch to more secure equivalents as soon as possible. If it remains absolutely necessary to run one of these services for legacy reasons, care should be taken to restrict the service as much as possible, for instance by configuring host firewall software such as `iptables` to restrict access to the vulnerable service to only those remote hosts which have a known need to use it.

Group Xinetd Group contains 2 rules
[ref] The `xinetd` service acts as a dedicated listener for some network services (mostly, obsolete ones) and can be used to provide access controls and perform some logging. It has been largely obsoleted by other features, and it is not installed by default. The older Inetd service is not even available as part of Red Hat Enterprise Linux 8.

#### Rule Uninstall xinetd Package [ref]

The `xinetd` package can be removed with the following command:

    $ sudo yum erase xinetd

Rationale: | Removing the `xinetd` package decreases the risk of the xinetd service's accidental (or intentional) activation.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_package_xinetd_removed

Identifiers and References

References: BP28(R1), 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000305, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, 2.2.4, 2.2.1

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    # CAUTION: This remediation script will remove xinetd
    #	   from the system, and may remove any packages
    #	   that depend on xinetd. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "xinetd" ; then

        yum remove -y "xinetd"

    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=xinetd

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure xinetd is removed
      package:
        name: xinetd
        state: absent
      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)
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - low_severity
      - no_reboot_needed
      - package_xinetd_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_xinetd

    class remove_xinetd {
      package { 'xinetd':
        ensure => 'purged',
      }
    }

#### Rule Disable xinetd Service [ref]

The `xinetd` service can be disabled with the following command:

    $ sudo systemctl mask --now xinetd.service

Rationale: | The xinetd service provides a dedicated listener service for some programs, which is no longer necessary for commonly-used network services. Disabling it ensures that these uncommon services are not running, and also prevents attacks against xinetd itself.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_xinetd_disabled

Identifiers and References

References: 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, 3.4.7, CCI-000305, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, 2.1.7

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" stop 'xinetd.service'
    "$SYSTEMCTL_EXEC" disable 'xinetd.service'
    "$SYSTEMCTL_EXEC" mask 'xinetd.service'
    # Disable socket activation if we have a unit file for it
    if "$SYSTEMCTL_EXEC" -q list-unit-files xinetd.socket; then
        "$SYSTEMCTL_EXEC" stop 'xinetd.socket'
        "$SYSTEMCTL_EXEC" mask 'xinetd.socket'
    fi
    # The service may not be running because it has been started and failed,
    # so let's reset the state so OVAL checks pass.
    # Service should be 'inactive', not 'failed' after reboot though.
    "$SYSTEMCTL_EXEC" reset-failed 'xinetd.service' || true

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Block Disable service xinetd
      block:

      - name: Disable service xinetd
        block:

        - name: Disable service xinetd
          systemd:
            name: xinetd.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
        rescue:

        - name: Intentionally ignored previous 'Disable service xinetd' failure, service
            was already disabled
          meta: noop
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-171-3.4.7
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_xinetd_disabled

    - name: Unit Socket Exists - xinetd.socket
      command: systemctl -q list-unit-files xinetd.socket
      register: socket_file_exists
      changed_when: false
      failed_when: socket_file_exists.rc not in [0, 1]
      check_mode: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-171-3.4.7
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_xinetd_disabled

    - name: Disable socket xinetd
      systemd:
        name: xinetd.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - socket_file_exists.stdout_lines is search("xinetd.socket",multiline=True)
      tags:
      - NIST-800-171-3.4.7
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_xinetd_disabled

Remediation OSBuild Blueprint snippet ⇲

    [customizations.services]
    disabled = ["xinetd"]

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include disable_xinetd

    class disable_xinetd {
      service {'xinetd':
        enable => false,
        ensure => 'stopped',
      }
    }

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        systemd:
          units:
          - name: xinetd.service
            enabled: false
            mask: true
          - name: xinetd.socket
            enabled: false
            mask: true

Group NIS Group contains 1 rule
[ref] The Network Information Service (NIS), also known as 'Yellow Pages' (YP), and its successor NIS+ have been made obsolete by Kerberos, LDAP, and other modern centralized authentication services. NIS should not be used because it suffers from security problems inherent in its design, such as inadequate protection of important authentication information.

#### Rule Remove NIS Client [ref]

The Network Information Service (NIS), formerly known as Yellow Pages, is a client-server directory service protocol used to distribute system configuration files. The NIS client (`ypbind`) was used to bind a system to an NIS server and receive the distributed configuration files.
Rationale: | The NIS service is inherently an insecure system that has been vulnerable to DOS attacks, buffer overflows and has poor authentication for querying NIS maps. NIS generally has been replaced by such protocols as Lightweight Directory Access Protocol (LDAP). It is recommended that the service be removed.
Severity: | unknown
Rule ID: | xccdf_org.ssgproject.content_rule_package_ypbind_removed
Identifiers and References | References: BP28(R1), 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 2.2.4, 2.3.1

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove ypbind
    #	   from the system, and may remove any packages
    #	   that depend on ypbind. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "ypbind" ; then

        yum remove -y "ypbind"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=ypbind

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure ypbind is removed
      package:
        name: ypbind
        state: absent
      tags:
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - package_ypbind_removed
      - unknown_severity

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_ypbind

    class remove_ypbind {
      package { 'ypbind':
        ensure => 'purged',
      }
    }

Group Rlogin, Rsh, and Rexec Group contains 2 rules
[ref] The Berkeley r-commands are legacy services which allow cleartext remote access and have an insecure trust model.

#### Rule Uninstall rsh-server Package [ref]

The `rsh-server` package can be removed with the following command:

    $ sudo yum erase rsh-server

Warning: The package is not available in Red Hat Enterprise Linux 8.

Rationale:

The `rsh-server` service provides unencrypted remote access service which does not provide for the confidentiality and integrity of user passwords or the remote session and has very weak authentication. If a privileged user were to login using this service, the privileged user password could be compromised. The `rsh-server` package provides several obsolete and insecure network services. Removing it decreases the risk of those services' accidental (or intentional) activation.

Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_package_rsh-server_removed

Identifiers and References

References: BP28(R1), 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000381, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), IA-5(1)(c), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, 2.2.4, SRG-OS-000095-GPOS-00049, SV-230492r627750_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove rsh-server
    #	   from the system, and may remove any packages
    #	   that depend on rsh-server. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "rsh-server" ; then

        yum remove -y "rsh-server"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=rsh-server

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure rsh-server is removed
      package:
        name: rsh-server
        state: absent
      tags:
      - DISA-STIG-RHEL-08-040010
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-IA-5(1)(c)
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - package_rsh-server_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_rsh-server

    class remove_rsh-server {
      package { 'rsh-server':
        ensure => 'purged',
      }
    }

#### Rule Uninstall rsh Package [ref]

The `rsh` package contains the client commands for the rsh services Warning: The package is not available in Red Hat Enterprise Linux 8.

Rationale:

These legacy clients contain numerous security exposures and have been replaced with the more secure SSH package. Even if the server is removed, it is best to ensure the clients are also removed to prevent users from inadvertently attempting to use these commands and therefore exposing their credentials. Note that removing the `rsh` package removes the clients for `rsh`,`rcp`, and `rlogin`.

Severity: | unknown
Rule ID: | xccdf_org.ssgproject.content_rule_package_rsh_removed
Identifiers and References | References: BP28(R1), 3.1.13, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), A.8.2.3, A.13.1.1, A.13.2.1, A.13.2.3, A.14.1.2, A.14.1.3, 2.2.4, 2.3.2

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove rsh
    #	   from the system, and may remove any packages
    #	   that depend on rsh. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "rsh" ; then

        yum remove -y "rsh"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=rsh

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure rsh is removed
      package:
        name: rsh
        state: absent
      tags:
      - NIST-800-171-3.1.13
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - package_rsh_removed
      - unknown_severity

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_rsh

    class remove_rsh {
      package { 'rsh':
        ensure => 'purged',
      }
    }

Group Chat/Messaging Services Group contains 2 rules
[ref] The talk software makes it possible for users to send and receive messages across systems through a terminal session.

#### Rule Uninstall talk-server Package [ref]

The `talk-server` package can be removed with the following command:

     $ sudo yum erase talk-server

Warning: The package is not available in Red Hat Enterprise Linux 8.

Rationale: | The talk software presents a security risk as it uses unencrypted protocols for communications. Removing the `talk-server` package decreases the risk of the accidental (or intentional) activation of talk services.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_talk-server_removed
Identifiers and References | References: BP28(R1), 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 2.2.4

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove talk-server
    #	   from the system, and may remove any packages
    #	   that depend on talk-server. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "talk-server" ; then

        yum remove -y "talk-server"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=talk-server

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure talk-server is removed
      package:
        name: talk-server
        state: absent
      tags:
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_talk-server_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_talk-server

    class remove_talk-server {
      package { 'talk-server':
        ensure => 'purged',
      }
    }

#### Rule Uninstall talk Package [ref]

The `talk` package contains the client program for the Internet talk protocol, which allows the user to chat with other users on different systems. Talk is a communication program which copies lines from one terminal to the terminal of another user. The `talk` package can be removed with the following command:

    $ sudo yum erase talk

Warning: The package is not available in Red Hat Enterprise Linux 8.

Rationale: | The talk software presents a security risk as it uses unencrypted protocols for communications. Removing the `talk` package decreases the risk of the accidental (or intentional) activation of talk client program.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_talk_removed
Identifiers and References | References: BP28(R1), 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 2.2.4, 2.3.3

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove talk
    #	   from the system, and may remove any packages
    #	   that depend on talk. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "talk" ; then

        yum remove -y "talk"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=talk

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure talk is removed
      package:
        name: talk
        state: absent
      tags:
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_talk_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_talk

    class remove_talk {
      package { 'talk':
        ensure => 'purged',
      }
    }

Group Telnet Group contains 3 rules
[ref] The telnet protocol does not provide confidentiality or integrity for information transmitted on the network. This includes authentication information such as passwords. Organizations which use telnet should be actively working to migrate to a more secure protocol.

#### Rule Uninstall telnet-server Package [ref]

The `telnet-server` package can be removed with the following command:

    $ sudo yum erase telnet-server

Rationale:

It is detrimental for operating systems to provide, or install by default, functionality exceeding requirements or mission objectives. These unnecessary capabilities are often overlooked and therefore may remain unsecure. They increase the risk to the platform by providing additional attack vectors.
The telnet service provides an unencrypted remote access service which does not provide for the confidentiality and integrity of user passwords or the remote session. If a privileged user were to login using this service, the privileged user password could be compromised.
Removing the `telnet-server` package decreases the risk of the telnet service's accidental (or intentional) activation.

Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_package_telnet-server_removed

Identifiers and References

References: BP28(R1), 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000381, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, Req-2.2.2, 2.2.4, SRG-OS-000095-GPOS-00049, 2.2.16, SV-230487r627750_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove telnet-server
    #	   from the system, and may remove any packages
    #	   that depend on telnet-server. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "telnet-server" ; then

        yum remove -y "telnet-server"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=telnet-server

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure telnet-server is removed
      package:
        name: telnet-server
        state: absent
      tags:
      - DISA-STIG-RHEL-08-040000
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSS-Req-2.2.2
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - package_telnet-server_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_telnet-server

    class remove_telnet-server {
      package { 'telnet-server':
        ensure => 'purged',
      }
    }

#### Rule Remove telnet Clients [ref]

The telnet client allows users to start connections to other systems via the telnet protocol.
Rationale: | The `telnet` protocol is insecure and unencrypted. The use of an unencrypted transmission medium could allow an unauthorized user to steal credentials. The `ssh` package provides an encrypted session and stronger security and is included in Red Hat Enterprise Linux 8.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_package_telnet_removed
Identifiers and References | References: BP28(R1), 3.1.13, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), A.8.2.3, A.13.1.1, A.13.2.1, A.13.2.3, A.14.1.2, A.14.1.3, 2.2.4, 2.3.4

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove telnet
    #	   from the system, and may remove any packages
    #	   that depend on telnet. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "telnet" ; then

        yum remove -y "telnet"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=telnet

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure telnet is removed
      package:
        name: telnet
        state: absent
      tags:
      - NIST-800-171-3.1.13
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - low_severity
      - no_reboot_needed
      - package_telnet_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_telnet

    class remove_telnet {
      package { 'telnet':
        ensure => 'purged',
      }
    }

#### Rule Disable telnet Service [ref]

Make sure that the activation of the `telnet` service on system boot is disabled. The `telnet` socket can be disabled with the following command:

    $ sudo systemctl mask --now telnet.socket

Warning: If the system relies on `xinetd` to manage telnet sessions, ensure the telnet service is disabled by the following line: `disable = yes`. Note that the xinetd file for telnet is not created automatically, therefore it might have different names.

Rationale: | The telnet protocol uses unencrypted network communication, which means that data from the login session, including passwords and all other information transmitted during the session, can be stolen by eavesdroppers on the network. The telnet protocol is also subject to man-in-the-middle attacks.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_service_telnet_disabled

Identifiers and References

References: 1, 11, 12, 14, 15, 16, 3, 5, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.06, DSS06.10, 3.1.13, 3.4.7, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.18.1.4, A.6.2.1, A.6.2.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, CM-7(a), CM-7(b), CM-6(a), IA-5(1)(c), PR.AC-1, PR.AC-3, PR.AC-6, PR.AC-7, PR.IP-1, PR.PT-3, PR.PT-4

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" stop 'telnet.service'
    "$SYSTEMCTL_EXEC" disable 'telnet.service'
    "$SYSTEMCTL_EXEC" mask 'telnet.service'
    # Disable socket activation if we have a unit file for it
    if "$SYSTEMCTL_EXEC" -q list-unit-files telnet.socket; then
        "$SYSTEMCTL_EXEC" stop 'telnet.socket'
        "$SYSTEMCTL_EXEC" mask 'telnet.socket'
    fi
    # The service may not be running because it has been started and failed,
    # so let's reset the state so OVAL checks pass.
    # Service should be 'inactive', not 'failed' after reboot though.
    "$SYSTEMCTL_EXEC" reset-failed 'telnet.service' || true

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Block Disable service telnet
      block:

      - name: Disable service telnet
        block:

        - name: Disable service telnet
          systemd:
            name: telnet.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
        rescue:

        - name: Intentionally ignored previous 'Disable service telnet' failure, service
            was already disabled
          meta: noop
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-171-3.1.13
      - NIST-800-171-3.4.7
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-IA-5(1)(c)
      - disable_strategy
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - service_telnet_disabled

    - name: Unit Socket Exists - telnet.socket
      command: systemctl -q list-unit-files telnet.socket
      register: socket_file_exists
      changed_when: false
      failed_when: socket_file_exists.rc not in [0, 1]
      check_mode: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-171-3.1.13
      - NIST-800-171-3.4.7
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-IA-5(1)(c)
      - disable_strategy
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - service_telnet_disabled

    - name: Disable socket telnet
      systemd:
        name: telnet.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - socket_file_exists.stdout_lines is search("telnet.socket",multiline=True)
      tags:
      - NIST-800-171-3.1.13
      - NIST-800-171-3.4.7
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-IA-5(1)(c)
      - disable_strategy
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - service_telnet_disabled

Remediation OSBuild Blueprint snippet ⇲

    [customizations.services]
    disabled = ["telnet"]

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include disable_telnet

    class disable_telnet {
      service {'telnet':
        enable => false,
        ensure => 'stopped',
      }
    }

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        systemd:
          units:
          - name: telnet.service
            enabled: false
            mask: true
          - name: telnet.socket
            enabled: false
            mask: true

Group Proxy Server Group contains 1 group and 2 rules

[ref] A proxy server is a very desirable target for a potential adversary because much (or all) sensitive data for a given infrastructure may flow through it. Therefore, if one is required, the system acting as a proxy server should be dedicated to that purpose alone and be stored in a physically secure location. The system's default proxy server software is Squid, and provided in an RPM package of the same name.

Group Disable Squid if Possible Group contains 2 rules
[ref] If Squid was installed and activated, but the system does not need to act as a proxy server, then it should be disabled and removed.

#### Rule Uninstall squid Package [ref]

The `squid` package can be removed with the following command:

     $ sudo yum erase squid

Rationale: | If there is no need to make the proxy server software available, removing it provides a safeguard against its activation.
Severity: | unknown
Rule ID: | xccdf_org.ssgproject.content_rule_package_squid_removed
Identifiers and References | References: 2.2.13

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove squid
    #	   from the system, and may remove any packages
    #	   that depend on squid. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "squid" ; then

        yum remove -y "squid"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=squid

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure squid is removed
      package:
        name: squid
        state: absent
      tags:
      - disable_strategy
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - package_squid_removed
      - unknown_severity

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_squid

    class remove_squid {
      package { 'squid':
        ensure => 'purged',
      }
    }

#### Rule Disable Squid [ref]

The `squid` service can be disabled with the following command:

    $ sudo systemctl mask --now squid.service

Rationale: | Running proxy server software provides a network-based avenue of attack, and should be removed if not needed.
Severity: | unknown
Rule ID: | xccdf_org.ssgproject.content_rule_service_squid_disabled
Identifiers and References |

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Block Disable service squid
      block:

      - name: Disable service squid
        block:

        - name: Disable service squid
          systemd:
            name: squid.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
        rescue:

        - name: Intentionally ignored previous 'Disable service squid' failure, service
            was already disabled
          meta: noop
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - disable_strategy
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - service_squid_disabled
      - unknown_severity

    - name: Unit Socket Exists - squid.socket
      command: systemctl -q list-unit-files squid.socket
      register: socket_file_exists
      changed_when: false
      failed_when: socket_file_exists.rc not in [0, 1]
      check_mode: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - disable_strategy
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - service_squid_disabled
      - unknown_severity

    - name: Disable socket squid
      systemd:
        name: squid.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - socket_file_exists.stdout_lines is search("squid.socket",multiline=True)
      tags:
      - disable_strategy
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - service_squid_disabled
      - unknown_severity

Remediation OSBuild Blueprint snippet ⇲

    [customizations.services]
    disabled = ["squid"]

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include disable_squid

    class disable_squid {
      service {'squid':
        enable => false,
        ensure => 'stopped',
      }
    }

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        systemd:
          units:
          - name: squid.service
            enabled: false
            mask: true
          - name: squid.socket
            enabled: false
            mask: true

Group Network Routing Group contains 1 group and 2 rules

[ref] A router is a very desirable target for a potential adversary because they fulfill a variety of infrastructure networking roles such as access to network segments, gateways to other networks, filtering, etc. Therefore, if one is required, the system acting as a router should be dedicated to that purpose alone and be stored in a physically secure location. The system's default routing software is Quagga, and provided in an RPM package of the same name.

Group Disable Quagga if Possible Group contains 2 rules
[ref] If Quagga was installed and activated, but the system does not need to act as a router, then it should be disabled and removed.

#### Rule Uninstall quagga Package [ref]

The `quagga` package can be removed with the following command:

     $ sudo yum erase quagga

Rationale: | Routing software is typically used on routers to exchange network topology information with other routers. If routing software is used when not required, system network information may be unnecessarily transmitted across the network. If there is no need to make the router software available, removing it provides a safeguard against its activation.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_package_quagga_removed
Identifiers and References | References: 12, 15, 8, APO13.01, DSS05.02, CCI-000366, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.13.1.1, A.13.2.1, A.14.1.3, CM-7(a), CM-7(b), CM-6(a), PR.PT-4, SRG-OS-000480-GPOS-00227

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove quagga
    #	   from the system, and may remove any packages
    #	   that depend on quagga. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "quagga" ; then

        yum remove -y "quagga"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=quagga

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure quagga is removed
      package:
        name: quagga
        state: absent
      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
      - package_quagga_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_quagga

    class remove_quagga {
      package { 'quagga':
        ensure => 'purged',
      }
    }

#### Rule Disable Quagga Service [ref]

The `zebra` service can be disabled with the following command:

    $ sudo systemctl mask --now zebra.service

Rationale: | Routing protocol daemons are typically used on routers to exchange network topology information with other routers. If routing daemons are used when not required, system network information may be unnecessarily transmitted across the network.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_zebra_disabled
Identifiers and References | References: 12, 15, 8, APO13.01, DSS05.02, CCI-000366, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.13.1.1, A.13.2.1, A.14.1.3, CM-7(a), CM-7(b), CM-6(a), PR.PT-4, SRG-OS-000480-GPOS-00227

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Block Disable service zebra
      block:

      - name: Disable service zebra
        block:

        - name: Disable service zebra
          systemd:
            name: zebra.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
        rescue:

        - name: Intentionally ignored previous 'Disable service zebra' failure, service
            was already disabled
          meta: noop
      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_zebra_disabled

    - name: Unit Socket Exists - zebra.socket
      command: systemctl -q list-unit-files zebra.socket
      register: socket_file_exists
      changed_when: false
      failed_when: socket_file_exists.rc not in [0, 1]
      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_zebra_disabled

    - name: Disable socket zebra
      systemd:
        name: zebra.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - socket_file_exists.stdout_lines is search("zebra.socket",multiline=True)
      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_zebra_disabled

Remediation OSBuild Blueprint snippet ⇲

    [customizations.services]
    disabled = ["zebra"]

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include disable_zebra

    class disable_zebra {
      service {'zebra':
        enable => false,
        ensure => 'stopped',
      }
    }

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        systemd:
          units:
          - name: zebra.service
            enabled: false
            mask: true
          - name: zebra.socket
            enabled: false
            mask: true

Group SNMP Server Group contains 2 groups and 2 rules
[ref] The Simple Network Management Protocol allows administrators to monitor the state of network devices, including computers. Older versions of SNMP were well-known for weak security, such as plaintext transmission of the community string (used for authentication) and usage of easily-guessable choices for the community string.
Group Disable SNMP Server if Possible Group contains 1 rule
[ref] The system includes an SNMP daemon that allows for its remote monitoring, though it not installed by default. If it was installed and activated but is not needed, the software should be disabled and removed.

#### Rule Disable snmpd Service [ref]

The `snmpd` service can be disabled with the following command:

    $ sudo systemctl mask --now snmpd.service

Rationale: | Running SNMP software provides a network-based avenue of attack, and should be disabled if not needed.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_service_snmpd_disabled
Identifiers and References | References: 1311

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Block Disable service snmpd
      block:

      - name: Disable service snmpd
        block:

        - name: Disable service snmpd
          systemd:
            name: snmpd.service
            enabled: 'no'
            state: stopped
            masked: 'yes'
        rescue:

        - name: Intentionally ignored previous 'Disable service snmpd' failure, service
            was already disabled
          meta: noop
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - disable_strategy
      - low_complexity
      - low_disruption
      - low_severity
      - no_reboot_needed
      - service_snmpd_disabled

    - name: Unit Socket Exists - snmpd.socket
      command: systemctl -q list-unit-files snmpd.socket
      register: socket_file_exists
      changed_when: false
      failed_when: socket_file_exists.rc not in [0, 1]
      check_mode: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - disable_strategy
      - low_complexity
      - low_disruption
      - low_severity
      - no_reboot_needed
      - service_snmpd_disabled

    - name: Disable socket snmpd
      systemd:
        name: snmpd.socket
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - socket_file_exists.stdout_lines is search("snmpd.socket",multiline=True)
      tags:
      - disable_strategy
      - low_complexity
      - low_disruption
      - low_severity
      - no_reboot_needed
      - service_snmpd_disabled

Remediation OSBuild Blueprint snippet ⇲

    [customizations.services]
    disabled = ["snmpd"]

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include disable_snmpd

    class disable_snmpd {
      service {'snmpd':
        enable => false,
        ensure => 'stopped',
      }
    }

Remediation Kubernetes snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | true
Strategy: | disable

    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        systemd:
          units:
          - name: snmpd.service
            enabled: false
            mask: true
          - name: snmpd.socket
            enabled: false
            mask: true

Group Configure SNMP Server if Necessary Group contains 1 rule

[ref] If it is necessary to run the snmpd agent on the system, some best practices should be followed to minimize the security risk from the installation. The multiple security models implemented by SNMP cannot be fully covered here so only the following general configuration advice can be offered:

- use only SNMP version 3 security models and enable the use of authentication and encryption
- write access to the MIB (Management Information Base) should be allowed only if necessary
- all access to the MIB should be restricted following a principle of least privilege
- network access should be limited to the maximum extent possible including restricting to expected network addresses both in the configuration files and in the system firewall rules
- ensure SNMP agents send traps only to, and accept SNMP queries only from, authorized management stations
- ensure that permissions on the `snmpd.conf` configuration file (by default, in `/etc/snmp`) are 640 or more restrictive
- ensure that any MIB files' permissions are also 640 or more restrictive

#### Rule Configure SNMP Service to Use Only SNMPv3 or Newer [ref]

Edit `/etc/snmp/snmpd.conf`, removing any references to `rocommunity`, `rwcommunity`, or `com2sec`. Upon doing that, restart the SNMP service:

    $ sudo service snmpd restart

Rationale: | Earlier versions of SNMP are considered insecure, as they potentially allow unauthorized access to detailed system management information.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_snmpd_use_newer_protocol
Identifiers and References | References: 1311

Group SSH Server Group contains 1 group and 17 rules

[ref] The SSH protocol is recommended for remote login and remote file transfer. SSH provides confidentiality and integrity for data exchanged between two systems, as well as server authentication, through the use of public key cryptography. The implementation included with the system is called OpenSSH, and more detailed documentation is available from its website, https://www.openssh.com. Its server program is called `sshd` and provided by the RPM package `openssh-server`.

Group Configure OpenSSH Server if Necessary Group contains 16 rules
[ref] If the system needs to act as an SSH server, then certain changes should be made to the OpenSSH daemon configuration file `/etc/ssh/sshd_config`. The following recommendations can be applied to this file. See the `sshd_config(5)` man page for more detailed information.

#### Rule Disable Host-Based Authentication [ref]

SSH's cryptographic host-based authentication is more secure than `.rhosts` authentication. However, it is not recommended that hosts unilaterally trust one another, even within an organization.
The default SSH configuration disables host-based authentication. The appropriate configuration is used if no value is set for `HostbasedAuthentication`.
To explicitly disable host-based authentication, add or correct the following line in `/etc/ssh/sshd_config`:

    HostbasedAuthentication no

Rationale: | SSH trust relationships mean a compromise on one host can allow an attacker to move trivially to other hosts.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_disable_host_auth

Identifiers and References

References: 11, 12, 14, 15, 16, 18, 3, 5, 9, 5.5.6, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.03, DSS06.06, 3.1.12, CCI-000366, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.6.1.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.2.3, CIP-004-6 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.2, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3, AC-3, AC-17(a), CM-7(a), CM-7(b), CM-6(a), PR.AC-4, PR.AC-6, PR.IP-1, PR.PT-3, FIA_UAU.1, 8.3.1, SRG-OS-000480-GPOS-00229, 5.2.8

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if [ -e "/etc/ssh/sshd_config" ] ; then

        LC_ALL=C sed -i "/^\s*HostbasedAuthentication\s\+/Id" "/etc/ssh/sshd_config"
    else
        touch "/etc/ssh/sshd_config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config"

    cp "/etc/ssh/sshd_config" "/etc/ssh/sshd_config.bak"
    # Insert at the beginning of the file
    printf '%s\n' "HostbasedAuthentication no" > "/etc/ssh/sshd_config"
    cat "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Disable Host-Based Authentication
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*HostbasedAuthentication\s+
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*HostbasedAuthentication\s+
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*HostbasedAuthentication\s+
          line: HostbasedAuthentication no
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.5.6
      - NIST-800-171-3.1.12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-3
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSSv4-8.3.1
      - disable_host_auth
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Remediation Kubernetes snippet ⇲

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,%23%09%24OpenBSD%3A%20sshd_config%2Cv%201.103%202018%2F04%2F09%2020%3A41%3A22%20tj%20Exp%20%24%0A%0A%23%20This%20is%20the%20sshd%20server%20system-wide%20configuration%20file.%20%20See%0A%23%20sshd_config%285%29%20for%20more%20information.%0A%0A%23%20This%20sshd%20was%20compiled%20with%20PATH%3D%2Fusr%2Flocal%2Fbin%3A%2Fusr%2Fbin%3A%2Fusr%2Flocal%2Fsbin%3A%2Fusr%2Fsbin%0A%0A%23%20The%20strategy%20used%20for%20options%20in%20the%20default%20sshd_config%20shipped%20with%0A%23%20OpenSSH%20is%20to%20specify%20options%20with%20their%20default%20value%20where%0A%23%20possible%2C%20but%20leave%20them%20commented.%20%20Uncommented%20options%20override%20the%0A%23%20default%20value.%0A%0A%23%20If%20you%20want%20to%20change%20the%20port%20on%20a%20SELinux%20system%2C%20you%20have%20to%20tell%0A%23%20SELinux%20about%20this%20change.%0A%23%20semanage%20port%20-a%20-t%20ssh_port_t%20-p%20tcp%20%23PORTNUMBER%0A%23%0A%23Port%2022%0A%23AddressFamily%20any%0A%23ListenAddress%200.0.0.0%0A%23ListenAddress%20%3A%3A%0A%0AHostKey%20%2Fetc%2Fssh%2Fssh_host_rsa_key%0AHostKey%20%2Fetc%2Fssh%2Fssh_host_ecdsa_key%0AHostKey%20%2Fetc%2Fssh%2Fssh_host_ed25519_key%0A%0A%23%20Ciphers%20and%20keying%0ARekeyLimit%20512M%201h%0A%0A%23%20System-wide%20Crypto%20policy%3A%0A%23%20This%20system%20is%20following%20system-wide%20crypto%20policy.%20The%20changes%20to%0A%23%20Ciphers%2C%20MACs%2C%20KexAlgoritms%20and%20GSSAPIKexAlgorithsm%20will%20not%20have%20any%0A%23%20effect%20here.%20They%20will%20be%20overridden%20by%20command-line%20options%20passed%20on%0A%23%20the%20server%20start%20up.%0A%23%20To%20opt%20out%2C%20uncomment%20a%20line%20with%20redefinition%20of%20%20CRYPTO_POLICY%3D%0A%23%20variable%20in%20%20%2Fetc%2Fsysconfig%2Fsshd%20%20to%20overwrite%20the%20policy.%0A%23%20For%20more%20information%2C%20see%20manual%20page%20for%20update-crypto-policies%288%29.%0A%0A%23%20Logging%0A%23SyslogFacility%20AUTH%0ASyslogFacility%20AUTHPRIV%0A%23LogLevel%20INFO%0A%0A%23%20Authentication%3A%0A%0A%23LoginGraceTime%202m%0APermitRootLogin%20no%0AStrictModes%20yes%0A%23MaxAuthTries%206%0A%23MaxSessions%2010%0A%0APubkeyAuthentication%20yes%0A%0A%23%20The%20default%20is%20to%20check%20both%20.ssh%2Fauthorized_keys%20and%20.ssh%2Fauthorized_keys2%0A%23%20but%20this%20is%20overridden%20so%20installations%20will%20only%20check%20.ssh%2Fauthorized_keys%0AAuthorizedKeysFile%09.ssh%2Fauthorized_keys%0A%0A%23AuthorizedPrincipalsFile%20none%0A%0A%23AuthorizedKeysCommand%20none%0A%23AuthorizedKeysCommandUser%20nobody%0A%0A%23%20For%20this%20to%20work%20you%20will%20also%20need%20host%20keys%20in%20%2Fetc%2Fssh%2Fssh_known_hosts%0AHostbasedAuthentication%20no%0A%23%20Change%20to%20yes%20if%20you%20don%27t%20trust%20~%2F.ssh%2Fknown_hosts%20for%0A%23%20HostbasedAuthentication%0AIgnoreUserKnownHosts%20yes%0A%23%20Don%27t%20read%20the%20user%27s%20~%2F.rhosts%20and%20~%2F.shosts%20files%0AIgnoreRhosts%20yes%0A%0A%23%20To%20disable%20tunneled%20clear%20text%20passwords%2C%20change%20to%20no%20here%21%0A%23PasswordAuthentication%20yes%0APermitEmptyPasswords%20no%0APasswordAuthentication%20no%0A%0A%23%20Change%20to%20no%20to%20disable%20s%2Fkey%20passwords%0A%23ChallengeResponseAuthentication%20yes%0AChallengeResponseAuthentication%20no%0A%0A%23%20Kerberos%20options%0AKerberosAuthentication%20no%0A%23KerberosOrLocalPasswd%20yes%0A%23KerberosTicketCleanup%20yes%0A%23KerberosGetAFSToken%20no%0A%23KerberosUseKuserok%20yes%0A%0A%23%20GSSAPI%20options%0AGSSAPIAuthentication%20no%0AGSSAPICleanupCredentials%20no%0A%23GSSAPIStrictAcceptorCheck%20yes%0A%23GSSAPIKeyExchange%20no%0A%23GSSAPIEnablek5users%20no%0A%0A%23%20Set%20this%20to%20%27yes%27%20to%20enable%20PAM%20authentication%2C%20account%20processing%2C%0A%23%20and%20session%20processing.%20If%20this%20is%20enabled%2C%20PAM%20authentication%20will%0A%23%20be%20allowed%20through%20the%20ChallengeResponseAuthentication%20and%0A%23%20PasswordAuthentication.%20%20Depending%20on%20your%20PAM%20configuration%2C%0A%23%20PAM%20authentication%20via%20ChallengeResponseAuthentication%20may%20bypass%0A%23%20the%20setting%20of%20%22PermitRootLogin%20without-password%22.%0A%23%20If%20you%20just%20want%20the%20PAM%20account%20and%20session%20checks%20to%20run%20without%0A%23%20PAM%20authentication%2C%20then%20enable%20this%20but%20set%20PasswordAuthentication%0A%23%20and%20ChallengeResponseAuthentication%20to%20%27no%27.%0A%23%20WARNING%3A%20%27UsePAM%20no%27%20is%20not%20supported%20in%20Fedora%20and%20may%20cause%20several%0A%23%20problems.%0AUsePAM%20yes%0A%0A%23AllowAgentForwarding%20yes%0A%23AllowTcpForwarding%20yes%0A%23GatewayPorts%20no%0AX11Forwarding%20yes%0A%23X11DisplayOffset%2010%0A%23X11UseLocalhost%20yes%0A%23PermitTTY%20yes%0A%0A%23%20It%20is%20recommended%20to%20use%20pam_motd%20in%20%2Fetc%2Fpam.d%2Fsshd%20instead%20of%20PrintMotd%2C%0A%23%20as%20it%20is%20more%20configurable%20and%20versatile%20than%20the%20built-in%20version.%0APrintMotd%20no%0A%0APrintLastLog%20yes%0A%23TCPKeepAlive%20yes%0APermitUserEnvironment%20no%0ACompression%20no%0AClientAliveInterval%20600%0AClientAliveCountMax%200%0A%23UseDNS%20no%0A%23PidFile%20%2Fvar%2Frun%2Fsshd.pid%0A%23MaxStartups%2010%3A30%3A100%0A%23PermitTunnel%20no%0A%23ChrootDirectory%20none%0A%23VersionAddendum%20none%0A%0A%23%20no%20default%20banner%20path%0ABanner%20%2Fetc%2Fissue%0A%0A%23%20Accept%20locale-related%20environment%20variables%0AAcceptEnv%20LANG%20LC_CTYPE%20LC_NUMERIC%20LC_TIME%20LC_COLLATE%20LC_MONETARY%20LC_MESSAGES%0AAcceptEnv%20LC_PAPER%20LC_NAME%20LC_ADDRESS%20LC_TELEPHONE%20LC_MEASUREMENT%0AAcceptEnv%20LC_IDENTIFICATION%20LC_ALL%20LANGUAGE%0AAcceptEnv%20XMODIFIERS%0A%0A%23%20override%20default%20of%20no%20subsystems%0ASubsystem%09sftp%09%2Fusr%2Flibexec%2Fopenssh%2Fsftp-server%0A%0A%23%20Example%20of%20overriding%20settings%20on%20a%20per-user%20basis%0A%23Match%20User%20anoncvs%0A%23%09X11Forwarding%20no%0A%23%09AllowTcpForwarding%20no%0A%23%09PermitTTY%20no%0A%23%09ForceCommand%20cvs%20server%0A%0AUsePrivilegeSeparation%20sandbox
            mode: 0600
            path: /etc/ssh/sshd_config
            overwrite: true

#### Rule Enable SSH Server firewalld Firewall Exception [ref]

If the SSH server is in use, inbound connections to SSH's port should be allowed to permit remote access through SSH. In more restrictive firewalld settings, the SSH port should be added to the proper firewalld zone in order to allow SSH remote access.
To configure `firewalld` to allow `ssh` access, run the following command(s):

    firewall-cmd --permanent --add-service=ssh

Then run the following command to load the newly created rule(s):

    firewall-cmd --reload

Warning: The remediation for this rule uses `firewall-cmd` and `nmcli` tools. Therefore, it will only be executed if `firewalld` and `NetworkManager` services are running. Otherwise, the remediation will be aborted and a informative message will be shown in the remediation report. These respective services will not be started in order to preserve any intentional change in network components related to firewall and network interfaces.
Warning: This rule also checks if the SSH port was modified by the administrator in the firewalld services definitions and is reflecting the expected port number. Although this is checked, fixing the custom ssh.xml file placed by the administrator at /etc/firewalld/services it is not in the scope of the remediation since there is no reliable way to manually change the respective file. If the default SSH port is modified, it is on the administrator responsibility to ensure the firewalld customizations in the service port level are properly configured.

Rationale: | If inbound SSH connections are expected, adding the SSH port to the proper firewalld zone will allow remote access through the SSH port.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_firewalld_sshd_port_enabled
Identifiers and References | References: 3.1.12, 1416, AC-17(a), CM-6(b), CM-7(a), CM-7(b), SRG-OS-000096-GPOS-00050

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if ! rpm -q --quiet "firewalld" ; then
        yum install -y "firewalld"
    fi
    if ! rpm -q --quiet "NetworkManager" ; then
        yum install -y "NetworkManager"
    fi
    firewalld_sshd_zone='public'

    if systemctl is-active NetworkManager && systemctl is-active firewalld; then
        # First make sure the SSH service is enabled in run-time for the proper zone.
        # This is to avoid connection issues when new interfaces are addeded to this zone.
        firewall-cmd --zone="$firewalld_sshd_zone" --add-service=ssh

        # This will collect all NetworkManager connections names
        readarray -t nm_connections < <(nmcli -f UUID,TYPE con | grep ethernet | awk '{ print $1 }')
        # If the connection is not yet assigned to a firewalld zone, assign it to the proper zone.
        # This will not change connections which are already assigned to any firewalld zone.
        for connection in "${nm_connections[@]}"; do
            current_zone=$(nmcli -f connection.zone connection show "$connection" | awk '{ print $2}')
            if [ $current_zone = "--" ]; then
                nmcli connection modify "$connection" connection.zone $firewalld_sshd_zone
            fi
        done
        systemctl restart NetworkManager

        # Active zones are zones with at least one interface assigned to it.
        # It is possible that traffic is comming by any active interface and consequently any
        # active zone. So, this make sure all active zones are permanently allowing SSH service.
        readarray -t firewalld_active_zones < <(firewall-cmd --get-active-zones | grep -v interfaces)
        for zone in "${firewalld_active_zones[@]}"; do
            firewall-cmd --permanent --zone="$zone" --add-service=ssh
        done
        firewall-cmd --reload
    else
        echo "
        firewalld and NetworkManager services are not active. Remediation aborted!
        This remediation could not be applied because it depends on firewalld and NetworkManager services running.
        The service is not started by this remediation in order to prevent connection issues."
        exit 1
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    - name: XCCDF Value firewalld_sshd_zone # promote to variable
      set_fact:
        firewalld_sshd_zone: !!str public
      tags:
        - always

    - name: Enable SSH Server firewalld Firewall Exception - Ensure firewalld and NetworkManager
        packages are installed
      ansible.builtin.package:
        name: '{{ item }}'
        state: present
      with_items:
      - firewalld
      - NetworkManager
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-171-3.1.12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - configure_strategy
      - firewalld_sshd_port_enabled
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Enable SSH Server firewalld Firewall Exception - Collect facts about system
        services
      ansible.builtin.service_facts: null
      register: result_services_states
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-171-3.1.12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - configure_strategy
      - firewalld_sshd_port_enabled
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Enable SSH Server firewalld Firewall Exception - Remediation is applicable
        if firewalld and NetworkManager services are running
      block:

      - name: Enable SSH Server firewalld Firewall Exception - Collect NetworkManager
          connections names
        ansible.builtin.shell:
          cmd: nmcli -f UUID,TYPE con | grep ethernet | awk '{ print $1 }'
        register: result_nmcli_cmd_connections_names
        changed_when: false

      - name: Enable SSH Server firewalld Firewall Exception - Collect NetworkManager
          connections zones
        ansible.builtin.shell:
          cmd: nmcli -f connection.zone connection show {{ item | trim }} | awk '{ print
            $2}'
        register: result_nmcli_cmd_connections_zones
        changed_when: false
        with_items:
        - '{{ result_nmcli_cmd_connections_names.stdout_lines }}'

      - name: Enable SSH Server firewalld Firewall Exception - Ensure NetworkManager connections
          are assigned to a firewalld zone
        ansible.builtin.command:
          cmd: nmcli connection modify {{ item.0 }} connection.zone {{ firewalld_sshd_zone
            }}
        register: result_nmcli_cmd_connections_assignment
        with_together:
        - '{{ result_nmcli_cmd_connections_names.stdout_lines }}'
        - '{{ result_nmcli_cmd_connections_zones.results }}'
        when:
        - item.1.stdout == '--'

      - name: Enable SSH Server firewalld Firewall Exception - Ensure NetworkManager connections
          changes are applied
        ansible.builtin.service:
          name: NetworkManager
          state: restarted
        when:
        - result_nmcli_cmd_connections_assignment is changed

      - name: Enable SSH Server firewalld Firewall Exception - Collect firewalld active
          zones
        ansible.builtin.shell:
          cmd: firewall-cmd --get-active-zones | grep -v interfaces
        register: result_firewall_cmd_zones_names
        changed_when: false

      - name: Enable SSH Server firewalld Firewall Exception - Ensure firewalld zones
          allow SSH
        ansible.builtin.command:
          cmd: firewall-cmd --permanent --zone={{ item }} --add-service=ssh
        register: result_nmcli_cmd_connections_assignment
        changed_when:
        - '''ALREADY_ENABLED'' not in result_nmcli_cmd_connections_assignment.stderr'
        with_items:
        - '{{ result_firewall_cmd_zones_names.stdout_lines }}'

      - name: Enable SSH Server firewalld Firewall Exception - Ensure firewalld changes
          are applied
        ansible.builtin.service:
          name: firewalld
          state: reloaded
        when:
        - result_nmcli_cmd_connections_assignment is changed
      when:
      - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      - ansible_facts.services['firewalld.service'].state == 'running'
      - ansible_facts.services['NetworkManager.service'].state == 'running'
      tags:
      - NIST-800-171-3.1.12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - configure_strategy
      - firewalld_sshd_port_enabled
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Enable SSH Server firewalld Firewall Exception - Informative message based
        on services states
      ansible.builtin.assert:
        that:
        - ansible_facts.services['firewalld.service'].state == 'running'
        - ansible_facts.services['NetworkManager.service'].state == 'running'
        fail_msg:
        - firewalld and NetworkManager services are not active. Remediation aborted!
        - This remediation could not be applied because it depends on firewalld and NetworkManager
          services running.
        - The service is not started by this remediation in order to prevent connection
          issues.
        success_msg:
        - Enable SSH Server firewalld Firewall Exception remediation successfully executed
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-171-3.1.12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(b)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - configure_strategy
      - firewalld_sshd_port_enabled
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

#### Rule Allow Only SSH Protocol 2 [ref]

Only SSH protocol version 2 connections should be permitted. The default setting in `/etc/ssh/sshd_config` is correct, and can be verified by ensuring that the following line appears:

    Protocol 2

Warning: As of `openssh-server` version `7.4` and above, the only protocol supported is version 2, and line

    Protocol 2

in `/etc/ssh/sshd_config` is not necessary.

Rationale: | SSH protocol version 1 is an insecure implementation of the SSH protocol and has many well-known vulnerability exploits. Exploits of the SSH daemon could provide immediate root access to the system.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_allow_only_protocol2

Identifiers and References

References: NT007(R1), 1, 12, 15, 16, 5, 8, 5.5.6, APO13.01, DSS01.04, DSS05.02, DSS05.03, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.1.13, 3.5.4, CCI-000197, CCI-000366, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.6, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, 0487, 1449, 1506, A.11.2.6, A.13.1.1, A.13.2.1, A.14.1.3, A.18.1.4, A.6.2.1, A.6.2.2, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, CIP-003-8 R4.2, CIP-007-3 R5.1, CIP-007-3 R7.1, CM-6(a), AC-17(a), AC-17(2), IA-5(1)(c), SC-13, MA-4(6), PR.AC-1, PR.AC-3, PR.AC-6, PR.AC-7, PR.PT-4, SRG-OS-000074-GPOS-00042, SRG-OS-000480-GPOS-00227

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

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

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

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Allow Only SSH Protocol 2
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*Protocol\s+
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*Protocol\s+
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*Protocol\s+
          line: Protocol 2
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.5.6
      - NIST-800-171-3.1.13
      - NIST-800-171-3.5.4
      - NIST-800-53-AC-17(2)
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(c)
      - NIST-800-53-MA-4(6)
      - NIST-800-53-SC-13
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - restrict_strategy
      - sshd_allow_only_protocol2

#### Rule Disable SSH Access via Empty Passwords [ref]

Disallow SSH login with empty passwords. The default SSH configuration disables logins with empty passwords. The appropriate configuration is used if no value is set for `PermitEmptyPasswords`.
To explicitly disallow SSH login from accounts with empty passwords, add or correct the following line in `/etc/ssh/sshd_config`:

    PermitEmptyPasswords no

Any accounts with empty passwords should be disabled immediately, and PAM configuration should prevent users from being able to assign themselves empty passwords.

Rationale: | Configuring this setting for the SSH daemon provides additional assurance that remote login via SSH will require a password, even in the event of misconfiguration elsewhere.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_disable_empty_passwords

Identifiers and References

References: NT007(R17), 11, 12, 13, 14, 15, 16, 18, 3, 5, 9, 5.5.6, APO01.06, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.03, DSS06.06, 3.1.1, 3.1.5, CCI-000366, CCI-000766, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 5.2, SR 7.6, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, AC-17(a), CM-7(a), CM-7(b), CM-6(a), PR.AC-4, PR.AC-6, PR.DS-5, PR.IP-1, PR.PT-3, FIA_UAU.1, Req-2.2.4, 2.2.6, SRG-OS-000106-GPOS-00053, SRG-OS-000480-GPOS-00229, SRG-OS-000480-GPOS-00227, 5.2.9, SV-230380r858715_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if [ -e "/etc/ssh/sshd_config" ] ; then

        LC_ALL=C sed -i "/^\s*PermitEmptyPasswords\s\+/Id" "/etc/ssh/sshd_config"
    else
        touch "/etc/ssh/sshd_config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config"

    cp "/etc/ssh/sshd_config" "/etc/ssh/sshd_config.bak"
    # Insert at the beginning of the file
    printf '%s\n' "PermitEmptyPasswords no" > "/etc/ssh/sshd_config"
    cat "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Disable SSH Access via Empty Passwords
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*PermitEmptyPasswords\s+
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*PermitEmptyPasswords\s+
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*PermitEmptyPasswords\s+
          line: PermitEmptyPasswords no
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.5.6
      - DISA-STIG-RHEL-08-020330
      - NIST-800-171-3.1.1
      - NIST-800-171-3.1.5
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSS-Req-2.2.4
      - PCI-DSSv4-2.2.6
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - restrict_strategy
      - sshd_disable_empty_passwords

#### Rule Disable GSSAPI Authentication [ref]

Unless needed, SSH should not permit extraneous or unnecessary authentication mechanisms like GSSAPI.
The default SSH configuration disallows authentications based on GSSAPI. The appropriate configuration is used if no value is set for `GSSAPIAuthentication`.
To explicitly disable GSSAPI authentication, add or correct the following line in `/etc/ssh/sshd_config`:

    GSSAPIAuthentication no

Rationale: | GSSAPI authentication is used to provide additional authentication mechanisms to applications. Allowing GSSAPI authentication through SSH exposes the system's GSSAPI to remote hosts, increasing the attack surface of the system.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_disable_gssapi_auth

Identifiers and References

References: 11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.1.12, CCI-000318, CCI-000368, CCI-001812, CCI-001813, CCI-001814, CCI-000366, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.4.3.2, 4.3.4.3.3, SR 7.6, 0418, 1055, 1402, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, CM-7(a), CM-7(b), CM-6(a), AC-17(a), PR.IP-1, FTP_ITC_EXT.1, FCS_SSH_EXT.1.2, SRG-OS-000364-GPOS-00151, SRG-OS-000480-GPOS-00227, SV-244528r858709_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if [ -e "/etc/ssh/sshd_config" ] ; then

        LC_ALL=C sed -i "/^\s*GSSAPIAuthentication\s\+/Id" "/etc/ssh/sshd_config"
    else
        touch "/etc/ssh/sshd_config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config"

    cp "/etc/ssh/sshd_config" "/etc/ssh/sshd_config.bak"
    # Insert at the beginning of the file
    printf '%s\n' "GSSAPIAuthentication no" > "/etc/ssh/sshd_config"
    cat "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Disable GSSAPI Authentication
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*GSSAPIAuthentication\s+
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*GSSAPIAuthentication\s+
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*GSSAPIAuthentication\s+
          line: GSSAPIAuthentication no
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010522
      - NIST-800-171-3.1.12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_disable_gssapi_auth

#### Rule Disable Kerberos Authentication [ref]

Unless needed, SSH should not permit extraneous or unnecessary authentication mechanisms like Kerberos.
The default SSH configuration disallows authentication validation through Kerberos. The appropriate configuration is used if no value is set for `KerberosAuthentication`.
To explicitly disable Kerberos authentication, add or correct the following line in `/etc/ssh/sshd_config`:

    KerberosAuthentication no

Rationale: | Kerberos authentication for SSH is often implemented using GSSAPI. If Kerberos is enabled through SSH, the SSH daemon provides a means of access to the system's Kerberos implementation. Configuring these settings for the SSH daemon provides additional assurance that remote logon via SSH will not use unused methods of authentication, even in the event of misconfiguration elsewhere.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_disable_kerb_auth

Identifiers and References

References: 11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.1.12, CCI-000318, CCI-000368, CCI-001812, CCI-001813, CCI-001814, CCI-000366, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.4.3.2, 4.3.4.3.3, SR 7.6, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, AC-17(a), CM-7(a), CM-7(b), CM-6(a), PR.IP-1, FTP_ITC_EXT.1, FCS_SSH_EXT.1.2, SRG-OS-000364-GPOS-00151, SRG-OS-000480-GPOS-00227, SV-230291r858707_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if [ -e "/etc/ssh/sshd_config" ] ; then

        LC_ALL=C sed -i "/^\s*KerberosAuthentication\s\+/Id" "/etc/ssh/sshd_config"
    else
        touch "/etc/ssh/sshd_config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config"

    cp "/etc/ssh/sshd_config" "/etc/ssh/sshd_config.bak"
    # Insert at the beginning of the file
    printf '%s\n' "KerberosAuthentication no" > "/etc/ssh/sshd_config"
    cat "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Disable Kerberos Authentication
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*KerberosAuthentication\s+
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*KerberosAuthentication\s+
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*KerberosAuthentication\s+
          line: KerberosAuthentication no
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010521
      - NIST-800-171-3.1.12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_disable_kerb_auth

#### Rule Disable SSH Support for .rhosts Files [ref]

SSH can emulate the behavior of the obsolete rsh command in allowing users to enable insecure access to their accounts via `.rhosts` files.
The default SSH configuration disables support for `.rhosts`. The appropriate configuration is used if no value is set for `IgnoreRhosts`.
To explicitly disable support for .rhosts files, add or correct the following line in `/etc/ssh/sshd_config`:

    IgnoreRhosts yes

Rationale: | SSH trust relationships mean a compromise on one host can allow an attacker to move trivially to other hosts.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_disable_rhosts

Identifiers and References

References: 11, 12, 14, 15, 16, 18, 3, 5, 9, 5.5.6, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.03, DSS06.06, 3.1.12, CCI-000366, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.6.1.2, A.7.1.1, A.9.1.2, A.9.2.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, AC-17(a), CM-7(a), CM-7(b), CM-6(a), PR.AC-4, PR.AC-6, PR.IP-1, PR.PT-3, FIA_UAU.1, 2.2.6, SRG-OS-000480-GPOS-00227, 5.2.11

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if [ -e "/etc/ssh/sshd_config" ] ; then

        LC_ALL=C sed -i "/^\s*IgnoreRhosts\s\+/Id" "/etc/ssh/sshd_config"
    else
        touch "/etc/ssh/sshd_config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config"

    cp "/etc/ssh/sshd_config" "/etc/ssh/sshd_config.bak"
    # Insert at the beginning of the file
    printf '%s\n' "IgnoreRhosts yes" > "/etc/ssh/sshd_config"
    cat "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Disable SSH Support for .rhosts Files
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*IgnoreRhosts\s+
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*IgnoreRhosts\s+
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*IgnoreRhosts\s+
          line: IgnoreRhosts yes
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.5.6
      - NIST-800-171-3.1.12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_disable_rhosts

#### Rule Disable SSH Root Login [ref]

The root user should never be allowed to login to a system directly over a network. To disable root login via SSH, add or correct the following line in `/etc/ssh/sshd_config`:

    PermitRootLogin no

Warning: This rule is disabled on Red Hat Virtualization Hosts and Managers, it will report not applicable. RHV hosts require root access to be managed by RHV Manager.

Rationale: | Even though the communications channel may be encrypted, an additional layer of security is gained by extending the policy of not logging directly on as root. In addition, logging in with a user-specific account provides individual accountability of actions performed on the system and also helps to minimize direct attack attempts on root's password.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_disable_root_login

Identifiers and References

References: BP28(R19), NT007(R21), 1, 11, 12, 13, 14, 15, 16, 18, 3, 5, 5.5.6, APO01.06, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.02, DSS06.03, DSS06.06, DSS06.10, 3.1.1, 3.1.5, CCI-000366, CCI-000770, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.18.1.4, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.2.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CIP-007-3 R5.2, CIP-007-3 R5.3.1, CIP-007-3 R5.3.2, CIP-007-3 R5.3.3, AC-6(2), AC-17(a), IA-2, IA-2(5), CM-7(a), CM-7(b), CM-6(a), PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.DS-5, PR.PT-3, FAU_GEN.1, Req-2.2.4, 2.2.6, SRG-OS-000109-GPOS-00056, SRG-OS-000480-GPOS-00227, SRG-APP-000148-CTR-000335, SRG-APP-000190-CTR-000500, 5.2.7, SV-230296r858711_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if [ -e "/etc/ssh/sshd_config" ] ; then

        LC_ALL=C sed -i "/^\s*PermitRootLogin\s\+/Id" "/etc/ssh/sshd_config"
    else
        touch "/etc/ssh/sshd_config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config"

    cp "/etc/ssh/sshd_config" "/etc/ssh/sshd_config.bak"
    # Insert at the beginning of the file
    printf '%s\n' "PermitRootLogin no" > "/etc/ssh/sshd_config"
    cat "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Disable SSH Root Login
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*PermitRootLogin\s+
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*PermitRootLogin\s+
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*PermitRootLogin\s+
          line: PermitRootLogin no
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.5.6
      - DISA-STIG-RHEL-08-010550
      - NIST-800-171-3.1.1
      - NIST-800-171-3.1.5
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-6(2)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-IA-2
      - NIST-800-53-IA-2(5)
      - PCI-DSS-Req-2.2.4
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_disable_root_login

#### Rule Disable SSH Support for User Known Hosts [ref]

SSH can allow system users to connect to systems if a cache of the remote systems public keys is available. This should be disabled.
To ensure this behavior is disabled, add or correct the following line in `/etc/ssh/sshd_config`:

    IgnoreUserKnownHosts yes

Rationale: | Configuring this setting for the SSH daemon provides additional assurance that remote login via SSH will require a password, even in the event of misconfiguration elsewhere.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_disable_user_known_hosts
Identifiers and References | References: 11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.1.12, CCI-000366, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.4.3.2, 4.3.4.3.3, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, AC-17(a), CM-7(a), CM-7(b), CM-6(a), PR.IP-1, FIA_UAU.1, SRG-OS-000480-GPOS-00227, SV-230290r858705_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if [ -e "/etc/ssh/sshd_config" ] ; then

        LC_ALL=C sed -i "/^\s*IgnoreUserKnownHosts\s\+/Id" "/etc/ssh/sshd_config"
    else
        touch "/etc/ssh/sshd_config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config"

    cp "/etc/ssh/sshd_config" "/etc/ssh/sshd_config.bak"
    # Insert at the beginning of the file
    printf '%s\n' "IgnoreUserKnownHosts yes" > "/etc/ssh/sshd_config"
    cat "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Disable SSH Support for User Known Hosts
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*IgnoreUserKnownHosts\s+
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*IgnoreUserKnownHosts\s+
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*IgnoreUserKnownHosts\s+
          line: IgnoreUserKnownHosts yes
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010520
      - NIST-800-171-3.1.12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_disable_user_known_hosts

#### Rule Disable X11 Forwarding [ref]

The X11Forwarding parameter provides the ability to tunnel X11 traffic through the connection to enable remote graphic connections. SSH has the capability to encrypt remote X11 connections when SSH's `X11Forwarding` option is enabled.
The default SSH configuration disables X11Forwarding. The appropriate configuration is used if no value is set for `X11Forwarding`.
To explicitly disable X11 Forwarding, add or correct the following line in `/etc/ssh/sshd_config`:

    X11Forwarding no

Rationale: | Disable X11 forwarding unless there is an operational requirement to use X11 applications directly. There is a small risk that the remote X11 servers of users who are logged in via SSH with X11 forwarding could be compromised by other users on the X11 server. Note that even if X11 forwarding is disabled, users can always install their own forwarders.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_disable_x11_forwarding
Identifiers and References | References: CCI-000366, CM-6(b), 2.2.6, SRG-OS-000480-GPOS-00227, 5.2.12, SV-230555r858721_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if [ -e "/etc/ssh/sshd_config" ] ; then

        LC_ALL=C sed -i "/^\s*X11Forwarding\s\+/Id" "/etc/ssh/sshd_config"
    else
        touch "/etc/ssh/sshd_config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config"

    cp "/etc/ssh/sshd_config" "/etc/ssh/sshd_config.bak"
    # Insert at the beginning of the file
    printf '%s\n' "X11Forwarding no" > "/etc/ssh/sshd_config"
    cat "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Disable X11 Forwarding
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*X11Forwarding\s+
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*X11Forwarding\s+
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*X11Forwarding\s+
          line: X11Forwarding no
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040340
      - NIST-800-53-CM-6(b)
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_disable_x11_forwarding

#### Rule Do Not Allow SSH Environment Options [ref]

Ensure that users are not able to override environment variables of the SSH daemon.
The default SSH configuration disables environment processing. The appropriate configuration is used if no value is set for `PermitUserEnvironment`.
To explicitly disable Environment options, add or correct the following `/etc/ssh/sshd_config`:

    PermitUserEnvironment no

Rationale: | SSH environment options potentially allow users to bypass access restriction in some configurations.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_do_not_permit_user_env

Identifiers and References

References: 11, 3, 9, 5.5.6, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.1.12, CCI-000366, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.4.3.2, 4.3.4.3.3, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, AC-17(a), CM-7(a), CM-7(b), CM-6(a), PR.IP-1, Req-2.2.4, 2.2.6, SRG-OS-000480-GPOS-00229, 5.2.10, SV-230330r877377_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if [ -e "/etc/ssh/sshd_config" ] ; then

        LC_ALL=C sed -i "/^\s*PermitUserEnvironment\s\+/Id" "/etc/ssh/sshd_config"
    else
        touch "/etc/ssh/sshd_config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config"

    cp "/etc/ssh/sshd_config" "/etc/ssh/sshd_config.bak"
    # Insert at the beginning of the file
    printf '%s\n' "PermitUserEnvironment no" > "/etc/ssh/sshd_config"
    cat "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Do Not Allow SSH Environment Options
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*PermitUserEnvironment\s+
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*PermitUserEnvironment\s+
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*PermitUserEnvironment\s+
          line: PermitUserEnvironment no
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.5.6
      - DISA-STIG-RHEL-08-010830
      - NIST-800-171-3.1.12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSS-Req-2.2.4
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_do_not_permit_user_env

#### Rule Enable Use of Strict Mode Checking [ref]

SSHs `StrictModes` option checks file and ownership permissions in the user's home directory `.ssh` folder before accepting login. If world- writable permissions are found, logon is rejected.
The default SSH configuration has `StrictModes` enabled. The appropriate configuration is used if no value is set for `StrictModes`.
To explicitly enable `StrictModes` in SSH, add or correct the following line in `/etc/ssh/sshd_config`:

    StrictModes yes

Rationale: | If other users have access to modify user-specific SSH configuration files, they may be able to log into the system as another user.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_enable_strictmodes

Identifiers and References

References: 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 3.1.12, CCI-000366, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, AC-6, AC-17(a), CM-6(a), PR.AC-4, PR.DS-5, SRG-OS-000480-GPOS-00227, SV-230288r858701_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if [ -e "/etc/ssh/sshd_config" ] ; then

        LC_ALL=C sed -i "/^\s*StrictModes\s\+/Id" "/etc/ssh/sshd_config"
    else
        touch "/etc/ssh/sshd_config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config"

    cp "/etc/ssh/sshd_config" "/etc/ssh/sshd_config.bak"
    # Insert at the beginning of the file
    printf '%s\n' "StrictModes yes" > "/etc/ssh/sshd_config"
    cat "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Enable Use of Strict Mode Checking
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*StrictModes\s+
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*StrictModes\s+
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*StrictModes\s+
          line: StrictModes yes
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010500
      - NIST-800-171-3.1.12
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-6
      - NIST-800-53-CM-6(a)
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_enable_strictmodes

#### Rule Enable SSH Warning Banner [ref]

To enable the warning banner and ensure it is consistent across the system, add or correct the following line in `/etc/ssh/sshd_config`:

    Banner /etc/issue

Another section contains information on how to create an appropriate system-wide warning banner.

Rationale: | The warning message reinforces policy awareness during the logon process and facilitates possible legal action against attackers. Alternatively, systems whose ownership should not be obvious should ensure usage of a banner that does not provide easy attribution.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_enable_warning_banner

Identifiers and References

References: 1, 12, 15, 16, 5.5.6, DSS05.04, DSS05.10, DSS06.10, 3.1.9, CCI-000048, CCI-000050, CCI-001384, CCI-001385, CCI-001386, CCI-001387, CCI-001388, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, AC-8(a), AC-8(c), AC-17(a), CM-6(a), PR.AC-7, FTA_TAB.1, Req-2.2.4, SRG-OS-000023-GPOS-00006, SRG-OS-000228-GPOS-00088, SV-230225r858694_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if [ -e "/etc/ssh/sshd_config" ] ; then

        LC_ALL=C sed -i "/^\s*Banner\s\+/Id" "/etc/ssh/sshd_config"
    else
        touch "/etc/ssh/sshd_config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config"

    cp "/etc/ssh/sshd_config" "/etc/ssh/sshd_config.bak"
    # Insert at the beginning of the file
    printf '%s\n' "Banner /etc/issue" > "/etc/ssh/sshd_config"
    cat "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Enable SSH Warning Banner
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*Banner\s+
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*Banner\s+
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*Banner\s+
          line: Banner /etc/issue
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - CJIS-5.5.6
      - DISA-STIG-RHEL-08-010040
      - NIST-800-171-3.1.9
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-8(a)
      - NIST-800-53-AC-8(c)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-2.2.4
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_enable_warning_banner

#### Rule Enable SSH Print Last Log [ref]

Ensure that SSH will display the date and time of the last successful account logon.
The default SSH configuration enables print of the date and time of the last login. The appropriate configuration is used if no value is set for `PrintLastLog`.
To explicitly enable LastLog in SSH, add or correct the following line in `/etc/ssh/sshd_config`:

    PrintLastLog yes

Rationale: | Providing users feedback on when account accesses last occurred facilitates user recognition and reporting of unauthorized account use.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_print_last_log
Identifiers and References | References: 1, 12, 15, 16, DSS05.04, DSS05.10, DSS06.10, CCI-000052, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, AC-9, AC-9(1), PR.AC-7, SRG-OS-000480-GPOS-00227, SV-230382r858717_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if [ -e "/etc/ssh/sshd_config" ] ; then

        LC_ALL=C sed -i "/^\s*PrintLastLog\s\+/Id" "/etc/ssh/sshd_config"
    else
        touch "/etc/ssh/sshd_config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config"

    cp "/etc/ssh/sshd_config" "/etc/ssh/sshd_config.bak"
    # Insert at the beginning of the file
    printf '%s\n' "PrintLastLog yes" > "/etc/ssh/sshd_config"
    cat "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Enable SSH Print Last Log
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*PrintLastLog\s+
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*PrintLastLog\s+
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*PrintLastLog\s+
          line: PrintLastLog yes
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-020350
      - NIST-800-53-AC-9
      - NIST-800-53-AC-9(1)
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_print_last_log

#### Rule Set LogLevel to INFO [ref]

The INFO parameter specifices that record login and logout activity will be logged.
The default SSH configuration sets the log level to INFO. The appropriate configuration is used if no value is set for `LogLevel`.
To explicitly specify the log level in SSH, add or correct the following line in `/etc/ssh/sshd_config`:

    LogLevel INFO

Rationale:

SSH provides several logging levels with varying amounts of verbosity. `DEBUG` is specifically not recommended other than strictly for debugging SSH communications since it provides so much data that it is difficult to identify important security information. `INFO` level is the basic level that only records login activity of SSH users. In many situations, such as Incident Response, it is important to determine when a particular user was active on a system. The logout record can eliminate those users who disconnected, which helps narrow the field.

Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_set_loglevel_info
Identifiers and References | References: AC-17(a), CM-6(a), 5.2.5

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    if [ -e "/etc/ssh/sshd_config" ] ; then

        LC_ALL=C sed -i "/^\s*LogLevel\s\+/Id" "/etc/ssh/sshd_config"
    else
        touch "/etc/ssh/sshd_config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config"

    cp "/etc/ssh/sshd_config" "/etc/ssh/sshd_config.bak"
    # Insert at the beginning of the file
    printf '%s\n' "LogLevel INFO" > "/etc/ssh/sshd_config"
    cat "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Set LogLevel to INFO
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*LogLevel\s+
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*LogLevel\s+
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*LogLevel\s+
          line: LogLevel INFO
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-AC-17(a)
      - NIST-800-53-CM-6(a)
      - low_complexity
      - low_disruption
      - low_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_set_loglevel_info

#### Rule Set SSH authentication attempt limit [ref]

The `MaxAuthTries` parameter specifies the maximum number of authentication attempts permitted per connection. Once the number of failures reaches half this value, additional failures are logged. to set MaxAUthTries edit `/etc/ssh/sshd_config` as follows:

    MaxAuthTries 5

Rationale: | Setting the MaxAuthTries parameter to a low number will minimize the risk of successful brute force attacks to the SSH server.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sshd_set_max_auth_tries
Identifiers and References | References: 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, 2.2.6, 5.2.16

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    sshd_max_auth_tries_value='5'

    if [ -e "/etc/ssh/sshd_config" ] ; then

        LC_ALL=C sed -i "/^\s*MaxAuthTries\s\+/Id" "/etc/ssh/sshd_config"
    else
        touch "/etc/ssh/sshd_config"
    fi
    # make sure file has newline at the end
    sed -i -e '$a\' "/etc/ssh/sshd_config"

    cp "/etc/ssh/sshd_config" "/etc/ssh/sshd_config.bak"
    # Insert at the beginning of the file
    printf '%s\n' "MaxAuthTries $sshd_max_auth_tries_value" > "/etc/ssh/sshd_config"
    cat "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
    # Clean up after ourselves.
    rm "/etc/ssh/sshd_config.bak"

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: XCCDF Value sshd_max_auth_tries_value # promote to variable
      set_fact:
        sshd_max_auth_tries_value: !!str 5
      tags:
        - always

    - name: Set SSH authentication attempt limit
      block:

      - name: Check for duplicate values
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*MaxAuthTries\s+
          state: absent
        check_mode: true
        changed_when: false
        register: dupes

      - name: Deduplicate values from /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*MaxAuthTries\s+
          state: absent
        when: dupes.found is defined and dupes.found > 1

      - name: Insert correct line to /etc/ssh/sshd_config
        lineinfile:
          path: /etc/ssh/sshd_config
          create: true
          regexp: (?i)^\s*MaxAuthTries\s+
          line: MaxAuthTries {{ sshd_max_auth_tries_value }}
          state: present
          insertbefore: BOF
          validate: /usr/sbin/sshd -t -f %s
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - PCI-DSSv4-2.2.6
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sshd_set_max_auth_tries

#### Rule Verify Permissions on SSH Server Private *_key Key Files [ref]

SSH server private keys - files that match the `/etc/ssh/*_key` glob, have to have restricted permissions. If those files are owned by the `root` user and the `root` group, they have to have the `0600` permission or stricter. If they are owned by the `root` user, but by a dedicated group `ssh_keys`, they can have the `0640` permission or stricter.
Rationale: | If an unauthorized user obtains the private SSH host key file, the host could be impersonated.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_permissions_sshd_private_key

Identifiers and References

References: BP28(R36), 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 3.1.13, 3.13.10, CCI-000366, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, AC-17(a), CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, Req-2.2.4, 2.2.6, SRG-OS-000480-GPOS-00227, 5.2.2, SV-230287r880714_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    for keyfile in /etc/ssh/*_key; do
        test -f "$keyfile" || continue
        if test root:root = "$(stat -c "%U:%G" "$keyfile")"; then

    	chmod u-xs,g-xwrs,o-xwrt "$keyfile"

        elif test root:ssh_keys = "$(stat -c "%U:%G" "$keyfile")"; then
    	chmod u-xs,g-xws,o-xwrt "$keyfile"
        else
            echo "Key-like file '$keyfile' is owned by an unexpected user:group combination"
        fi
    done

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    - name: Find root:root-owned keys
      ansible.builtin.command: find -H /etc/ssh/ -maxdepth 1 -user root -regex ".*_key$"
        -type f -group root -perm /u+xs,g+xwrs,o+xwrt
      register: root_owned_keys
      changed_when: false
      failed_when: false
      check_mode: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010490
      - NIST-800-171-3.1.13
      - NIST-800-171-3.13.10
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-2.2.4
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_permissions_sshd_private_key
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Set permissions for root:root-owned keys
      ansible.builtin.file:
        path: '{{ item }}'
        mode: u-xs,g-xwrs,o-xwrt
        state: file
      with_items:
      - '{{ root_owned_keys.stdout_lines }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010490
      - NIST-800-171-3.1.13
      - NIST-800-171-3.13.10
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-2.2.4
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_permissions_sshd_private_key
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Find root:ssh_keys-owned keys
      ansible.builtin.command: find -H /etc/ssh/ -maxdepth 1 -user root -regex ".*_key$"
        -type f -group ssh_keys -perm /u+xs,g+xws,o+xwrt
      register: dedicated_group_owned_keys
      changed_when: false
      failed_when: false
      check_mode: false
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010490
      - NIST-800-171-3.1.13
      - NIST-800-171-3.13.10
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-2.2.4
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_permissions_sshd_private_key
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

    - name: Set permissions for root:ssh_keys-owned keys
      ansible.builtin.file:
        path: '{{ item }}'
        mode: u-xs,g-xws,o-xwrt
        state: file
      with_items:
      - '{{ dedicated_group_owned_keys.stdout_lines }}'
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-010490
      - NIST-800-171-3.1.13
      - NIST-800-171-3.13.10
      - NIST-800-53-AC-17(a)
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-2.2.4
      - PCI-DSSv4-2.2.6
      - configure_strategy
      - file_permissions_sshd_private_key
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed

Remediation Puppet snippet ⇲

    include ssh_private_key_perms

    class ssh_private_key_perms {
      exec { 'sshd_priv_key':
        command => "chmod 0640 /etc/ssh/*_key",
        path    => '/bin:/usr/bin'
      }
    }

Group USBGuard daemon Group contains 3 rules
[ref] The USBGuard daemon enforces the USB device authorization policy for all USB devices.

#### Rule Install usbguard Package [ref]

The `usbguard` package can be installed with the following command:

    $ sudo yum install usbguard

Rationale: | `usbguard` is a software framework that helps to protect against rogue USB devices by implementing basic whitelisting/blacklisting capabilities based on USB device attributes.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_usbguard_installed
Identifiers and References | References: CCI-001958, 1418, CM-8(3), IA-3, SRG-OS-000378-GPOS-00163, SV-244547r854076_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    # Remediation is applicable only in certain platforms
    if ( [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && ! grep -q s390x /proc/sys/kernel/osrelease ); then

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

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    package --add=usbguard

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: Ensure usbguard is installed
      package:
        name: usbguard
        state: present
      when: ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman",
        "container"] and ansible_architecture != "s390x" )
      tags:
      - DISA-STIG-RHEL-08-040139
      - NIST-800-53-CM-8(3)
      - NIST-800-53-IA-3
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_usbguard_installed

Remediation OSBuild Blueprint snippet ⇲

    [[packages]]
    name = "usbguard"
    version = "*"

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include install_usbguard

    class install_usbguard {
      package { 'usbguard':
        ensure => 'installed',
      }
    }

Remediation Kubernetes snippet ⇲

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    spec:
      config:
        ignition:
          version: 3.1.0
      extensions:
        - usbguard

#### Rule Enable the USBGuard Service [ref]

The USBGuard service should be enabled. The `usbguard` service can be enabled with the following command:

    $ sudo systemctl enable usbguard.service

Rationale: | The `usbguard` service must be running in order to enforce the USB device authorization policy for all USB devices.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_service_usbguard_enabled
Identifiers and References | References: CCI-000416, CCI-001958, 1418, CM-8(3)(a), IA-3, FMT_SMF_EXT.1, SRG-OS-000378-GPOS-00163, SV-244548r854077_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    # Remediation is applicable only in certain platforms
    if ( [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && ! grep -q s390x /proc/sys/kernel/osrelease ); then

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" unmask 'usbguard.service'
    "$SYSTEMCTL_EXEC" start 'usbguard.service'
    "$SYSTEMCTL_EXEC" enable 'usbguard.service'

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: Enable service usbguard
      block:

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

      - name: Enable service usbguard
        systemd:
          name: usbguard
          enabled: 'yes'
          state: started
          masked: 'no'
        when:
        - '"usbguard" in ansible_facts.packages'
      when: ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman",
        "container"] and ansible_architecture != "s390x" )
      tags:
      - DISA-STIG-RHEL-08-040141
      - NIST-800-53-CM-8(3)(a)
      - NIST-800-53-IA-3
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - service_usbguard_enabled

Remediation OSBuild Blueprint snippet ⇲

    [customizations.services]
    enabled = ["usbguard"]

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include enable_usbguard

    class enable_usbguard {
      service {'usbguard':
        enable => true,
        ensure => 'running',
      }
    }

Remediation Kubernetes snippet ⇲

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    metadata:
      annotations:
        complianceascode.io/depends-on: xccdf_org.ssgproject.content_rule_package_usbguard_installed
    spec:
      config:
        ignition:
          version: 3.1.0
        systemd:
          units:
          - name: usbguard.service
            enabled: true

#### Rule Authorize Human Interface Devices and USB hubs in USBGuard daemon [ref]

To allow authorization of USB devices combining human interface device and hub capabilities by USBGuard daemon, add the line `allow with-interface match-all { 03:*:* 09:00:* }` to `/etc/usbguard/rules.conf`.
Warning: This rule should be understood primarily as a convenience administration feature. This rule ensures that if the USBGuard default rules.conf file is present, it will alter it so that USB human interface devices and hubs are allowed. However, if the rules.conf file is altered by system administrator, the rule does not check if USB human interface devices and hubs are allowed. This assumes that an administrator modified the file with some purpose in mind.

Rationale: | Without allowing Human Interface Devices, it might not be possible to interact with the system. Without allowing hubs, it might not be possible to use any USB devices on the system.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_usbguard_allow_hid_and_hub
Identifiers and References | References: CM-8(3), IA-3, FMT_SMF_EXT.1, SRG-OS-000114-GPOS-00059

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if ( [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && ! grep -q s390x /proc/sys/kernel/osrelease ); then

    echo "allow with-interface match-all { 03:*:* 09:00:* }" >> /etc/usbguard/rules.conf

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | configure

    - name: Allow HID devices and hubs
      lineinfile:
        path: /etc/usbguard/rules.conf
        create: true
        line: allow with-interface match-all { 03:*:* 09:00:* }
        state: present
      when: ( ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman",
        "container"] and ansible_architecture != "s390x" )
      tags:
      - NIST-800-53-CM-8(3)
      - NIST-800-53-IA-3
      - configure_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - usbguard_allow_hid_and_hub

Remediation Kubernetes snippet ⇲

    ---
    apiVersion: machineconfiguration.openshift.io/v1
    kind: MachineConfig
    metadata:
      annotations:
        complianceascode.io/depends-on: xccdf_org.ssgproject.content_rule_package_usbguard_installed
    spec:
      config:
        ignition:
          version: 3.1.0
        storage:
          files:
          - contents:
              source: data:,{{ %0Aallow%20with-interface%20match-all%20%7B%2003%3A%2A%3A%2A%2009%3A00%3A%2A%20%7D }}
            mode: 0600
            path: /etc/usbguard/rules.d/75-hid-and-hub.conf
            overwrite: true

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.