Guide to the Secure Configuration of Red Hat Virtualization 4

with profile VPP - Protection Profile for Virtualization v. 1.0 for Red Hat Virtualization Host (RHVH)
This compliance profile reflects the core set of security related configuration settings for deployment of Red Hat Virtualization Host (RHVH) 4.x into U.S. Defense, Intelligence, and Civilian agencies. Development partners and sponsors include the U.S. National Institute of Standards and Technology (NIST), U.S. Department of Defense, the National Security Agency, and Red Hat. This baseline implements configuration requirements from the following sources: - Committee on National Security Systems Instruction No. 1253 (CNSSI 1253) - NIST 800-53 control selections for MODERATE impact systems (NIST 800-53) - U.S. Government Configuration Baseline (USGCB) - NIAP Protection Profile for Virtualization v1.0 (VPP v1.0) For any differing configuration requirements, e.g. password lengths, the stricter security setting was chosen. Security Requirement Traceability Guides (RTMs) and sample System Security Configuration Guides are provided via the scap-security-guide-docs package. This profile reflects U.S. Government consensus content and is developed through the ComplianceAsCode project, championed by the National Security Agency. Except for differences in formatting to accommodate publishing processes, this profile mirrors ComplianceAsCode content as minor divergences, such as bugfixes, work through the consensus and release processes.
This guide presents a catalog of security-relevant configuration settings for Red Hat Virtualization 4. It is a rendering of content structured in the eXtensible Configuration Checklist Description Format (XCCDF) in order to support security automation. The SCAP content is is available in the scap-security-guide package which is developed at https://www.open-scap.org/security-policies/scap-security-guide.

Providing system administrators with such guidance informs them how to securely configure systems under their control in a variety of network roles. Policy makers and baseline creators can use this catalog of settings, with its associated references to higher-level security control catalogs, in order to assist them in security baseline creation. This guide is a catalog, not a checklist, and satisfaction of every item is not likely to be possible or sensible in many operational scenarios. However, the XCCDF format enables granular selection and adjustment of settings, and their association with OVAL and OCIL content provides an automated checking capability. Transformations of this document, and its associated automated checking content, are capable of providing baselines that meet a diverse set of policy objectives. Some example XCCDF Profiles, which are selections of items that form checklists and can be used as baselines, are available with this guide. They can be processed, in an automated fashion, with tools that support the Security Content Automation Protocol (SCAP). The DISA STIG, which provides required settings for US Department of Defense systems, is one example of a baseline created from this guidance.
Do not attempt to implement any of the settings in this guide without first testing them in a non-operational environment. The creators of this guidance assume no responsibility whatsoever for its use by other parties, and makes no guarantees, expressed or implied, about its quality, reliability, or any other characteristic.
Profile TitleVPP - Protection Profile for Virtualization v. 1.0 for Red Hat Virtualization Host (RHVH)
Profile IDxccdf_org.ssgproject.content_profile_rhvh-vpp

Revision History

Current version: 0.1.60

  • draft (as of 2022-02-23)

Platforms

  • cpe:/o:redhat:enterprise_linux:8::hypervisor
  • cpe:/a:redhat:enterprise_virtualization_manager:4

Table of Contents

  1. System Settings
    1. Installing and Maintaining Software
    2. Account and Access Control
    3. System Accounting with auditd
    4. GRUB2 bootloader configuration
    5. Network Configuration and Firewalls
    6. File Permissions and Masks
    7. SELinux
  2. Services
    1. SSH Server
    2. System Security Services Daemon

Checklist

contains 143 rules

System Settingsgroup

Contains rules that check correct system settings.

contains 126 rules

Installing and Maintaining Softwaregroup

The following sections contain information on security-relevant choices during the initial operating system installation process and the setup of software updates.

contains 17 rules

System and Software Integritygroup

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.

contains 12 rules

Software Integrity Checkinggroup

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.

contains 3 rules

Verify Integrity with RPMgroup

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.

contains 3 rules

Verify File Hashes with RPMrule

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.

Remediation 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 script:
- 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
    - high_complexity
    - high_severity
    - medium_disruption
    - no_reboot_needed
    - restrict_strategy
    - rpm_verify_hashes

- name: 'Set fact: Package manager reinstall command (yum)'
  set_fact:
    package_manager_reinstall_cmd: yum reinstall -y
  when: (ansible_distribution == "RedHat" or ansible_distribution == "OracleLinux")
  tags:
    - CJIS-5.10.4.1
    - NIST-800-171-3.3.8
    - NIST-800-171-3.4.1
    - NIST-800-53-AU-9(3)
    - NIST-800-53-CM-6(c)
    - NIST-800-53-CM-6(d)
    - NIST-800-53-SI-7
    - NIST-800-53-SI-7(1)
    - NIST-800-53-SI-7(6)
    - PCI-DSS-Req-11.5
    - high_complexity
    - high_severity
    - medium_disruption
    - no_reboot_needed
    - restrict_strategy
    - rpm_verify_hashes

- name: Read files with incorrect hash
  command: rpm -Va --nodeps --nosize --nomtime --nordev --nocaps --nolinkto --nouser
    --nogroup --nomode --noghost --noconfig
  args:
    warn: false
  register: files_with_incorrect_hash
  changed_when: false
  failed_when: files_with_incorrect_hash.rc > 1
  check_mode: false
  when: (package_manager_reinstall_cmd is defined)
  tags:
    - CJIS-5.10.4.1
    - NIST-800-171-3.3.8
    - NIST-800-171-3.4.1
    - NIST-800-53-AU-9(3)
    - NIST-800-53-CM-6(c)
    - NIST-800-53-CM-6(d)
    - NIST-800-53-SI-7
    - NIST-800-53-SI-7(1)
    - NIST-800-53-SI-7(6)
    - PCI-DSS-Req-11.5
    - high_complexity
    - high_severity
    - medium_disruption
    - no_reboot_needed
    - restrict_strategy
    - rpm_verify_hashes

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

- name: Reinstall packages of files with incorrect hash
  command: '{{ package_manager_reinstall_cmd }} ''{{ item }}'''
  args:
    warn: false
  with_items: '{{ list_of_packages.results | map(attribute=''stdout_lines'') | list
    | unique }}'
  when:
    - files_with_incorrect_hash.stdout_lines is defined
    - (package_manager_reinstall_cmd is defined and (files_with_incorrect_hash.stdout_lines
      | length > 0))
  tags:
    - CJIS-5.10.4.1
    - NIST-800-171-3.3.8
    - NIST-800-171-3.4.1
    - NIST-800-53-AU-9(3)
    - NIST-800-53-CM-6(c)
    - NIST-800-53-CM-6(d)
    - NIST-800-53-SI-7
    - NIST-800-53-SI-7(1)
    - NIST-800-53-SI-7(6)
    - PCI-DSS-Req-11.5
    - high_complexity
    - high_severity
    - medium_disruption
    - no_reboot_needed
    - restrict_strategy
    - rpm_verify_hashes

Verify and Correct Ownership with RPMrule

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.

Remediation script:

# 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 script:
- name: Read list of files with incorrect ownership
  command: rpm -Va --nodeps --nosignature --nofiledigest --nosize --nomtime --nordev
    --nocaps --nolinkto --nomode
  args:
    warn: false
  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
    - high_complexity
    - high_severity
    - medium_disruption
    - no_reboot_needed
    - restrict_strategy
    - rpm_verify_ownership

- name: Create list of packages
  command: rpm -qf "{{ item }}"
  args:
    warn: false
  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
    - high_complexity
    - high_severity
    - medium_disruption
    - no_reboot_needed
    - restrict_strategy
    - rpm_verify_ownership

- name: Correct file ownership with RPM
  command: rpm --quiet --setugids '{{ item }}'
  args:
    warn: false
  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
    - high_complexity
    - high_severity
    - medium_disruption
    - no_reboot_needed
    - restrict_strategy
    - rpm_verify_ownership

Verify and Correct File Permissions with RPMrule

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.

Remediation script:

# 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 script:
- name: Read list of files with incorrect permissions
  command: rpm -Va --nodeps --nosignature --nofiledigest --nosize --nomtime --nordev
    --nocaps --nolinkto --nouser --nogroup
  args:
    warn: false
  register: files_with_incorrect_permissions
  failed_when: files_with_incorrect_permissions.rc > 1
  changed_when: false
  check_mode: false
  tags:
    - CJIS-5.10.4.1
    - NIST-800-171-3.3.8
    - NIST-800-171-3.4.1
    - NIST-800-53-AU-9(3)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-CM-6(c)
    - NIST-800-53-CM-6(d)
    - NIST-800-53-SI-7
    - NIST-800-53-SI-7(1)
    - NIST-800-53-SI-7(6)
    - PCI-DSS-Req-11.5
    - high_complexity
    - high_severity
    - medium_disruption
    - no_reboot_needed
    - restrict_strategy
    - rpm_verify_permissions

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

- name: Correct file permissions with RPM
  command: rpm --setperms '{{ item }}'
  args:
    warn: false
  with_items: '{{ list_of_packages.results | map(attribute=''stdout_lines'') | list
    | unique }}'
  when: (files_with_incorrect_permissions.stdout_lines | length > 0)
  tags:
    - CJIS-5.10.4.1
    - NIST-800-171-3.3.8
    - NIST-800-171-3.4.1
    - NIST-800-53-AU-9(3)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-CM-6(c)
    - NIST-800-53-CM-6(d)
    - NIST-800-53-SI-7
    - NIST-800-53-SI-7(1)
    - NIST-800-53-SI-7(6)
    - PCI-DSS-Req-11.5
    - high_complexity
    - high_severity
    - medium_disruption
    - no_reboot_needed
    - restrict_strategy
    - rpm_verify_permissions

Federal Information Processing Standard (FIPS)group

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 Virtualization 4.

See http://csrc.nist.gov/publications/PubsFIPS.html for more information.

contains 1 rule

Enable FIPS Moderule

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
This rule also ensures that the system policy is set to FIPS:OSPP. Furthermore, the system running in FIPS mode should be FIPS certified by NIST.

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:

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.

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

var_system_crypto_policy='FIPS:OSPP'


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

System Cryptographic Policiesgroup

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.

contains 6 rules

Configure BIND to use System Crypto Policyrule

Crypto Policies provide a centralized control over crypto algorithms usage of many packages. BIND is supported by crypto policy, but the BIND configuration may be set up to ignore it. To check that Crypto Policies settings are configured correctly, ensure that the /etc/named.conf includes the appropriate configuration: In the options section of /etc/named.conf, make sure that the following line is not commented out or superseded by later includes: include "/etc/crypto-policies/back-ends/bind.config";

Rationale:

Overriding the system crypto policy makes the behavior of the BIND service violate expectations, and makes system configuration more fragmented.

Remediation script:

function remediate_bind_crypto_policy() {
	CONFIG_FILE="/etc/named.conf"
	if test -f "$CONFIG_FILE"; then
		sed -i 's|options {|&\n\tinclude "/etc/crypto-policies/back-ends/bind.config";|' "$CONFIG_FILE"
		return 0
	else
		echo "Aborting remediation as '$CONFIG_FILE' was not even found." >&2
		return 1
	fi
}

remediate_bind_crypto_policy

Configure System Cryptography Policyrule

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

$ sudo update-crypto-policies --set FIPS:OSPP
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.

Remediation script:

var_system_crypto_policy='FIPS:OSPP'


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

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

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

Configure Kerberos to use System Crypto Policyrule

Crypto Policies provide a centralized control over crypto algorithms usage of many packages. Kerberos is supported by crypto policy, but it's configuration may be set up to ignore it. To check that Crypto Policies settings for Kerberos are configured correctly, examine that there is a symlink at /etc/krb5.conf.d/crypto-policies targeting /etc/cypto-policies/back-ends/krb5.config. If the symlink exists, kerberos is configured to use the system-wide crypto policy settings.

Rationale:

Overriding the system crypto policy makes the behavior of Kerberos violate expectations, and makes system configuration more fragmented.

Remediation script:

rm -f /etc/krb5.conf.d/crypto-policies
ln -s /etc/crypto-policies/back-ends/krb5.config /etc/krb5.conf.d/crypto-policies
Remediation script:
- name: Configure Kerberos to use System Crypto Policy
  file:
    src: /etc/crypto-policies/back-ends/krb5.config
    path: /etc/krb5.conf.d/crypto-policies
    state: link
  tags:
    - NIST-800-53-SC-12(2)
    - NIST-800-53-SC-12(3)
    - NIST-800-53-SC-13
    - configure_kerberos_crypto_policy
    - configure_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required

Configure Libreswan to use System Crypto Policyrule

Crypto Policies provide a centralized control over crypto algorithms usage of many packages. Libreswan is supported by system crypto policy, but the Libreswan configuration may be set up to ignore it. To check that Crypto Policies settings are configured correctly, ensure that the /etc/ipsec.conf includes the appropriate configuration file. In /etc/ipsec.conf, make sure that the following line is not commented out or superseded by later includes: include /etc/crypto-policies/back-ends/libreswan.config

Rationale:

Overriding the system crypto policy makes the behavior of the Libreswan service violate expectations, and makes system configuration more fragmented.

Remediation script:

function remediate_libreswan_crypto_policy() {
    CONFIG_FILE="/etc/ipsec.conf"
    if ! grep -qP "^\s*include\s+/etc/crypto-policies/back-ends/libreswan.config\s*(?:#.*)?$" "$CONFIG_FILE" ; then
        echo 'include /etc/crypto-policies/back-ends/libreswan.config' >> "$CONFIG_FILE"
    fi
    return 0
}

remediate_libreswan_crypto_policy
Remediation script:
- name: Configure Libreswan to use System Crypto Policy
  lineinfile:
    path: /etc/ipsec.conf
    line: include /etc/crypto-policies/back-ends/libreswan.config
    create: true
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-MA-4(6)
    - NIST-800-53-SC-12(2)
    - NIST-800-53-SC-12(3)
    - NIST-800-53-SC-13
    - configure_libreswan_crypto_policy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Configure OpenSSL library to use System Crypto Policyrule

Crypto Policies provide a centralized control over crypto algorithms usage of many packages. OpenSSL is supported by crypto policy, but the OpenSSL configuration may be set up to ignore it. To check that Crypto Policies settings are configured correctly, you have to examine the OpenSSL config file available under /etc/pki/tls/openssl.cnf. This file has the ini format, and it enables crypto policy support if there is a [ crypto_policy ] section that contains the .include /etc/crypto-policies/back-ends/opensslcnf.config directive.

Rationale:

Overriding the system crypto policy makes the behavior of the Java runtime violates expectations, and makes system configuration more fragmented.

Remediation script:

OPENSSL_CRYPTO_POLICY_SECTION='[ crypto_policy ]'
OPENSSL_CRYPTO_POLICY_SECTION_REGEX='\[\s*crypto_policy\s*\]'
OPENSSL_CRYPTO_POLICY_INCLUSION='.include /etc/crypto-policies/back-ends/opensslcnf.config'
OPENSSL_CRYPTO_POLICY_INCLUSION_REGEX='^\s*\.include\s*/etc/crypto-policies/back-ends/opensslcnf.config$'

function remediate_openssl_crypto_policy() {
	CONFIG_FILE="/etc/pki/tls/openssl.cnf"
	if test -f "$CONFIG_FILE"; then
		if ! grep -q "^\\s*$OPENSSL_CRYPTO_POLICY_SECTION_REGEX" "$CONFIG_FILE"; then
			printf '\n%s\n\n%s' "$OPENSSL_CRYPTO_POLICY_SECTION" "$OPENSSL_CRYPTO_POLICY_INCLUSION" >> "$CONFIG_FILE"
			return 0
		elif ! grep -q "^\\s*$OPENSSL_CRYPTO_POLICY_INCLUSION_REGEX" "$CONFIG_FILE"; then
			sed -i "s|$OPENSSL_CRYPTO_POLICY_SECTION_REGEX|&\\n\\n$OPENSSL_CRYPTO_POLICY_INCLUSION\\n|" "$CONFIG_FILE"
			return 0
		fi
	else
		echo "Aborting remediation as '$CONFIG_FILE' was not even found." >&2
		return 1
	fi
}

remediate_openssl_crypto_policy
Remediation script:
- name: Test for crypto_policy group
  command: grep '^\s*\[\s*crypto_policy\s*]' /etc/pki/tls/openssl.cnf
  register: test_crypto_policy_group
  ignore_errors: true
  changed_when: false
  check_mode: false
  tags:
    - NIST-800-53-AC-17(2)
    - NIST-800-53-AC-17(a)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-MA-4(6)
    - NIST-800-53-SC-12(2)
    - NIST-800-53-SC-12(3)
    - NIST-800-53-SC-13
    - configure_openssl_crypto_policy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed
    - unknown_strategy

- name: Add .include for opensslcnf.config to crypto_policy section
  lineinfile:
    create: true
    insertafter: ^\s*\[\s*crypto_policy\s*]\s*
    line: .include /etc/crypto-policies/back-ends/opensslcnf.config
    path: /etc/pki/tls/openssl.cnf
  when:
    - test_crypto_policy_group.stdout is defined
    - test_crypto_policy_group.stdout | length > 0
  tags:
    - NIST-800-53-AC-17(2)
    - NIST-800-53-AC-17(a)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-MA-4(6)
    - NIST-800-53-SC-12(2)
    - NIST-800-53-SC-12(3)
    - NIST-800-53-SC-13
    - configure_openssl_crypto_policy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed
    - unknown_strategy

- name: Add crypto_policy group and set include opensslcnf.config
  lineinfile:
    create: true
    line: |-
      [crypto_policy]
      .include /etc/crypto-policies/back-ends/opensslcnf.config
    path: /etc/pki/tls/openssl.cnf
  when:
    - test_crypto_policy_group.stdout is defined
    - test_crypto_policy_group.stdout | length < 1
  tags:
    - NIST-800-53-AC-17(2)
    - NIST-800-53-AC-17(a)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-MA-4(6)
    - NIST-800-53-SC-12(2)
    - NIST-800-53-SC-12(3)
    - NIST-800-53-SC-13
    - configure_openssl_crypto_policy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed
    - unknown_strategy

Configure SSH to use System Crypto Policyrule

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.

Remediation script:

SSH_CONF="/etc/sysconfig/sshd"

sed -i "/^\s*CRYPTO_POLICY.*$/d" $SSH_CONF
Remediation script:
- name: Configure SSH to use System Crypto Policy
  lineinfile:
    dest: /etc/sysconfig/sshd
    state: absent
    regexp: ^\s*CRYPTO_POLICY.*$
  tags:
    - NIST-800-53-AC-17(2)
    - NIST-800-53-AC-17(a)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-MA-4(6)
    - NIST-800-53-SC-13
    - configure_ssh_crypto_policy
    - disable_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - reboot_required

Operating System Vendor Support and Certificationgroup

The assurance of a vendor to provide operating system support and maintenance for their product is an important criterion to ensure product stability and security over the life of the product. A certified product that follows the necessary standards and government certification requirements guarantees that known software vulnerabilities will be remediated, and proper guidance for protecting and securing the operating system will be given.

contains 2 rules

The Installed Operating System Is FIPS 140-2 Certifiedrule

To enable processing of sensitive information the operating system must provide certified cryptographic modules compliant with FIPS 140-2 standard.

warning  There is no remediation besides switching to a different operating system.
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:

The Federal Information Processing Standard (FIPS) Publication 140-2, (FIPS PUB 140-2) is a computer security standard. The standard specifies security requirements for cryptographic modules used to protect sensitive unclassified information. Refer to the full FIPS 140-2 standard at http://csrc.nist.gov/publications/fips/fips140-2/fips1402.pdf for further details on the requirements. FIPS 140-2 validation is required by U.S. law when information systems use cryptography to protect sensitive government information. In order to achieve FIPS 140-2 certification, cryptographic modules are subject to extensive testing by independent laboratories, accredited by National Institute of Standards and Technology (NIST).

The Installed Operating System Is Vendor Supportedrule

The installed operating system must be maintained by a vendor. Red Hat Enterprise Linux is supported by Red Hat, Inc. As the Red Hat Enterprise Linux vendor, Red Hat, Inc. is responsible for providing security patches.

warning  There is no remediation besides switching to a different operating system.
Rationale:

An operating system is considered "supported" if the vendor continues to provide security patches for the product. With an unsupported release, it will not be possible to resolve any security issue discovered in the system software.

Updating Softwaregroup

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 Virtualization 4 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.

contains 5 rules

Ensure yum Removes Previous Package Versionsrule

yum should be configured to remove previous software components after new versions have been installed. To configure yum to remove the previous software components after updating, set the clean_requirements_on_remove to 1 in /etc/yum.conf.

Rationale:

Previous versions of software components that are not removed from the information system after updates have been installed may be exploited by some adversaries.

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

if grep --silent ^clean_requirements_on_remove /etc/yum.conf ; then
        sed -i "s/^clean_requirements_on_remove.*/clean_requirements_on_remove=1/g" /etc/yum.conf
else
        echo -e "\n# Set clean_requirements_on_remove to 1 per security requirements" >> /etc/yum.conf
        echo "clean_requirements_on_remove=1" >> /etc/yum.conf
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.4.8
    - NIST-800-53-CM-11(a)
    - NIST-800-53-CM-11(b)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SI-2(6)
    - clean_components_post_updating
    - low_complexity
    - low_disruption
    - low_severity
    - no_reboot_needed
    - restrict_strategy

- name: Ensure YUM Removes Previous Package Versions
  lineinfile:
    dest: /etc/yum.conf
    regexp: ^#?clean_requirements_on_remove
    line: clean_requirements_on_remove=1
    insertafter: \[main\]
    create: true
  when: '"yum" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.4.8
    - NIST-800-53-CM-11(a)
    - NIST-800-53-CM-11(b)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SI-2(6)
    - clean_components_post_updating
    - low_complexity
    - low_disruption
    - low_severity
    - no_reboot_needed
    - restrict_strategy

Ensure gpgcheck Enabled In Main yum Configurationrule

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).

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

# Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
# Otherwise, regular sed command will do.
sed_command=('sed' '-i')
if test -L "/etc/yum.conf"; then
    sed_command+=('--follow-symlinks')
fi

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

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

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^gpgcheck\\>" "/etc/yum.conf"; then
    "${sed_command[@]}" "s/^gpgcheck\\>.*/$formatted_output/gi" "/etc/yum.conf"
else
    # \n is precaution for case where file ends without trailing newline
    
    printf '%s\n' "$formatted_output" >> "/etc/yum.conf"
fi

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

- name: Ensure GPG check is globally activated
  ini_file:
    dest: /etc/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
    - NIST-800-171-3.4.8
    - NIST-800-53-CM-11(a)
    - NIST-800-53-CM-11(b)
    - NIST-800-53-CM-5(3)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SA-12
    - NIST-800-53-SA-12(10)
    - NIST-800-53-SC-12
    - NIST-800-53-SC-12(3)
    - NIST-800-53-SI-7
    - PCI-DSS-Req-6.2
    - configure_strategy
    - ensure_gpgcheck_globally_activated
    - high_severity
    - low_complexity
    - medium_disruption
    - no_reboot_needed

Ensure gpgcheck Enabled for Local Packagesrule

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.

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

# Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
# Otherwise, regular sed command will do.
sed_command=('sed' '-i')
if test -L "/etc/yum.conf"; then
    sed_command+=('--follow-symlinks')
fi

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^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
    "${sed_command[@]}" "s/^localpkg_gpgcheck\\>.*/$formatted_output/gi" "/etc/yum.conf"
else
    # \n is precaution for case where file ends without trailing newline
    
    printf '%s\n' "$formatted_output" >> "/etc/yum.conf"
fi

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

- name: Ensure GPG check Enabled for Local Packages (Yum)
  ini_file:
    dest: /etc/yum.conf
    section: main
    option: localpkg_gpgcheck
    value: 1
    no_extra_spaces: true
    create: true
  when: '"yum" in ansible_facts.packages'
  tags:
    - 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

Ensure gpgcheck Enabled for All yum Package Repositoriesrule

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)."

Remediation script:
sed -i 's/gpgcheck\s*=.*/gpgcheck=1/g' /etc/yum.repos.d/*
Remediation script:
- name: Grep for yum repo section names
  shell: |
    set -o pipefail
    grep -HEr '^\[.+\]' -r /etc/yum.repos.d/
  register: repo_grep_results
  ignore_errors: true
  changed_when: false
  tags:
    - CJIS-5.10.4.1
    - NIST-800-171-3.4.8
    - NIST-800-53-CM-11(a)
    - NIST-800-53-CM-11(b)
    - NIST-800-53-CM-5(3)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SA-12
    - NIST-800-53-SA-12(10)
    - NIST-800-53-SC-12
    - NIST-800-53-SC-12(3)
    - NIST-800-53-SI-7
    - PCI-DSS-Req-6.2
    - enable_strategy
    - ensure_gpgcheck_never_disabled
    - high_severity
    - low_complexity
    - medium_disruption
    - no_reboot_needed

- name: Set gpgcheck=1 for each 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
    - NIST-800-171-3.4.8
    - NIST-800-53-CM-11(a)
    - NIST-800-53-CM-11(b)
    - NIST-800-53-CM-5(3)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SA-12
    - NIST-800-53-SA-12(10)
    - NIST-800-53-SC-12
    - NIST-800-53-SC-12(3)
    - NIST-800-53-SI-7
    - PCI-DSS-Req-6.2
    - enable_strategy
    - ensure_gpgcheck_never_disabled
    - high_severity
    - low_complexity
    - medium_disruption
    - no_reboot_needed

Ensure Red Hat GPG Key Installedrule

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.

Remediation 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 script:
- 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
    - NIST-800-171-3.4.8
    - NIST-800-53-CM-5(3)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SC-12
    - NIST-800-53-SC-12(3)
    - NIST-800-53-SI-7
    - PCI-DSS-Req-6.2
    - ensure_redhat_gpgkey_installed
    - high_severity
    - medium_complexity
    - medium_disruption
    - no_reboot_needed
    - restrict_strategy

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

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

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

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

Account and Access Controlgroup

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 Virtualization 4.

contains 33 rules

Warning Banners for System Accessesgroup

Each system should expose as little information about itself as possible.

System banners, which are typically displayed just before a login prompt, give out information about the service or the host's operating system. This might include the distribution name and the system kernel version, and the particular version of a network service. This information can assist intruders in gaining access to the system as it can reveal whether the system is running vulnerable software. Most network services can be configured to limit what information is displayed.

Many organizations implement security policies that require a system banner provide notice of the system's ownership, provide warning to unauthorized users, and remind authorized users of their consent to monitoring.

contains 1 rule

Modify the System Login Bannerrule

To configure the system login banner edit /etc/issue. Replace the default text with a message compliant with the local site policy or a legal disclaimer. The DoD required text is either:

You are accessing a U.S. Government (USG) Information System (IS) that is provided for USG-authorized use only. By using this IS (which includes any device attached to this IS), you consent to the following conditions:
-The USG routinely intercepts and monitors communications on this IS for purposes including, but not limited to, penetration testing, COMSEC monitoring, network operations and defense, personnel misconduct (PM), law enforcement (LE), and counterintelligence (CI) investigations.
-At any time, the USG may inspect and seize data stored on this IS.
-Communications using, or data stored on, this IS are not private, are subject to routine monitoring, interception, and search, and may be disclosed or used for any USG-authorized purpose.
-This IS includes security measures (e.g., authentication and access controls) to protect USG interests -- not for your personal benefit or privacy.
-Notwithstanding the above, using this IS does not constitute consent to PM, LE or CI investigative searching or monitoring of the content of privileged communications, or work product, related to personal representation or services by attorneys, psychotherapists, or clergy, and their assistants. Such communications and work product are private and confidential. See User Agreement for details.


OR:

I've read & consent to terms in IS user agreem't.

Rationale:

Display of a standardized and approved use notification before granting access to the operating system ensures privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance.

System use notifications are required only for access via login interfaces with human users and are not required when such human interfaces do not exist.

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

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


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

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

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

Protect Accounts by Restricting Password-Based Logingroup

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.

contains 9 rules
contains 1 rule

Set Password Expiration Parametersgroup

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

contains 2 rules

Verify Proper Storage and Existence of Password Hashesgroup

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.

contains 1 rule

Prevent Login to Accounts With Empty Passwordrule

If an account is configured for password authentication but does not have an assigned password, it may be possible to log into the account without authentication. Remove any instances of the nullok in /etc/pam.d/system-auth to prevent logins with empty passwords. 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.

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.
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.

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

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

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

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

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

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

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

- name: Informative message based on the authselect integrity check result
  ansible.builtin.assert:
    that:
      - result_authselect_check_cmd is success
    fail_msg:
      - authselect integrity check failed. Remediation aborted!
      - This remediation could not be applied because the authselect profile is not
        intact.
      - It is not recommended to manually edit the PAM files when authselect is available
      - In cases where the default authselect profile does not cover a specific demand,
        a custom authselect profile is recommended.
    success_msg:
      - authselect integrity check passed
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - CJIS-5.5.2
    - NIST-800-171-3.1.1
    - NIST-800-171-3.1.5
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(a)
    - NIST-800-53-IA-5(c)
    - PCI-DSS-Req-8.2.3
    - configure_strategy
    - high_severity
    - low_complexity
    - medium_disruption
    - no_empty_passwords
    - no_reboot_needed

- name: Ensure nullok property is absent by directly editing pam files
  replace:
    dest: '{{ item }}'
    regexp: nullok
  loop:
    - /etc/pam.d/system-auth
    - /etc/pam.d/password-auth
  when:
    - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    - not result_authselect_present.stat.exists
  tags:
    - CJIS-5.5.2
    - NIST-800-171-3.1.1
    - NIST-800-171-3.1.5
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(a)
    - NIST-800-53-IA-5(c)
    - PCI-DSS-Req-8.2.3
    - configure_strategy
    - high_severity
    - low_complexity
    - medium_disruption
    - no_empty_passwords
    - no_reboot_needed
Remediation script:
---
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

Restrict Root Loginsgroup

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.

contains 5 rules

Verify Only Root Has UID 0rule

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.

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

Direct root Logins Not Allowedrule

To further limit access to the root account, administrators can disable root logins at the console by editing the /etc/securetty file. This file lists all devices the root user is allowed to login to. If the file does not exist at all, the root user can login through any communication device on the system, whether via the console or via a raw network interface. This is dangerous as user can login to the system as root via Telnet, which sends the password in plain text over the network. By default, Red Hat Virtualization 4's /etc/securetty file only allows the root user to login at the console physically attached to the system. To prevent root from logging in, remove the contents of this file. To prevent direct root logins, remove the contents of this file by typing the following command:

$ sudo echo > /etc/securetty

Rationale:

Disabling direct root logins ensures proper accountability and multifactor authentication to privileged accounts. Users will first login, then escalate to privileged (root) access via su / sudo. This is required for FISMA Low and FISMA Moderate systems.

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

echo > /etc/securetty

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Direct root Logins Not Allowed
  copy:
    dest: /etc/securetty
    content: ''
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-171-3.1.1
    - NIST-800-171-3.1.6
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-2
    - low_complexity
    - low_disruption
    - medium_severity
    - no_direct_root_logins
    - no_reboot_needed
    - restrict_strategy

Ensure that System Accounts Are Lockedrule

Some accounts are not associated with a human user of the system, and exist to perform some administrative function. An attacker should not be able to log into these accounts.

System accounts are those user accounts with a user ID less than UID_MIN, where value of the UID_MIN directive is set in /etc/login.defs configuration file. In the default configuration UID_MIN is set to 500, thus system accounts are those user accounts with a user ID less than 500. If any system account SYSACCT (other than root) has an unlocked password, disable it with the command:

$ sudo passwd -l SYSACCT

Rationale:

Disabling authentication for default system accounts makes it more difficult for attackers to make use of them to compromise a system.false

Restrict Serial Port Root Loginsrule

To restrict root logins on serial ports, ensure lines of this form do not appear in /etc/securetty:

ttyS0
ttyS1

Rationale:

Preventing direct root login to serial port interfaces helps ensure accountability for actions taken on the systems using the root account.

Remediation script:
sed -i '/ttyS/d' /etc/securetty
Remediation script:
- name: Restrict Serial Port Root Logins
  lineinfile:
    dest: /etc/securetty
    regexp: ttyS[0-9]
    state: absent
  tags:
    - NIST-800-171-3.1.1
    - NIST-800-171-3.1.5
    - NIST-800-53-AC-6
    - NIST-800-53-CM-6(a)
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_serial_port_logins
    - restrict_strategy

Protect Physical Console Accessgroup

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.

contains 7 rules

Configure Screen Lockinggroup

When a user must temporarily leave an account logged-in, screen locking should be employed to prevent passersby from abusing the account. User education and training is particularly important for screen locking to be effective, and policies can be implemented to reinforce this.

Automatic screen locking is only meant as a safeguard for those cases where a user forgot to lock the screen.

contains 5 rules
contains 5 rules

Install the opensc Package For Multifactor Authenticationrule

The opensc package can be installed with the following command:

$ sudo yum install opensc

Rationale:

Using an authentication device, such as a CAC or token that is separate from the information system, ensures that even if the information system is compromised, that compromise will not affect credentials stored on the authentication device.

Multifactor solutions that require devices separate from information systems gaining access include, for example, hardware tokens providing time-based or challenge-response authenticators and smart cards such as the U.S. Government Personal Identity Verification card and the DoD Common Access Card.

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

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

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Ensure opensc is installed
  package:
    name: opensc
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-53-CM-6(a)
    - enable_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - package_opensc_installed
Remediation script:
include install_opensc

class install_opensc {
  package { 'opensc':
    ensure => 'installed',
  }
}
Remediation script:

package --add=opensc
Remediation script:

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

Install the pcsc-lite packagerule

The pcsc-lite package can be installed with the following command:

$ sudo yum install pcsc-lite

Rationale:

The pcsc-lite package must be installed if it is to be available for multifactor authentication using smartcards.

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

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

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Ensure pcsc-lite is installed
  package:
    name: pcsc-lite
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-53-CM-6(a)
    - enable_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - package_pcsc-lite_installed
Remediation script:
include install_pcsc-lite

class install_pcsc-lite {
  package { 'pcsc-lite':
    ensure => 'installed',
  }
}
Remediation script:

package --add=pcsc-lite
Remediation script:

[[packages]]
name = "pcsc-lite"
version = "*"

Enable the pcscd Servicerule

The pcscd service can be enabled with the following command:

$ sudo systemctl enable pcscd.service

Rationale:

Using an authentication device, such as a CAC or token that is separate from the information system, ensures that even if the information system is compromised, that compromise will not affect credentials stored on the authentication device.

Multifactor solutions that require devices separate from information systems gaining access include, for example, hardware tokens providing time-based or challenge-response authenticators and smart cards such as the U.S. Government Personal Identity Verification card and the DoD Common Access Card.

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

SYSTEMCTL_EXEC='/usr/bin/systemctl'
"$SYSTEMCTL_EXEC" unmask 'pcscd.service'
"$SYSTEMCTL_EXEC" start 'pcscd.service'
"$SYSTEMCTL_EXEC" enable 'pcscd.service'

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Enable service pcscd
  block:

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

    - name: Enable service pcscd
      service:
        name: pcscd
        enabled: 'yes'
        state: started
        masked: 'no'
      when:
        - '"pcsc-lite" in ansible_facts.packages'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-2(1)
    - NIST-800-53-IA-2(11)
    - NIST-800-53-IA-2(2)
    - NIST-800-53-IA-2(3)
    - NIST-800-53-IA-2(4)
    - NIST-800-53-IA-2(6)
    - NIST-800-53-IA-2(7)
    - enable_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - service_pcscd_enabled
Remediation script:
include enable_pcscd

class enable_pcscd {
  service {'pcscd':
    enable => true,
    ensure => 'running',
  }
}
Remediation script:

[customizations.services]
enabled = ["pcscd"]

Configure opensc Smart Card Driversrule

The OpenSC smart card tool can auto-detect smart card drivers; however, setting the smart card drivers in use by your organization helps to prevent users from using unauthorized smart cards. The default smart card driver for this profile is cac. To configure the OpenSC driver, edit the /etc/opensc.conf and add the following line into the file in the app default block, so it will look like:

app default {
   ...
   card_drivers = cac;
}

Rationale:

Smart card login provides two-factor authentication stronger than that provided by a username and password combination. Smart cards leverage PKI (public key infrastructure) in order to provide and verify credentials. Configuring the smart card driver in use by your organization helps to prevent users from using unauthorized smart cards.

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

var_smartcard_drivers='cac'


OPENSC_TOOL="/usr/bin/opensc-tool"

if [ -f "${OPENSC_TOOL}" ]; then
    ${OPENSC_TOOL} -S app:default:card_drivers:$var_smartcard_drivers
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: XCCDF Value var_smartcard_drivers # promote to variable
  set_fact:
    var_smartcard_drivers: !!str cac
  tags:
    - always

- name: Check existence of opensc conf
  stat:
    path: /etc/opensc-{{ ansible_architecture }}.conf
  register: opensc_conf_cd
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-2(1)
    - NIST-800-53-IA-2(11)
    - NIST-800-53-IA-2(2)
    - NIST-800-53-IA-2(3)
    - NIST-800-53-IA-2(4)
    - NIST-800-53-IA-2(6)
    - NIST-800-53-IA-2(7)
    - PCI-DSS-Req-8.3
    - configure_opensc_card_drivers
    - configure_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

- name: Configure smartcard driver block
  block:

    - name: Check if card_drivers is defined
      command: /usr/bin/opensc-tool -G app:default:card_drivers
      changed_when: false
      register: card_drivers

    - name: Configure opensc Smart Card Drivers
      command: |
        /usr/bin/opensc-tool -S app:default:card_drivers:{{ var_smartcard_drivers }}
      when:
        - card_drivers.stdout != var_smartcard_drivers
  when:
    - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    - opensc_conf_cd.stat.exists
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-2(1)
    - NIST-800-53-IA-2(11)
    - NIST-800-53-IA-2(2)
    - NIST-800-53-IA-2(3)
    - NIST-800-53-IA-2(4)
    - NIST-800-53-IA-2(6)
    - NIST-800-53-IA-2(7)
    - PCI-DSS-Req-8.3
    - configure_opensc_card_drivers
    - configure_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

Force opensc To Use Defined Smart Card Driverrule

The OpenSC smart card middleware can auto-detect smart card drivers; however by forcing the smart card driver in use by your organization, opensc will no longer autodetect or use other drivers unless specified. This helps to prevent users from using unauthorized smart cards. The default smart card driver for this profile is cac. To force the OpenSC driver, edit the /etc/opensc.conf. Look for a line similar to:

# force_card_driver = customcos;
and change it to:
force_card_driver = cac;

Rationale:

Smart card login provides two-factor authentication stronger than that provided by a username and password combination. Smart cards leverage PKI (public key infrastructure) in order to provide and verify credentials. Forcing the smart card driver in use by your organization helps to prevent users from using unauthorized smart cards.

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

var_smartcard_drivers='cac'


OPENSC_TOOL="/usr/bin/opensc-tool"

if [ -f "${OPENSC_TOOL}" ]; then
    ${OPENSC_TOOL} -S app:default:force_card_driver:$var_smartcard_drivers
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: XCCDF Value var_smartcard_drivers # promote to variable
  set_fact:
    var_smartcard_drivers: !!str cac
  tags:
    - always

- name: Check existence of opensc conf
  stat:
    path: /etc/opensc-{{ ansible_architecture }}.conf
  register: opensc_conf_fcd
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-2(1)
    - NIST-800-53-IA-2(11)
    - NIST-800-53-IA-2(2)
    - NIST-800-53-IA-2(3)
    - NIST-800-53-IA-2(4)
    - NIST-800-53-IA-2(6)
    - NIST-800-53-IA-2(7)
    - PCI-DSS-Req-8.3
    - configure_strategy
    - force_opensc_card_drivers
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

- name: Force smartcard driver block
  block:

    - name: Check if force_card_driver is defined
      command: /usr/bin/opensc-tool -G app:default:force_card_driver
      changed_when: false
      register: force_card_driver

    - name: Force opensc To Use Defined Smart Card Driver
      command: |
        /usr/bin/opensc-tool -S app:default:force_card_driver:{{ var_smartcard_drivers }}
      when:
        - force_card_driver.stdout != var_smartcard_drivers
  when:
    - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    - opensc_conf_fcd.stat.exists
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-2(1)
    - NIST-800-53-IA-2(11)
    - NIST-800-53-IA-2(2)
    - NIST-800-53-IA-2(3)
    - NIST-800-53-IA-2(4)
    - NIST-800-53-IA-2(6)
    - NIST-800-53-IA-2(7)
    - PCI-DSS-Req-8.3
    - configure_strategy
    - force_opensc_card_drivers
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

Verify that Interactive Boot is Disabledrule

Red Hat Virtualization 4 systems support an "interactive boot" option that can be used to prevent services from being started. On a Red Hat Virtualization 4 system, interactive boot can be enabled by providing a 1, yes, true, or on value to the systemd.confirm_spawn kernel argument in /etc/default/grub. Remove any instance of

systemd.confirm_spawn=(1|yes|true|on)
from the kernel arguments in that file to disable interactive boot. It is also required to change the runtime configuration, run:
/sbin/grubby --update-kernel=ALL --remove-args="systemd.confirm_spawn"

Rationale:

Using interactive boot, the console user could disable auditing, firewalls, or other services, weakening system security.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q grub2-common; then

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

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

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.2
    - NIST-800-171-3.4.5
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SC-2(1)
    - grub2_disable_interactive_boot
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

- name: Verify that Interactive Boot is Disabled (runtime)
  command: /sbin/grubby --update-kernel=ALL --remove-args="systemd.confirm_spawn"
  when: '"grub2-common" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.2
    - NIST-800-171-3.4.5
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SC-2(1)
    - grub2_disable_interactive_boot
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Require Authentication for Single User Moderule

Single-user mode is intended as a system recovery method, providing a single user root access to the system by providing a boot option at startup. By default, no authentication is performed if single-user mode is selected.

By default, single-user mode is protected by requiring a password and is set in /usr/lib/systemd/system/rescue.service.

Rationale:

This prevents attackers with physical access from trivially bypassing security on the machine and gaining root access. Such accesses are further prevented by configuring the bootloader password.

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

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

sulogin='/bin/sh -c "/sbin/sulogin; /usr/bin/systemctl --fail --no-block default"'

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 script:
- name: require single user mode password
  lineinfile:
    create: true
    dest: /usr/lib/systemd/system/rescue.service
    regexp: ^#?ExecStart=
    line: ExecStart=-/bin/sh -c "/sbin/sulogin; /usr/bin/systemctl --fail --no-block
      default"
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - 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

Protect Accounts by Configuring PAMgroup

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 http://www.linux-pam.org/Linux-PAM-html/sag-configuration-file.html.
contains 16 rules

Set Lockouts for Failed Password Attemptsgroup

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.
contains 5 rules

Limit Password Reuserule

Do not allow users to reuse recent passwords. This can be accomplished by using the remember option for the pam_unix or pam_pwhistory PAM modules.

In the file /etc/pam.d/system-auth, append remember=5 to the line which refers to the pam_unix.so or pam_pwhistory.somodule, as shown below:

  • for the pam_unix.so case:
    password sufficient pam_unix.so ...existing_options... remember=5
  • for the pam_pwhistory.so case:
    password requisite pam_pwhistory.so ...existing_options... remember=5
The DoD STIG requirement is 5 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.
Rationale:

Preventing re-use of previous passwords helps ensure that a compromised password is not re-used by a user.

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

var_password_pam_unix_remember='5'


if [ -f /usr/bin/authselect ]; then
    if authselect check; then
        CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
        # Standard profiles delivered with authselect should not be modified.
        # 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"
            # Ensure a backup before changing the profile
            authselect apply-changes -b --backup=before-pwhistory-hardening.backup
            authselect select $CURRENT_PROFILE
            for feature in $ENABLED_FEATURES; do
                authselect enable-feature $feature;
            done
        fi
        # Include the desired configuration in the custom profile
        CUSTOM_SYSTEM_AUTH="/etc/authselect/$CURRENT_PROFILE/system-auth"
        CUSTOM_PASSWORD_AUTH="/etc/authselect/$CURRENT_PROFILE/password-auth"
        for custom_pam_file in $CUSTOM_SYSTEM_AUTH $CUSTOM_PASSWORD_AUTH; do
            if ! $(grep -q "^[^#].*pam_pwhistory.so.*remember=" $custom_pam_file); then
                sed -i --follow-symlinks "/^password.*requisite.*pam_pwquality.so/a password    requisite     pam_pwhistory.so remember=$var_password_pam_unix_remember use_authtok" $custom_pam_file
            else
                sed -i --follow-symlinks "s/\(.*pam_pwhistory.so.*remember=\)[[:digit:]]\+\s\(.*\)/\1$var_password_pam_unix_remember \2/g" $custom_pam_file
            fi
        done
        authselect apply-changes -b --backup=after-pwhistory-hardening.backup
    else
        echo "
authselect integrity check failed. Remediation aborted!
This remediation could not be applied because the authselect profile is not intact.
It is not recommended to manually edit the PAM files when authselect is available
In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
        false
    fi
else
    
    AUTH_FILES[0]="/etc/pam.d/system-auth"
    
    AUTH_FILES[1]="/etc/pam.d/password-auth"

    for pamFile in "${AUTH_FILES[@]}"; do
        if grep -q "pam_unix.so.*" $pamFile; then
            if [ -e "$pamFile" ] ; then
    valueRegex="$var_password_pam_unix_remember" defaultValue="$var_password_pam_unix_remember"
    # non-empty values need to be preceded by an equals sign
    [ -n "${valueRegex}" ] && valueRegex="=${valueRegex}"
    # add an equals sign to non-empty values
    [ -n "${defaultValue}" ] && defaultValue="=${defaultValue}"

    # fix the value for 'option' if one exists but does not match 'valueRegex'
    if grep -q -P "^\\s*password\\s+sufficient\\s+pam_unix.so(\\s.+)?\\s+remember(?"'!'"${valueRegex}(\\s|\$))" < "$pamFile" ; then
        sed --follow-symlinks -i -E -e "s/^(\\s*password\\s+sufficient\\s+pam_unix.so(\\s.+)?\\s)remember=[^[:space:]]*/\\1remember${defaultValue}/" "$pamFile"

    # add 'option=default' if option is not set
    elif grep -q -E "^\\s*password\\s+sufficient\\s+pam_unix.so" < "$pamFile" &&
            grep    -E "^\\s*password\\s+sufficient\\s+pam_unix.so" < "$pamFile" | grep -q -E -v "\\sremember(=|\\s|\$)" ; then

        sed --follow-symlinks -i -E -e "s/^(\\s*password\\s+sufficient\\s+pam_unix.so[^\\n]*)/\\1 remember${defaultValue}/" "$pamFile"
    # add a new entry if none exists
    elif ! grep -q -P "^\\s*password\\s+sufficient\\s+pam_unix.so(\\s.+)?\\s+remember${valueRegex}(\\s|\$)" < "$pamFile" ; then
        echo "password sufficient pam_unix.so remember${defaultValue}" >> "$pamFile"
    fi
else
    echo "$pamFile doesn't exist" >&2
fi
        fi
        if grep -q "pam_pwhistory.so.*" $pamFile; then
            if [ -e "$pamFile" ] ; then
    valueRegex="$var_password_pam_unix_remember" defaultValue="$var_password_pam_unix_remember"
    # non-empty values need to be preceded by an equals sign
    [ -n "${valueRegex}" ] && valueRegex="=${valueRegex}"
    # add an equals sign to non-empty values
    [ -n "${defaultValue}" ] && defaultValue="=${defaultValue}"

    # fix the value for 'option' if one exists but does not match 'valueRegex'
    if grep -q -P "^\\s*password\\s+required\\s+pam_pwhistory.so(\\s.+)?\\s+remember(?"'!'"${valueRegex}(\\s|\$))" < "$pamFile" ; then
        sed --follow-symlinks -i -E -e "s/^(\\s*password\\s+required\\s+pam_pwhistory.so(\\s.+)?\\s)remember=[^[:space:]]*/\\1remember${defaultValue}/" "$pamFile"

    # add 'option=default' if option is not set
    elif grep -q -E "^\\s*password\\s+required\\s+pam_pwhistory.so" < "$pamFile" &&
            grep    -E "^\\s*password\\s+required\\s+pam_pwhistory.so" < "$pamFile" | grep -q -E -v "\\sremember(=|\\s|\$)" ; then

        sed --follow-symlinks -i -E -e "s/^(\\s*password\\s+required\\s+pam_pwhistory.so[^\\n]*)/\\1 remember${defaultValue}/" "$pamFile"
    # add a new entry if none exists
    elif ! grep -q -P "^\\s*password\\s+required\\s+pam_pwhistory.so(\\s.+)?\\s+remember${valueRegex}(\\s|\$)" < "$pamFile" ; then
        echo "password required pam_pwhistory.so remember${defaultValue}" >> "$pamFile"
    fi
else
    echo "$pamFile doesn't exist" >&2
fi
        fi
    done
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed
- name: XCCDF Value var_password_pam_unix_remember # promote to variable
  set_fact:
    var_password_pam_unix_remember: !!str 5
  tags:
    - always

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

- name: Check the integrity of the current authselect profile
  ansible.builtin.command:
    cmd: authselect check
  register: result_authselect_check_cmd
  changed_when: false
  ignore_errors: true
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present.stat.exists
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: Informative message based on the authselect integrity check result
  ansible.builtin.assert:
    that:
      - result_authselect_check_cmd is success
    fail_msg:
      - authselect integrity check failed. Remediation aborted!
      - This remediation could not be applied because the authselect profile is not
        intact.
      - It is not recommended to manually edit the PAM files when authselect is available
      - In cases where the default authselect profile does not cover a specific demand,
        a custom authselect profile is recommended.
    success_msg:
      - authselect integrity check passed
  when: '"pam" in ansible_facts.packages'
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: Get authselect current profile
  ansible.builtin.shell:
    cmd: authselect current -r | awk '{ print $1 }'
  register: result_authselect_profile
  changed_when: false
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present.stat.exists
    - result_authselect_check_cmd is success
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: 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:
    - '"pam" in ansible_facts.packages'
    - result_authselect_profile is not skipped
    - result_authselect_profile.stdout is match("custom/")
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: 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:
    - '"pam" in ansible_facts.packages'
    - result_authselect_profile is not skipped
    - result_authselect_profile.stdout is not match("custom/")
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: 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:
    - '"pam" in ansible_facts.packages'
    - result_authselect_profile is not skipped
    - authselect_current_profile is not match("custom/")
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: Check if any custom profile with the same name was already created in the
    past
  ansible.builtin.stat:
    path: /etc/authselect/{{ authselect_custom_profile }}
  register: result_authselect_custom_profile_present
  changed_when: false
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present.stat.exists
    - authselect_current_profile is not match("custom/")
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: Create a custom profile based on the current profile
  ansible.builtin.command:
    cmd: authselect create-profile hardening -b sssd
  register: result_authselect_create_profile
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present.stat.exists
    - result_authselect_check_cmd is success
    - authselect_current_profile is not match("custom/")
    - not result_authselect_custom_profile_present.stat.exists
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: Ensure the desired configuration is updated in the custom profile
  ansible.builtin.replace:
    dest: '{{ item }}'
    regexp: (.*pam_pwhistory.so.*remember=)(\S+)(.*)$
    replace: \g<1>{{ var_password_pam_unix_remember }}\g<3>
  loop:
    - /etc/authselect/{{ authselect_custom_profile }}/system-auth
    - /etc/authselect/{{ authselect_custom_profile }}/password-auth
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_profile is not skipped
    - authselect_custom_profile is match("custom/")
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: Ensure the desired configuration in present in the custom profile
  ansible.builtin.lineinfile:
    dest: '{{ item }}'
    insertafter: ^password.*requisite.*pam_pwquality.so.*
    line: password    requisite     pam_pwhistory.so remember={{ var_password_pam_unix_remember
      }} use_authtok
  loop:
    - /etc/authselect/{{ authselect_custom_profile }}/system-auth
    - /etc/authselect/{{ authselect_custom_profile }}/password-auth
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_profile is not skipped
    - authselect_custom_profile is match("custom/")
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: Ensure a backup of current authselect profile before select the custom profile
  ansible.builtin.command:
    cmd: authselect apply-changes -b --backup=before-pwhistory-hardening.backup
  register: result_authselect_backup
  when:
    - '"pam" in ansible_facts.packages'
    - 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)
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: Ensure the custom profile is selected
  ansible.builtin.command:
    cmd: authselect select {{ authselect_custom_profile }} --force
  register: result_pam_authselect_select_profile
  when:
    - '"pam" in ansible_facts.packages'
    - 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)
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: Restore the authselect features in the custom profile
  ansible.builtin.command:
    cmd: authselect enable-feature {{ item }}
  register: result_pam_authselect_select_features
  loop: '{{ result_authselect_features.stdout_lines }}'
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_profile is not skipped
    - result_authselect_features is not skipped
    - result_pam_authselect_select_profile is not skipped
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: Ensure the custom profile changes are applied
  ansible.builtin.command:
    cmd: authselect apply-changes -b --backup=after-pwhistory-hardening.backup
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_check_cmd is success
    - result_authselect_profile is not skipped
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: Do not allow users to reuse recent passwords - system-auth (change)
  replace:
    dest: /etc/pam.d/system-auth
    regexp: ^(password\s+sufficient\s+pam_unix\.so\s.*remember\s*=\s*)(\S+)(.*)$
    replace: \g<1>{{ var_password_pam_unix_remember }}\g<3>
  when:
    - '"pam" in ansible_facts.packages'
    - not result_authselect_present.stat.exists
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: Do not allow users to reuse recent passwords - system-auth (add)
  replace:
    dest: /etc/pam.d/system-auth
    regexp: ^password\s+sufficient\s+pam_unix\.so\s(?!.*remember\s*=\s*).*$
    replace: \g<0> remember={{ var_password_pam_unix_remember }}
  when:
    - '"pam" in ansible_facts.packages'
    - not result_authselect_present.stat.exists
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: Do not allow users to reuse recent passwords - system-auth (change)
  replace:
    dest: /etc/pam.d/system-auth
    regexp: ^(password\s+(?:(?:requisite)|(?:required))\s+pam_pwhistory\.so\s.*remember\s*=\s*)(\S+)(.*)$
    replace: \g<1>{{ var_password_pam_unix_remember }}\g<3>
  when:
    - '"pam" in ansible_facts.packages'
    - not result_authselect_present.stat.exists
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

- name: Do not allow users to reuse recent passwords - system-auth (add)
  replace:
    dest: /etc/pam.d/system-auth
    regexp: ^password\s+(?:(?:requisite)|(?:required))\s+pam_pwhistory\.so\s(?!.*remember\s*=\s*).*$
    replace: \g<0> remember={{ var_password_pam_unix_remember }}
  when:
    - '"pam" in ansible_facts.packages'
    - not result_authselect_present.stat.exists
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-171-3.5.8
    - NIST-800-53-IA-5(1)(e)
    - NIST-800-53-IA-5(f)
    - PCI-DSS-Req-8.2.5
    - accounts_password_pam_unix_remember
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed

Lock Accounts After Failed Password Attemptsrule

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. 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.

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:

Locking out user accounts after a number of incorrect attempts prevents direct password guessing attacks. In combination with the silent option, user enumeration attacks are also mitigated.

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

var_accounts_passwords_pam_faillock_deny='3'


if [ -f /usr/sbin/authconfig ]; then
    authconfig --enablefaillock --update
elif [ -f /usr/bin/authselect ]; then
    if authselect check; then
        authselect enable-feature with-faillock
        authselect apply-changes
    else
        echo "
authselect integrity check failed. Remediation aborted!
This remediation could not be applied because the authselect profile is not intact.
It is not recommended to manually edit the PAM files when authselect is available
In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
        false
    fi
fi

FAILLOCK_CONF="/etc/security/faillock.conf"
if [ -f $FAILLOCK_CONF ]; then
    if $(grep -q '^\s*deny\s*=' $FAILLOCK_CONF); then
        sed -i --follow-symlinks "s/^\s*\(deny\s*\)=.*$/\1 = $var_accounts_passwords_pam_faillock_deny/g" $FAILLOCK_CONF
    else
        echo "deny = $var_accounts_passwords_pam_faillock_deny" >> $FAILLOCK_CONF
    fi
else
    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")

for pam_file in "${AUTH_FILES[@]}"
do
    # is auth required pam_faillock.so preauth present?
    if grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+preauth.*$' "$pam_file" ; then
        # is the option set?
        if grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+preauth.*'"deny"'=([0-9]*).*$' "$pam_file" ; then
            # just change the value of option to a correct value
            sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock.so.*preauth.*silent.*\)\('"deny"' *= *\).*/\1\2'"$var_accounts_passwords_pam_faillock_deny"'/' "$pam_file"
        # the option is not set.
        else
            # append the option
            sed -i --follow-symlinks '/^auth.*required.*pam_faillock.so.*preauth.*silent.*/ s/$/ '"deny"'='"$var_accounts_passwords_pam_faillock_deny"'/' "$pam_file"
        fi
    # auth required pam_faillock.so preauth is not present, insert the whole line
    else
        sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix.so.*/i auth        required      pam_faillock.so preauth silent '"deny"'='"$var_accounts_passwords_pam_faillock_deny" "$pam_file"
    fi
    # is auth default pam_faillock.so authfail present?
    if grep -qE '^\s*auth\s+(\[default=die\])\s+pam_faillock\.so\s+authfail.*$' "$pam_file" ; then
        # is the option set?
        if grep -qE '^\s*auth\s+(\[default=die\])\s+pam_faillock\.so\s+authfail.*'"deny"'=([0-9]*).*$' "$pam_file" ; then
            # just change the value of option to a correct value
            sed -i --follow-symlinks 's/\(^auth.*[default=die].*pam_faillock.so.*authfail.*\)\('"deny"' *= *\).*/\1\2'"$var_accounts_passwords_pam_faillock_deny"'/' "$pam_file"
        # the option is not set.
        else
            # append the option
            sed -i --follow-symlinks '/^auth.*[default=die].*pam_faillock.so.*authfail.*/ s/$/ '"deny"'='"$var_accounts_passwords_pam_faillock_deny"'/' "$pam_file"
        fi
    # auth default pam_faillock.so authfail is not present, insert the whole line
    else
        sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix.so.*/a auth        [default=die] pam_faillock.so authfail '"deny"'='"$var_accounts_passwords_pam_faillock_deny" "$pam_file"
    fi
    if ! grep -qE '^\s*account\s+required\s+pam_faillock\.so.*$' "$pam_file" ; then
        sed -E -i --follow-symlinks '/^\s*account\s*required\s*pam_unix.so/i account     required      pam_faillock.so' "$pam_file"
    fi
done
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(a)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.6
    - 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: Check if system relies on authconfig
  ansible.builtin.stat:
    path: /usr/sbin/authconfig
  register: result_authconfig_check
  when: '"pam" in ansible_facts.packages'
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(a)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.6
    - accounts_passwords_pam_faillock_deny
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Ensure pam_faillock.so is properly enabled using authconfig tool
  ansible.builtin.command:
    cmd: authconfig --enablefaillock --update
  when:
    - '"pam" in ansible_facts.packages'
    - result_authconfig_check.stat.exists
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(a)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.6
    - accounts_passwords_pam_faillock_deny
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Check if system relies on authselect
  ansible.builtin.stat:
    path: /usr/bin/authselect
  register: result_authselect_present
  when:
    - '"pam" in ansible_facts.packages'
    - not result_authconfig_check.stat.exists
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(a)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.6
    - accounts_passwords_pam_faillock_deny
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Check integrity of authselect current profile
  ansible.builtin.command:
    cmd: authselect check
  register: result_authselect_check_cmd
  changed_when: false
  ignore_errors: true
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present is not skipped
    - result_authselect_present.stat.exists
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(a)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.6
    - accounts_passwords_pam_faillock_deny
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Informative message based on the authselect integrity check result
  ansible.builtin.assert:
    that:
      - result_authselect_check_cmd is success
    fail_msg:
      - authselect integrity check failed. Remediation aborted!
      - This remediation could not be applied because the authselect profile is not
        intact.
      - It is not recommended to manually edit the PAM files when authselect is available
      - In cases where the default authselect profile does not cover a specific demand,
        a custom authselect profile is recommended.
    success_msg:
      - authselect integrity check passed
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present is not skipped
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(a)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.6
    - accounts_passwords_pam_faillock_deny
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Get authselect current features
  ansible.builtin.shell:
    cmd: authselect current | tail -n+3 | awk '{ print $2 }'
  register: result_authselect_features
  changed_when: false
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present is not skipped
    - result_authselect_present.stat.exists
    - result_authselect_check_cmd is success
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(a)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.6
    - accounts_passwords_pam_faillock_deny
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Ensure with-faillock feature is enabled via authselect tool
  ansible.builtin.command:
    cmd: authselect enable-feature with-faillock
  register: result_authselect_cmd
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present is not skipped
    - result_authselect_present.stat.exists
    - result_authselect_check_cmd is success
    - result_authselect_features.stdout is not search("with-faillock")
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(a)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.6
    - accounts_passwords_pam_faillock_deny
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: 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
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(a)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.6
    - accounts_passwords_pam_faillock_deny
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: 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
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(a)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.6
    - accounts_passwords_pam_faillock_deny
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Ensure the 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-5]+(.*)
    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:
    - '"pam" in ansible_facts.packages'
    - not result_faillock_conf_check.stat.exists
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(a)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.6
    - accounts_passwords_pam_faillock_deny
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Ensure the 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-5]+(.*)
    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:
    - '"pam" in ansible_facts.packages'
    - not result_faillock_conf_check.stat.exists
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(a)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.6
    - accounts_passwords_pam_faillock_deny
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Configure the root Account for Failed Password Attemptsrule

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 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.

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.

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

if [ -f /usr/sbin/authconfig ]; then
    authconfig --enablefaillock --update
elif [ -f /usr/bin/authselect ]; then
    if authselect check; then
        authselect enable-feature with-faillock
        authselect apply-changes
    else
        echo "
authselect integrity check failed. Remediation aborted!
This remediation could not be applied because the authselect profile is not intact.
It is not recommended to manually edit the PAM files when authselect is available
In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
        false
    fi
fi

FAILLOCK_CONF="/etc/security/faillock.conf"
if [ -f $FAILLOCK_CONF ]; then
    if [ ! $(grep -q '^\s*even_deny_root' $FAILLOCK_CONF) ]; then
        echo "even_deny_root" >> $FAILLOCK_CONF
    fi
else
    SYSTEM_AUTH="/etc/pam.d/system-auth"
    PASSWORD_AUTH="/etc/pam.d/password-auth"
    for file in $SYSTEM_AUTH $PASSWORD_AUTH; do
        if ! grep -q "^auth.*pam_faillock.so \(preauth silent\|authfail\).*even_deny_root" $file; then
			sed -i --follow-symlinks 's/\(pam_faillock.so \(preauth silent\|authfail\).*\)$/\1 even_deny_root/g' $file
		fi
    done
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - 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: Check if system relies on authconfig
  ansible.builtin.stat:
    path: /usr/sbin/authconfig
  register: result_authconfig_check
  when: '"pam" in ansible_facts.packages'
  tags:
    - 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: Ensure pam_faillock.so is properly enabled using authconfig tool
  ansible.builtin.command:
    cmd: authconfig --enablefaillock --update
  when:
    - '"pam" in ansible_facts.packages'
    - result_authconfig_check.stat.exists
  tags:
    - 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: Check if system relies on authselect
  ansible.builtin.stat:
    path: /usr/bin/authselect
  register: result_authselect_present
  when:
    - '"pam" in ansible_facts.packages'
    - not result_authconfig_check.stat.exists
  tags:
    - 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: Check integrity of authselect current profile
  ansible.builtin.command:
    cmd: authselect check
  register: result_authselect_check_cmd
  changed_when: false
  ignore_errors: true
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present is not skipped
    - result_authselect_present.stat.exists
  tags:
    - 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: Informative message based on the authselect integrity check result
  ansible.builtin.assert:
    that:
      - result_authselect_check_cmd is success
    fail_msg:
      - authselect integrity check failed. Remediation aborted!
      - This remediation could not be applied because the authselect profile is not
        intact.
      - It is not recommended to manually edit the PAM files when authselect is available
      - In cases where the default authselect profile does not cover a specific demand,
        a custom authselect profile is recommended.
    success_msg:
      - authselect integrity check passed
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present is not skipped
  tags:
    - 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: Get authselect current features
  ansible.builtin.shell:
    cmd: authselect current | tail -n+3 | awk '{ print $2 }'
  register: result_authselect_features
  changed_when: false
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present is not skipped
    - result_authselect_present.stat.exists
    - result_authselect_check_cmd is success
  tags:
    - 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: Ensure with-faillock feature is enabled via authselect tool
  ansible.builtin.command:
    cmd: authselect enable-feature with-faillock
  register: result_authselect_cmd
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present is not skipped
    - result_authselect_present.stat.exists
    - result_authselect_check_cmd is success
    - result_authselect_features.stdout is not search("with-faillock")
  tags:
    - 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: 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:
    - 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: 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:
    - 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: 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
  when:
    - '"pam" in ansible_facts.packages'
    - not result_faillock_conf_check.stat.exists
  tags:
    - 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: Ensure the 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:
    - '"pam" in ansible_facts.packages'
    - not result_faillock_conf_check.stat.exists
    - result_pam_faillock_even_deny_root.found == 0
  tags:
    - 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: Ensure the 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:
    - '"pam" in ansible_facts.packages'
    - not result_faillock_conf_check.stat.exists
    - result_pam_faillock_even_deny_root.found == 0
  tags:
    - 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

Set Interval For Counting Failed Password Attemptsrule

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. First make sure the feature is enabled using the following command:

  • authselect enable-feature with-faillock

Then edit the /etc/security/faillock.conf file as follows:

  • add, uncomment or edit the following line:
    fail_interval = 900
  • add or uncomment the following line:
    silent

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.

Remediation 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/sbin/authconfig ]; then
    authconfig --enablefaillock --update
elif [ -f /usr/bin/authselect ]; then
    if authselect check; then
        authselect enable-feature with-faillock
        authselect apply-changes
    else
        echo "
authselect integrity check failed. Remediation aborted!
This remediation could not be applied because the authselect profile is not intact.
It is not recommended to manually edit the PAM files when authselect is available
In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
        false
    fi
fi

FAILLOCK_CONF="/etc/security/faillock.conf"
if [ -f $FAILLOCK_CONF ]; then
    if $(grep -q '^\s*fail_interval\s*=' $FAILLOCK_CONF); then
        sed -i --follow-symlinks "s/^\s*\(fail_interval\s*\)=.*$/\1 = $var_accounts_passwords_pam_faillock_fail_interval/g" $FAILLOCK_CONF
    else
        echo "fail_interval = $var_accounts_passwords_pam_faillock_fail_interval" >> $FAILLOCK_CONF
    fi
else
    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")

for pam_file in "${AUTH_FILES[@]}"
do
    # is auth required pam_faillock.so preauth present?
    if grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+preauth.*$' "$pam_file" ; then
        # is the option set?
        if grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+preauth.*'"fail_interval"'=([0-9]*).*$' "$pam_file" ; then
            # just change the value of option to a correct value
            sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock.so.*preauth.*silent.*\)\('"fail_interval"' *= *\).*/\1\2'"$var_accounts_passwords_pam_faillock_fail_interval"'/' "$pam_file"
        # the option is not set.
        else
            # append the option
            sed -i --follow-symlinks '/^auth.*required.*pam_faillock.so.*preauth.*silent.*/ s/$/ '"fail_interval"'='"$var_accounts_passwords_pam_faillock_fail_interval"'/' "$pam_file"
        fi
    # auth required pam_faillock.so preauth is not present, insert the whole line
    else
        sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix.so.*/i auth        required      pam_faillock.so preauth silent '"fail_interval"'='"$var_accounts_passwords_pam_faillock_fail_interval" "$pam_file"
    fi
    # is auth default pam_faillock.so authfail present?
    if grep -qE '^\s*auth\s+(\[default=die\])\s+pam_faillock\.so\s+authfail.*$' "$pam_file" ; then
        # is the option set?
        if grep -qE '^\s*auth\s+(\[default=die\])\s+pam_faillock\.so\s+authfail.*'"fail_interval"'=([0-9]*).*$' "$pam_file" ; then
            # just change the value of option to a correct value
            sed -i --follow-symlinks 's/\(^auth.*[default=die].*pam_faillock.so.*authfail.*\)\('"fail_interval"' *= *\).*/\1\2'"$var_accounts_passwords_pam_faillock_fail_interval"'/' "$pam_file"
        # the option is not set.
        else
            # append the option
            sed -i --follow-symlinks '/^auth.*[default=die].*pam_faillock.so.*authfail.*/ s/$/ '"fail_interval"'='"$var_accounts_passwords_pam_faillock_fail_interval"'/' "$pam_file"
        fi
    # auth default pam_faillock.so authfail is not present, insert the whole line
    else
        sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix.so.*/a auth        [default=die] pam_faillock.so authfail '"fail_interval"'='"$var_accounts_passwords_pam_faillock_fail_interval" "$pam_file"
    fi
    if ! grep -qE '^\s*account\s+required\s+pam_faillock\.so.*$' "$pam_file" ; then
        sed -E -i --follow-symlinks '/^\s*account\s*required\s*pam_unix.so/i account     required      pam_faillock.so' "$pam_file"
    fi
done
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - 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: Check if system relies on authconfig
  ansible.builtin.stat:
    path: /usr/sbin/authconfig
  register: result_authconfig_check
  when: '"pam" in ansible_facts.packages'
  tags:
    - 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: Ensure pam_faillock.so is properly enabled using authconfig tool
  ansible.builtin.command:
    cmd: authconfig --enablefaillock --update
  when:
    - '"pam" in ansible_facts.packages'
    - result_authconfig_check.stat.exists
  tags:
    - 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: Check if system relies on authselect
  ansible.builtin.stat:
    path: /usr/bin/authselect
  register: result_authselect_present
  when:
    - '"pam" in ansible_facts.packages'
    - not result_authconfig_check.stat.exists
  tags:
    - 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: Check integrity of authselect current profile
  ansible.builtin.command:
    cmd: authselect check
  register: result_authselect_check_cmd
  changed_when: false
  ignore_errors: true
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present is not skipped
    - result_authselect_present.stat.exists
  tags:
    - 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: Informative message based on the authselect integrity check result
  ansible.builtin.assert:
    that:
      - result_authselect_check_cmd is success
    fail_msg:
      - authselect integrity check failed. Remediation aborted!
      - This remediation could not be applied because the authselect profile is not
        intact.
      - It is not recommended to manually edit the PAM files when authselect is available
      - In cases where the default authselect profile does not cover a specific demand,
        a custom authselect profile is recommended.
    success_msg:
      - authselect integrity check passed
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present is not skipped
  tags:
    - 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: Get authselect current features
  ansible.builtin.shell:
    cmd: authselect current | tail -n+3 | awk '{ print $2 }'
  register: result_authselect_features
  changed_when: false
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present is not skipped
    - result_authselect_present.stat.exists
    - result_authselect_check_cmd is success
  tags:
    - 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: Ensure with-faillock feature is enabled via authselect tool
  ansible.builtin.command:
    cmd: authselect enable-feature with-faillock
  register: result_authselect_cmd
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present is not skipped
    - result_authselect_present.stat.exists
    - result_authselect_check_cmd is success
    - result_authselect_features.stdout is not search("with-faillock")
  tags:
    - 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: 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:
    - 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: 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:
    - 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: 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
  when:
    - '"pam" in ansible_facts.packages'
    - not result_faillock_conf_check.stat.exists
  tags:
    - 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: 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:
    - '"pam" in ansible_facts.packages'
    - not result_faillock_conf_check.stat.exists
    - result_pam_faillock_fail_interval.found == 0
  tags:
    - 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: 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:
    - '"pam" in ansible_facts.packages'
    - not result_faillock_conf_check.stat.exists
    - result_pam_faillock_fail_interval.found == 0
  tags:
    - 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: 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-5]+(.*)
    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:
    - '"pam" in ansible_facts.packages'
    - not result_faillock_conf_check.stat.exists
    - result_pam_faillock_fail_interval.found > 0
  tags:
    - 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: 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-5]+(.*)
    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:
    - '"pam" in ansible_facts.packages'
    - not result_faillock_conf_check.stat.exists
    - result_pam_faillock_fail_interval.found > 0
  tags:
    - 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

Set Lockout Time for Failed Password Attemptsrule

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. 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.

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:

Locking out user accounts after a number of incorrect attempts prevents direct password guessing attacks. Ensuring that an administrator is involved in unlocking locked accounts draws appropriate attention to such situations.

Remediation 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/sbin/authconfig ]; then
    authconfig --enablefaillock --update
elif [ -f /usr/bin/authselect ]; then
    if authselect check; then
        authselect enable-feature with-faillock
        authselect apply-changes
    else
        echo "
authselect integrity check failed. Remediation aborted!
This remediation could not be applied because the authselect profile is not intact.
It is not recommended to manually edit the PAM files when authselect is available
In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
        false
    fi
fi

FAILLOCK_CONF="/etc/security/faillock.conf"
if [ -f $FAILLOCK_CONF ]; then
    if $(grep -q '^\s*unlock_time\s*=' $FAILLOCK_CONF); then
        sed -i --follow-symlinks "s/^\s*\(unlock_time\s*\)=.*$/\1 = $var_accounts_passwords_pam_faillock_unlock_time/g" $FAILLOCK_CONF
    else
        echo "unlock_time = $var_accounts_passwords_pam_faillock_unlock_time" >> $FAILLOCK_CONF
    fi
else
    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")

for pam_file in "${AUTH_FILES[@]}"
do
    # is auth required pam_faillock.so preauth present?
    if grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+preauth.*$' "$pam_file" ; then
        # is the option set?
        if grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+preauth.*'"unlock_time"'=([0-9]*).*$' "$pam_file" ; then
            # just change the value of option to a correct value
            sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock.so.*preauth.*silent.*\)\('"unlock_time"' *= *\).*/\1\2'"$var_accounts_passwords_pam_faillock_unlock_time"'/' "$pam_file"
        # the option is not set.
        else
            # append the option
            sed -i --follow-symlinks '/^auth.*required.*pam_faillock.so.*preauth.*silent.*/ s/$/ '"unlock_time"'='"$var_accounts_passwords_pam_faillock_unlock_time"'/' "$pam_file"
        fi
    # auth required pam_faillock.so preauth is not present, insert the whole line
    else
        sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix.so.*/i auth        required      pam_faillock.so preauth silent '"unlock_time"'='"$var_accounts_passwords_pam_faillock_unlock_time" "$pam_file"
    fi
    # is auth default pam_faillock.so authfail present?
    if grep -qE '^\s*auth\s+(\[default=die\])\s+pam_faillock\.so\s+authfail.*$' "$pam_file" ; then
        # is the option set?
        if grep -qE '^\s*auth\s+(\[default=die\])\s+pam_faillock\.so\s+authfail.*'"unlock_time"'=([0-9]*).*$' "$pam_file" ; then
            # just change the value of option to a correct value
            sed -i --follow-symlinks 's/\(^auth.*[default=die].*pam_faillock.so.*authfail.*\)\('"unlock_time"' *= *\).*/\1\2'"$var_accounts_passwords_pam_faillock_unlock_time"'/' "$pam_file"
        # the option is not set.
        else
            # append the option
            sed -i --follow-symlinks '/^auth.*[default=die].*pam_faillock.so.*authfail.*/ s/$/ '"unlock_time"'='"$var_accounts_passwords_pam_faillock_unlock_time"'/' "$pam_file"
        fi
    # auth default pam_faillock.so authfail is not present, insert the whole line
    else
        sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix.so.*/a auth        [default=die] pam_faillock.so authfail '"unlock_time"'='"$var_accounts_passwords_pam_faillock_unlock_time" "$pam_file"
    fi
    if ! grep -qE '^\s*account\s+required\s+pam_faillock\.so.*$' "$pam_file" ; then
        sed -E -i --follow-symlinks '/^\s*account\s*required\s*pam_unix.so/i account     required      pam_faillock.so' "$pam_file"
    fi
done
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(b)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.7
    - 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: Check if system relies on authconfig
  ansible.builtin.stat:
    path: /usr/sbin/authconfig
  register: result_authconfig_check
  when: '"pam" in ansible_facts.packages'
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(b)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.7
    - accounts_passwords_pam_faillock_unlock_time
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Ensure pam_faillock.so is properly enabled using authconfig tool
  ansible.builtin.command:
    cmd: authconfig --enablefaillock --update
  when:
    - '"pam" in ansible_facts.packages'
    - result_authconfig_check.stat.exists
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(b)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.7
    - accounts_passwords_pam_faillock_unlock_time
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Check if system relies on authselect
  ansible.builtin.stat:
    path: /usr/bin/authselect
  register: result_authselect_present
  when:
    - '"pam" in ansible_facts.packages'
    - not result_authconfig_check.stat.exists
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(b)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.7
    - accounts_passwords_pam_faillock_unlock_time
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Check integrity of authselect current profile
  ansible.builtin.command:
    cmd: authselect check
  register: result_authselect_check_cmd
  changed_when: false
  ignore_errors: true
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present is not skipped
    - result_authselect_present.stat.exists
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(b)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.7
    - accounts_passwords_pam_faillock_unlock_time
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Informative message based on the authselect integrity check result
  ansible.builtin.assert:
    that:
      - result_authselect_check_cmd is success
    fail_msg:
      - authselect integrity check failed. Remediation aborted!
      - This remediation could not be applied because the authselect profile is not
        intact.
      - It is not recommended to manually edit the PAM files when authselect is available
      - In cases where the default authselect profile does not cover a specific demand,
        a custom authselect profile is recommended.
    success_msg:
      - authselect integrity check passed
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present is not skipped
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(b)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.7
    - accounts_passwords_pam_faillock_unlock_time
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Get authselect current features
  ansible.builtin.shell:
    cmd: authselect current | tail -n+3 | awk '{ print $2 }'
  register: result_authselect_features
  changed_when: false
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present is not skipped
    - result_authselect_present.stat.exists
    - result_authselect_check_cmd is success
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(b)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.7
    - accounts_passwords_pam_faillock_unlock_time
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Ensure with-faillock feature is enabled via authselect tool
  ansible.builtin.command:
    cmd: authselect enable-feature with-faillock
  register: result_authselect_cmd
  when:
    - '"pam" in ansible_facts.packages'
    - result_authselect_present is not skipped
    - result_authselect_present.stat.exists
    - result_authselect_check_cmd is success
    - result_authselect_features.stdout is not search("with-faillock")
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(b)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.7
    - accounts_passwords_pam_faillock_unlock_time
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: 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
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(b)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.7
    - accounts_passwords_pam_faillock_unlock_time
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: 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
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(b)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.7
    - accounts_passwords_pam_faillock_unlock_time
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Ensure the 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-5]+(.*)
    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:
    - '"pam" in ansible_facts.packages'
    - not result_faillock_conf_check.stat.exists
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(b)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.7
    - accounts_passwords_pam_faillock_unlock_time
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Ensure the 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-5]+(.*)
    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:
    - '"pam" in ansible_facts.packages'
    - not result_faillock_conf_check.stat.exists
  tags:
    - CJIS-5.5.3
    - NIST-800-171-3.1.8
    - NIST-800-53-AC-7(b)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-8.1.7
    - accounts_passwords_pam_faillock_unlock_time
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Set Password Quality Requirementsgroup

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.

contains 8 rules

Set Password Quality Requirements with pam_pwqualitygroup

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.

contains 8 rules

Ensure PAM Enforces Password Requirements - Minimum Digit Charactersrule

The pam_pwquality module's dcredit parameter controls requirements for usage of digits in a password. When set to a negative number, any password will be required to contain that many digits. When set to a positive number, pam_pwquality will grant +1 additional length credit for each digit. Modify the dcredit setting in /etc/security/pwquality.conf to require the use of a digit in passwords.

Rationale:

Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks.

Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. Requiring digits makes password guessing attacks more difficult by ensuring a larger search space.

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

var_password_pam_dcredit='-1'


# Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
# Otherwise, regular sed command will do.
sed_command=('sed' '-i')
if test -L "/etc/security/pwquality.conf"; then
    sed_command+=('--follow-symlinks')
fi

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

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

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^dcredit\\>" "/etc/security/pwquality.conf"; then
    "${sed_command[@]}" "s/^dcredit\\>.*/$formatted_output/gi" "/etc/security/pwquality.conf"
else
    # \n is precaution for case where file ends without trailing newline
    
    printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(a)
    - NIST-800-53-IA-5(4)
    - NIST-800-53-IA-5(c)
    - PCI-DSS-Req-8.2.3
    - accounts_password_pam_dcredit
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
- name: XCCDF Value var_password_pam_dcredit # promote to variable
  set_fact:
    var_password_pam_dcredit: !!str -1
  tags:
    - always

- name: Ensure PAM variable dcredit is set accordingly
  lineinfile:
    create: true
    dest: /etc/security/pwquality.conf
    regexp: ^#?\s*dcredit
    line: dcredit = {{ var_password_pam_dcredit }}
  when: '"pam" in ansible_facts.packages'
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(a)
    - NIST-800-53-IA-5(4)
    - NIST-800-53-IA-5(c)
    - PCI-DSS-Req-8.2.3
    - accounts_password_pam_dcredit
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure PAM Enforces Password Requirements - Minimum Different Charactersrule

The pam_pwquality module's difok parameter sets the number of characters in a password that must not be present in and old password during a password change.

Modify the difok setting in /etc/security/pwquality.conf to equal 8 to require differing characters when changing passwords.

Rationale:

Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute–force attacks.

Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised.

Requiring a minimum number of different characters during password changes ensures that newly changed passwords should not resemble previously compromised ones. Note that passwords which are changed on compromised systems will still be compromised, however.

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

var_password_pam_difok='8'


# Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
# Otherwise, regular sed command will do.
sed_command=('sed' '-i')
if test -L "/etc/security/pwquality.conf"; then
    sed_command+=('--follow-symlinks')
fi

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

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

# 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 "^difok\\>" "/etc/security/pwquality.conf"; then
    "${sed_command[@]}" "s/^difok\\>.*/$formatted_output/gi" "/etc/security/pwquality.conf"
else
    # \n is precaution for case where file ends without trailing newline
    
    printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(b)
    - NIST-800-53-IA-5(4)
    - NIST-800-53-IA-5(c)
    - accounts_password_pam_difok
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
- name: XCCDF Value var_password_pam_difok # promote to variable
  set_fact:
    var_password_pam_difok: !!str 8
  tags:
    - always

- name: Ensure PAM variable difok is set accordingly
  lineinfile:
    create: true
    dest: /etc/security/pwquality.conf
    regexp: ^#?\s*difok
    line: difok = {{ var_password_pam_difok }}
  when: '"pam" in ansible_facts.packages'
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(b)
    - NIST-800-53-IA-5(4)
    - NIST-800-53-IA-5(c)
    - accounts_password_pam_difok
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure PAM Enforces Password Requirements - Minimum Lowercase Charactersrule

The pam_pwquality module's lcredit parameter controls requirements for usage of lowercase letters in a password. When set to a negative number, any password will be required to contain that many lowercase characters. When set to a positive number, pam_pwquality will grant +1 additional length credit for each lowercase character. Modify the lcredit setting in /etc/security/pwquality.conf to require the use of a lowercase character in passwords.

Rationale:

Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks.

Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possble combinations that need to be tested before the password is compromised. Requiring a minimum number of lowercase characters makes password guessing attacks more difficult by ensuring a larger search space.

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

var_password_pam_lcredit='-1'


# Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
# Otherwise, regular sed command will do.
sed_command=('sed' '-i')
if test -L "/etc/security/pwquality.conf"; then
    sed_command+=('--follow-symlinks')
fi

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

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

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^lcredit\\>" "/etc/security/pwquality.conf"; then
    "${sed_command[@]}" "s/^lcredit\\>.*/$formatted_output/gi" "/etc/security/pwquality.conf"
else
    # \n is precaution for case where file ends without trailing newline
    
    printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(a)
    - NIST-800-53-IA-5(4)
    - NIST-800-53-IA-5(c)
    - PCI-DSS-Req-8.2.3
    - accounts_password_pam_lcredit
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
- name: XCCDF Value var_password_pam_lcredit # promote to variable
  set_fact:
    var_password_pam_lcredit: !!str -1
  tags:
    - always

- name: Ensure PAM variable lcredit is set accordingly
  lineinfile:
    create: true
    dest: /etc/security/pwquality.conf
    regexp: ^#?\s*lcredit
    line: lcredit = {{ var_password_pam_lcredit }}
  when: '"pam" in ansible_facts.packages'
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(a)
    - NIST-800-53-IA-5(4)
    - NIST-800-53-IA-5(c)
    - PCI-DSS-Req-8.2.3
    - accounts_password_pam_lcredit
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating Characters from Same Character Classrule

The pam_pwquality module's maxclassrepeat parameter controls requirements for consecutive repeating characters from the same character class. When set to a positive number, it will reject passwords which contain more than that number of consecutive characters from the same character class. Modify the maxclassrepeat setting in /etc/security/pwquality.conf to equal 4 to prevent a run of (4 + 1) or more identical characters.

Rationale:

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

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

var_password_pam_maxclassrepeat='4'


# Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
# Otherwise, regular sed command will do.
sed_command=('sed' '-i')
if test -L "/etc/security/pwquality.conf"; then
    sed_command+=('--follow-symlinks')
fi

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

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

# 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 "^maxclassrepeat\\>" "/etc/security/pwquality.conf"; then
    "${sed_command[@]}" "s/^maxclassrepeat\\>.*/$formatted_output/gi" "/etc/security/pwquality.conf"
else
    # \n is precaution for case where file ends without trailing newline
    
    printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(a)
    - NIST-800-53-IA-5(4)
    - NIST-800-53-IA-5(c)
    - accounts_password_pam_maxclassrepeat
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
- name: XCCDF Value var_password_pam_maxclassrepeat # promote to variable
  set_fact:
    var_password_pam_maxclassrepeat: !!str 4
  tags:
    - always

- name: Ensure PAM variable maxclassrepeat is set accordingly
  lineinfile:
    create: true
    dest: /etc/security/pwquality.conf
    regexp: ^#?\s*maxclassrepeat
    line: maxclassrepeat = {{ var_password_pam_maxclassrepeat }}
  when: '"pam" in ansible_facts.packages'
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(a)
    - NIST-800-53-IA-5(4)
    - NIST-800-53-IA-5(c)
    - accounts_password_pam_maxclassrepeat
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Set Password Maximum Consecutive Repeating Charactersrule

The pam_pwquality module's maxrepeat parameter controls requirements for consecutive repeating characters. When set to a positive number, it will reject passwords which contain more than that number of consecutive characters. Modify the maxrepeat setting in /etc/security/pwquality.conf to equal 3 to prevent a run of (3 + 1) or more identical characters.

Rationale:

Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks.

Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised.

Passwords with excessive repeating characters may be more vulnerable to password-guessing attacks.

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

var_password_pam_maxrepeat='3'


# Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
# Otherwise, regular sed command will do.
sed_command=('sed' '-i')
if test -L "/etc/security/pwquality.conf"; then
    sed_command+=('--follow-symlinks')
fi

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

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

# 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 "^maxrepeat\\>" "/etc/security/pwquality.conf"; then
    "${sed_command[@]}" "s/^maxrepeat\\>.*/$formatted_output/gi" "/etc/security/pwquality.conf"
else
    # \n is precaution for case where file ends without trailing newline
    
    printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(4)
    - NIST-800-53-IA-5(c)
    - accounts_password_pam_maxrepeat
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
- name: XCCDF Value var_password_pam_maxrepeat # promote to variable
  set_fact:
    var_password_pam_maxrepeat: !!str 3
  tags:
    - always

- name: Ensure PAM variable maxrepeat is set accordingly
  lineinfile:
    create: true
    dest: /etc/security/pwquality.conf
    regexp: ^#?\s*maxrepeat
    line: maxrepeat = {{ var_password_pam_maxrepeat }}
  when: '"pam" in ansible_facts.packages'
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(4)
    - NIST-800-53-IA-5(c)
    - accounts_password_pam_maxrepeat
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure PAM Enforces Password Requirements - Minimum Lengthrule

The pam_pwquality module's minlen parameter controls requirements for minimum characters required in a password. Add minlen=15 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 compromose the password.

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

var_password_pam_minlen='15'


# Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
# Otherwise, regular sed command will do.
sed_command=('sed' '-i')
if test -L "/etc/security/pwquality.conf"; then
    sed_command+=('--follow-symlinks')
fi

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^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
    "${sed_command[@]}" "s/^minlen\\>.*/$formatted_output/gi" "/etc/security/pwquality.conf"
else
    # \n is precaution for case where file ends without trailing newline
    
    printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - CJIS-5.6.2.1.1
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(a)
    - NIST-800-53-IA-5(4)
    - NIST-800-53-IA-5(c)
    - PCI-DSS-Req-8.2.3
    - accounts_password_pam_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 15
  tags:
    - always

- name: Ensure PAM variable minlen is set accordingly
  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
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(a)
    - NIST-800-53-IA-5(4)
    - NIST-800-53-IA-5(c)
    - PCI-DSS-Req-8.2.3
    - accounts_password_pam_minlen
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure PAM Enforces Password Requirements - Minimum Special Charactersrule

The pam_pwquality module's ocredit= parameter controls requirements for usage of special (or "other") characters in a password. When set to a negative number, any password will be required to contain that many special characters. When set to a positive number, pam_pwquality will grant +1 additional length credit for each special character. Modify the ocredit setting in /etc/security/pwquality.conf to equal -1 to require use of a special character in passwords.

Rationale:

Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks.

Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possble combinations that need to be tested before the password is compromised. Requiring a minimum number of special characters makes password guessing attacks more difficult by ensuring a larger search space.

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

var_password_pam_ocredit='-1'


# Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
# Otherwise, regular sed command will do.
sed_command=('sed' '-i')
if test -L "/etc/security/pwquality.conf"; then
    sed_command+=('--follow-symlinks')
fi

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

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

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^ocredit\\>" "/etc/security/pwquality.conf"; then
    "${sed_command[@]}" "s/^ocredit\\>.*/$formatted_output/gi" "/etc/security/pwquality.conf"
else
    # \n is precaution for case where file ends without trailing newline
    
    printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(a)
    - NIST-800-53-IA-5(4)
    - NIST-800-53-IA-5(c)
    - accounts_password_pam_ocredit
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
- name: XCCDF Value var_password_pam_ocredit # promote to variable
  set_fact:
    var_password_pam_ocredit: !!str -1
  tags:
    - always

- name: Ensure PAM variable ocredit is set accordingly
  lineinfile:
    create: true
    dest: /etc/security/pwquality.conf
    regexp: ^#?\s*ocredit
    line: ocredit = {{ var_password_pam_ocredit }}
  when: '"pam" in ansible_facts.packages'
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(a)
    - NIST-800-53-IA-5(4)
    - NIST-800-53-IA-5(c)
    - accounts_password_pam_ocredit
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure PAM Enforces Password Requirements - Minimum Uppercase Charactersrule

The pam_pwquality module's ucredit= parameter controls requirements for usage of uppercase letters in a password. When set to a negative number, any password will be required to contain that many uppercase characters. When set to a positive number, pam_pwquality will grant +1 additional length credit for each uppercase character. Modify the ucredit setting in /etc/security/pwquality.conf to require the use of an uppercase character in passwords.

Rationale:

Use of a complex password helps to increase the time and resources reuiqred to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks.

Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised.

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

var_password_pam_ucredit='-1'


# Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
# Otherwise, regular sed command will do.
sed_command=('sed' '-i')
if test -L "/etc/security/pwquality.conf"; then
    sed_command+=('--follow-symlinks')
fi

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

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

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^ucredit\\>" "/etc/security/pwquality.conf"; then
    "${sed_command[@]}" "s/^ucredit\\>.*/$formatted_output/gi" "/etc/security/pwquality.conf"
else
    # \n is precaution for case where file ends without trailing newline
    
    printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(a)
    - NIST-800-53-IA-5(4)
    - NIST-800-53-IA-5(c)
    - PCI-DSS-Req-8.2.3
    - accounts_password_pam_ucredit
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
- name: XCCDF Value var_password_pam_ucredit # promote to variable
  set_fact:
    var_password_pam_ucredit: !!str -1
  tags:
    - always

- name: Ensure PAM variable ucredit is set accordingly
  lineinfile:
    create: true
    dest: /etc/security/pwquality.conf
    regexp: ^#?\s*ucredit
    line: ucredit = {{ var_password_pam_ucredit }}
  when: '"pam" in ansible_facts.packages'
  tags:
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(a)
    - NIST-800-53-IA-5(4)
    - NIST-800-53-IA-5(c)
    - PCI-DSS-Req-8.2.3
    - accounts_password_pam_ucredit
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Set Password Hashing Algorithmgroup

The system's default algorithm for storing password hashes in /etc/shadow is SHA-512. This can be configured in several locations.

contains 3 rules

Set Password Hashing Algorithm in /etc/libuser.confrule

In /etc/libuser.conf, add or correct the following line in its [defaults] section to ensure the system will use the SHA-512 algorithm for password hashing:

crypt_style = sha512

Rationale:

Passwords need to be protected at all times, and encryption is the standard method for protecting passwords. If passwords are not encrypted, they can be plainly read (i.e., clear text) and easily compromised. Passwords that are encrypted with a weak algorithm are no more protected than if they are kepy in plain text.

This setting ensures user and group account administration utilities are configured to store only encrypted representations of passwords. Additionally, the crypt_style configuration option ensures the use of a strong hashing algorithm that makes password cracking attacks more difficult.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q libuser; then

LIBUSER_CONF="/etc/libuser.conf"
CRYPT_STYLE_REGEX='[[:space:]]*\[defaults](.*(\n)+)+?[[:space:]]*crypt_style[[:space:]]*'

# Try find crypt_style in [defaults] section. If it is here, then change algorithm to sha512.
# If it isn't here, then add it to [defaults] section.
if grep -qzosP $CRYPT_STYLE_REGEX $LIBUSER_CONF ; then
        sed -i "s/\(crypt_style[[:space:]]*=[[:space:]]*\).*/\1sha512/g" $LIBUSER_CONF
elif grep -qs "\[defaults]" $LIBUSER_CONF ; then
        sed -i "/[[:space:]]*\[defaults]/a crypt_style = sha512" $LIBUSER_CONF
else
        echo -e "[defaults]\ncrypt_style = sha512" >> $LIBUSER_CONF
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - CJIS-5.6.2.2
    - NIST-800-171-3.13.11
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(c)
    - NIST-800-53-IA-5(c)
    - PCI-DSS-Req-8.2.1
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
    - set_password_hashing_algorithm_libuserconf

- name: Set Password Hashing Algorithm in /etc/libuser.conf
  lineinfile:
    dest: /etc/libuser.conf
    insertafter: ^\s*\[defaults]
    regexp: ^#?crypt_style
    line: crypt_style = sha512
    state: present
    create: true
  when: '"libuser" in ansible_facts.packages'
  tags:
    - CJIS-5.6.2.2
    - NIST-800-171-3.13.11
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(c)
    - NIST-800-53-IA-5(c)
    - PCI-DSS-Req-8.2.1
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
    - set_password_hashing_algorithm_libuserconf

Set Password Hashing Algorithm in /etc/login.defsrule

In /etc/login.defs, add or correct the following line to ensure the system will use SHA-512 as the hashing algorithm:

ENCRYPT_METHOD SHA512

Rationale:

Passwords need to be protected at all times, and encryption is the standard method for protecting passwords. If passwords are not encrypted, they can be plainly read (i.e., clear text) and easily compromised. Passwords that are encrypted with a weak algorithm are no more protected than if they are kept in plain text.

Using a stronger hashing algorithm makes password cracking attacks more difficult.

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

var_password_hashing_algorithm='SHA512'


if grep --silent ^ENCRYPT_METHOD /etc/login.defs ; then
	sed -i "s/^ENCRYPT_METHOD .*/ENCRYPT_METHOD $var_password_hashing_algorithm/g" /etc/login.defs
else
	echo "" >> /etc/login.defs
	echo "ENCRYPT_METHOD $var_password_hashing_algorithm" >> /etc/login.defs
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - CJIS-5.6.2.2
    - NIST-800-171-3.13.11
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(c)
    - NIST-800-53-IA-5(c)
    - PCI-DSS-Req-8.2.1
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
    - set_password_hashing_algorithm_logindefs
- name: XCCDF Value var_password_hashing_algorithm # promote to variable
  set_fact:
    var_password_hashing_algorithm: !!str SHA512
  tags:
    - always

- name: Set Password Hashing Algorithm in /etc/login.defs
  lineinfile:
    dest: /etc/login.defs
    regexp: ^#?ENCRYPT_METHOD
    line: ENCRYPT_METHOD {{ var_password_hashing_algorithm }}
    state: present
    create: true
  when: '"shadow-utils" in ansible_facts.packages'
  tags:
    - CJIS-5.6.2.2
    - NIST-800-171-3.13.11
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IA-5(1)(c)
    - NIST-800-53-IA-5(c)
    - PCI-DSS-Req-8.2.1
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
    - set_password_hashing_algorithm_logindefs

Set PAM's Password Hashing Algorithmrule

The PAM system service can be configured to only store encrypted representations of passwords. In /etc/pam.d/system-auth, the password section of the file controls which PAM modules execute during a password change. Set the pam_unix.so module in the password section to include the argument sha512, as shown below:

password    sufficient    pam_unix.so sha512 other arguments...

This will help ensure when local users change their passwords, hashes for the new passwords will be generated using the SHA-512 algorithm. This is the default.

Rationale:

Passwords need to be protected at all times, and encryption is the standard method for protecting passwords. If passwords are not encrypted, they can be plainly read (i.e., clear text) and easily compromised. Passwords that are encrypted with a weak algorithm are no more protected than if they are kepy in plain text.

This setting ensures user and group account administration utilities are configured to store only encrypted representations of passwords. Additionally, the crypt_style configuration option ensures the use of a strong hashing algorithm that makes password cracking attacks more difficult.

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

AUTH_FILES[0]="/etc/pam.d/system-auth"
AUTH_FILES[1]="/etc/pam.d/password-auth"

for pamFile in "${AUTH_FILES[@]}"
do
	if ! grep -q "^password.*sufficient.*pam_unix.so.*sha512" $pamFile; then
		sed -i --follow-symlinks "/^password.*sufficient.*pam_unix.so/ s/$/ sha512/" $pamFile
	fi
done

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

System Accounting with auditdgroup

The audit service provides substantial capabilities for recording system activities. By default, the service audits about SELinux AVC denials and certain types of security-relevant events such as system logins, account modifications, and authentication events performed by programs such as sudo. Under its default configuration, auditd has modest disk space requirements, and should not noticeably impact system performance.

NOTE: The Linux Audit daemon auditd can be configured to use the augenrules program to read audit rules files (*.rules) located in /etc/audit/rules.d location and compile them to create the resulting form of the /etc/audit/audit.rules configuration file during the daemon startup (default configuration). Alternatively, the auditd daemon can use the auditctl utility to read audit rules from the /etc/audit/audit.rules configuration file during daemon startup, and load them into the kernel. The expected behavior is configured via the appropriate ExecStartPost directive setting in the /usr/lib/systemd/system/auditd.service configuration file. To instruct the auditd daemon to use the augenrules program to read audit rules (default configuration), use the following setting:

ExecStartPost=-/sbin/augenrules --load
in the /usr/lib/systemd/system/auditd.service configuration file. In order to instruct the auditd daemon to use the auditctl utility to read audit rules, use the following setting:
ExecStartPost=-/sbin/auditctl -R /etc/audit/audit.rules
in the /usr/lib/systemd/system/auditd.service configuration file. Refer to [Service] section of the /usr/lib/systemd/system/auditd.service configuration file for further details.

Government networks often have substantial auditing requirements and auditd can be configured to meet these requirements. Examining some example audit records demonstrates how the Linux audit system satisfies common requirements. The following example from Fedora Documentation available at https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html-single/selinux_users_and_administrators_guide/index#sect-Security-Enhanced_Linux-Fixing_Problems-Raw_Audit_Messages shows the substantial amount of information captured in a two typical "raw" audit messages, followed by a breakdown of the most important fields. In this example the message is SELinux-related and reports an AVC denial (and the associated system call) that occurred when the Apache HTTP Server attempted to access the /var/www/html/file1 file (labeled with the samba_share_t type):
type=AVC msg=audit(1226874073.147:96): avc:  denied  { getattr } for pid=2465 comm="httpd"
path="/var/www/html/file1" dev=dm-0 ino=284133 scontext=unconfined_u:system_r:httpd_t:s0
tcontext=unconfined_u:object_r:samba_share_t:s0 tclass=file

type=SYSCALL msg=audit(1226874073.147:96): arch=40000003 syscall=196 success=no exit=-13
a0=b98df198 a1=bfec85dc a2=54dff4 a3=2008171 items=0 ppid=2463 pid=2465 auid=502 uid=48
gid=48 euid=48 suid=48 fsuid=48 egid=48 sgid=48 fsgid=48 tty=(none) ses=6 comm="httpd"
exe="/usr/sbin/httpd" subj=unconfined_u:system_r:httpd_t:s0 key=(null)
  • msg=audit(1226874073.147:96)
    • The number in parentheses is the unformatted time stamp (Epoch time) for the event, which can be converted to standard time by using the date command.
  • { getattr }
    • The item in braces indicates the permission that was denied. getattr indicates the source process was trying to read the target file's status information. This occurs before reading files. This action is denied due to the file being accessed having the wrong label. Commonly seen permissions include getattr, read, and write.
  • comm="httpd"
    • The executable that launched the process. The full path of the executable is found in the exe= section of the system call (SYSCALL) message, which in this case, is exe="/usr/sbin/httpd".
  • path="/var/www/html/file1"
    • The path to the object (target) the process attempted to access.
  • scontext="unconfined_u:system_r:httpd_t:s0"
    • The SELinux context of the process that attempted the denied action. In this case, it is the SELinux context of the Apache HTTP Server, which is running in the httpd_t domain.
  • tcontext="unconfined_u:object_r:samba_share_t:s0"
    • The SELinux context of the object (target) the process attempted to access. In this case, it is the SELinux context of file1. Note: the samba_share_t type is not accessible to processes running in the httpd_t domain.
  • From the system call (SYSCALL) message, two items are of interest:
    • success=no: indicates whether the denial (AVC) was enforced or not. success=no indicates the system call was not successful (SELinux denied access). success=yes indicates the system call was successful - this can be seen for permissive domains or unconfined domains, such as initrc_t and kernel_t.
    • exe="/usr/sbin/httpd": the full path to the executable that launched the process, which in this case, is exe="/usr/sbin/httpd".

contains 63 rules

Configure auditd Rules for Comprehensive Auditinggroup

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

contains 61 rules

Record Events that Modify the System's Discretionary Access Controlsgroup

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

contains 13 rules

Record Events that Modify the System's Discretionary Access Controls - chmodrule

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.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="chmod"
	KEY="perm_mod"
	SYSCALL_GROUPING="chmod fchmod fchmodat"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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.5.5
    - audit_rules_dac_modification_chmod
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit chmod tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_chmod
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of chmod in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of chmod in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_chmod
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of chmod in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of chmod in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_chmod
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify the System's Discretionary Access Controls - chownrule

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.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="chown"
	KEY="perm_mod"
	SYSCALL_GROUPING="chown fchown fchownat lchown"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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.5.5
    - audit_rules_dac_modification_chown
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit chown tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_chown
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of chown in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of chown in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_chown
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of chown in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of chown in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_chown
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify the System's Discretionary Access Controls - fchmodrule

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

-a always,exit -F arch=b32 -S fchmod -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fchmod -F auid>=1000 -F auid!=unset -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S fchmod -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fchmod -F auid>=1000 -F auid!=unset -F key=perm_mod

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

The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="fchmod"
	KEY="perm_mod"
	SYSCALL_GROUPING="chmod fchmod fchmodat"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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.5.5
    - audit_rules_dac_modification_fchmod
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit fchmod tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fchmod
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of fchmod in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of fchmod in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fchmod
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of fchmod in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of fchmod in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fchmod
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify the System's Discretionary Access Controls - fchmodatrule

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

-a always,exit -F arch=b32 -S fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fchmodat -F auid>=1000 -F auid!=unset -F key=perm_mod

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

The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="fchmodat"
	KEY="perm_mod"
	SYSCALL_GROUPING="chmod fchmod fchmodat"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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.5.5
    - audit_rules_dac_modification_fchmodat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit fchmodat tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fchmodat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of fchmodat in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of fchmodat in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fchmodat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of fchmodat in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of fchmodat in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fchmodat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify the System's Discretionary Access Controls - fchownrule

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

-a always,exit -F arch=b32 -S fchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S fchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fchown -F auid>=1000 -F auid!=unset -F key=perm_mod

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

The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="fchown"
	KEY="perm_mod"
	SYSCALL_GROUPING="chown fchown fchownat lchown"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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.5.5
    - audit_rules_dac_modification_fchown
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit fchown tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fchown
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of fchown in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of fchown in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fchown
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of fchown in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of fchown in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fchown
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify the System's Discretionary Access Controls - fchownatrule

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

-a always,exit -F arch=b32 -S fchownat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fchownat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S fchownat -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fchownat -F auid>=1000 -F auid!=unset -F key=perm_mod

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

The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="fchownat"
	KEY="perm_mod"
	SYSCALL_GROUPING="chown fchown fchownat lchown"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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.5.5
    - audit_rules_dac_modification_fchownat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit fchownat tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fchownat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of fchownat in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of fchownat in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fchownat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of fchownat in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of fchownat in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fchownat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify the System's Discretionary Access Controls - fremovexattrrule

At a minimum, the audit system should collect file permission changes for all users and root.

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:

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


If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod


If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod


If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod

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

The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="fremovexattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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.5.5
    - audit_rules_dac_modification_fremovexattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit fremovexattr tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fremovexattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of fremovexattr in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of fremovexattr in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fremovexattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of fremovexattr in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of fremovexattr in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fremovexattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify the System's Discretionary Access Controls - fsetxattrrule

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

-a always,exit -F arch=b32 -S fsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S fsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S fsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod

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

The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="fsetxattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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.5.5
    - audit_rules_dac_modification_fsetxattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit fsetxattr tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fsetxattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of fsetxattr in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of fsetxattr in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fsetxattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of fsetxattr in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of fsetxattr in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_fsetxattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify the System's Discretionary Access Controls - lchownrule

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

-a always,exit -F arch=b32 -S lchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S lchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S lchown -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S lchown -F auid>=1000 -F auid!=unset -F key=perm_mod

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

The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="lchown"
	KEY="perm_mod"
	SYSCALL_GROUPING="chown fchown fchownat lchown"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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.5.5
    - audit_rules_dac_modification_lchown
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit lchown tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_lchown
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of lchown in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of lchown in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_lchown
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of lchown in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of lchown in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_lchown
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify the System's Discretionary Access Controls - lremovexattrrule

At a minimum, the audit system should collect file permission changes for all users and root.

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:

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


If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S lremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod


If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S lremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod


If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S lremovexattr -F auid>=1000 -F auid!=unset -F key=perm_mod

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

The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="lremovexattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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.5.5
    - audit_rules_dac_modification_lremovexattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit lremovexattr tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_lremovexattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of lremovexattr in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of lremovexattr in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_lremovexattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of lremovexattr in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of lremovexattr in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_lremovexattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify the System's Discretionary Access Controls - lsetxattrrule

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

-a always,exit -F arch=b32 -S lsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S lsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S lsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S lsetxattr -F auid>=1000 -F auid!=unset -F key=perm_mod

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

The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="lsetxattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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.5.5
    - audit_rules_dac_modification_lsetxattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit lsetxattr tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_lsetxattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of lsetxattr in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of lsetxattr in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_lsetxattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of lsetxattr in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of lsetxattr in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_lsetxattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify the System's Discretionary Access Controls - removexattrrule

At a minimum, the audit system should collect file permission changes for all users and root.

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:

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


If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S removexattr -F auid>=1000 -F auid!=unset -F key=perm_mod


If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S removexattr -F auid>=1000 -F auid!=unset -F key=perm_mod


If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S removexattr -F auid>=1000 -F auid!=unset -F key=perm_mod

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

The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="removexattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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.5.5
    - audit_rules_dac_modification_removexattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit removexattr tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_removexattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of removexattr in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of removexattr in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_removexattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of removexattr in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of removexattr in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_removexattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify the System's Discretionary Access Controls - setxattrrule

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

-a always,exit -F arch=b32 -S setxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S setxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S setxattr -F auid>=1000 -F auid!=unset -F key=perm_mod
If the system is 64 bit then also add the following line:
-a always,exit -F arch=b64 -S setxattr -F auid>=1000 -F auid!=unset -F key=perm_mod

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

The changing of file permissions could indicate that a user is attempting to gain access to information that would otherwise be disallowed. Auditing DAC modifications can facilitate the identification of patterns of abuse among both authorized and unauthorized users.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="setxattr"
	KEY="perm_mod"
	SYSCALL_GROUPING="fremovexattr lremovexattr removexattr fsetxattr lsetxattr setxattr"

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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.5.5
    - audit_rules_dac_modification_setxattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit setxattr tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_setxattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of setxattr in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of setxattr in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_setxattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of setxattr in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of setxattr in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.5.5
    - audit_rules_dac_modification_setxattr
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Execution Attempts to Run SELinux Privileged Commandsgroup

At a minimum, the audit system should collect the execution of SELinux privileged commands for all users and root.

contains 4 rules

Record Any Attempts to Run chconrule

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 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 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.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/chcon"
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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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_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 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 auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_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 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 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 auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_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 auid>=1000
          -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_execution_chcon
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Record Any Attempts to Run semanagerule

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 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 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.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/semanage"
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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - 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 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 auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_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 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 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 auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_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 auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-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

Record Any Attempts to Run setfilesrule

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 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 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.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/setfiles"
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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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_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 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 auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_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 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 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 auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_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 auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_execution_setfiles
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Record Any Attempts to Run setseboolrule

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 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 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.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/setsebool"
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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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_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 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 auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_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 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 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 auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_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 auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_execution_setsebool
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Record File Deletion Events by Usergroup

At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 or b64 as appropriate for your system:

-a always,exit -F arch=ARCH -S rmdir,unlink,unlinkat,rename,renameat -F auid>=1000 -F auid!=unset -F key=delete
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S rmdir,unlink,unlinkat,rename,renameat -F auid>=1000 -F auid!=unset -F key=delete

contains 5 rules

Ensure auditd Collects File Deletion Events by User - renamerule

At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 or b64 as appropriate for your system:

-a always,exit -F arch=ARCH -S rename -F auid>=1000 -F auid!=unset -F key=delete
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S rename -F auid>=1000 -F auid!=unset -F key=delete

Rationale:

Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="rename"
	KEY="delete"
	SYSCALL_GROUPING="unlink unlinkat rename renameat rmdir"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_file_deletion_events_rename
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit rename tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_file_deletion_events_rename
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of rename in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules
      set_fact: audit_file="/etc/audit/rules.d/delete.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of rename in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=delete
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_file_deletion_events_rename
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of rename in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules
      set_fact: audit_file="/etc/audit/rules.d/delete.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of rename in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=delete
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_file_deletion_events_rename
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Ensure auditd Collects File Deletion Events by User - renameatrule

At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 or b64 as appropriate for your system:

-a always,exit -F arch=ARCH -S renameat -F auid>=1000 -F auid!=unset -F key=delete
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S renameat -F auid>=1000 -F auid!=unset -F key=delete

Rationale:

Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="renameat"
	KEY="delete"
	SYSCALL_GROUPING="unlink unlinkat rename renameat rmdir"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_file_deletion_events_renameat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit renameat tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_file_deletion_events_renameat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of renameat in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules
      set_fact: audit_file="/etc/audit/rules.d/delete.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of renameat in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=delete
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_file_deletion_events_renameat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of renameat in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules
      set_fact: audit_file="/etc/audit/rules.d/delete.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of renameat in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=delete
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_file_deletion_events_renameat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Ensure auditd Collects File Deletion Events by User - rmdirrule

At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 or b64 as appropriate for your system:

-a always,exit -F arch=ARCH -S rmdir -F auid>=1000 -F auid!=unset -F key=delete
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S rmdir -F auid>=1000 -F auid!=unset -F key=delete

Rationale:

Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="rmdir"
	KEY="delete"
	SYSCALL_GROUPING="unlink unlinkat rename renameat rmdir"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_file_deletion_events_rmdir
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit rmdir tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_file_deletion_events_rmdir
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of rmdir in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules
      set_fact: audit_file="/etc/audit/rules.d/delete.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of rmdir in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=delete
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_file_deletion_events_rmdir
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of rmdir in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules
      set_fact: audit_file="/etc/audit/rules.d/delete.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of rmdir in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=delete
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_file_deletion_events_rmdir
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Ensure auditd Collects File Deletion Events by User - unlinkatrule

At a minimum, the audit system should collect file deletion events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 or b64 as appropriate for your system:

-a always,exit -F arch=ARCH -S unlinkat -F auid>=1000 -F auid!=unset -F key=delete
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S unlinkat -F auid>=1000 -F auid!=unset -F key=delete

Rationale:

Auditing file deletions will create an audit trail for files that are removed from the system. The audit trail could aid in system troubleshooting, as well as, detecting malicious processes that attempt to delete log files to conceal their presence.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="unlinkat"
	KEY="delete"
	SYSCALL_GROUPING="unlink unlinkat rename renameat rmdir"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_file_deletion_events_unlinkat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit unlinkat tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_file_deletion_events_unlinkat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of unlinkat in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules
      set_fact: audit_file="/etc/audit/rules.d/delete.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of unlinkat in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=delete
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_file_deletion_events_unlinkat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

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

    - name: Check existence of unlinkat in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/delete.rules
      set_fact: audit_file="/etc/audit/rules.d/delete.rules"
      when: found_paths | length == 0

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

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

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

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

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

    - name: Check existence of unlinkat in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=delete
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_file_deletion_events_unlinkat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Unauthorized Access Attempts Events to Files (unsuccessful)group

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

contains 6 rules

Record Unsuccessful Access Attempts to Files - creatrule

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

-a always,exit -F arch=b32 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S creat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S creat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access

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

Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL="creat"
KEY="access"
SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EACCES"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EPERM"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_creat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit creat tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_creat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - creat
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of creat in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

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

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

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

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

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - creat
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of creat in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_creat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - creat
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of creat in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

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

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

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

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

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - creat
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of creat in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_creat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - creat
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of creat in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

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

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

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

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

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - creat
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of creat in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_creat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

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

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - creat
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of creat in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

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

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

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

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

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - creat
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of creat in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

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

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

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

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_creat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Unsuccessful Access Attempts to Files - ftruncaterule

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

-a always,exit -F arch=b32 -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S ftruncate -F exiu=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access

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

Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL="ftruncate"
KEY="access"
SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EACCES"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EPERM"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

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

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


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

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

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

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

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

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

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

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

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

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_ftruncate
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit ftruncate tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_ftruncate
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for ftruncate EACCES for x86 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - ftruncate
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of ftruncate in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - ftruncate
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of ftruncate in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_ftruncate
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for ftruncate EACCES for x86_64 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - ftruncate
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of ftruncate in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - ftruncate
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of ftruncate in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_ftruncate
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for ftruncate EPERM for x86 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - ftruncate
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of ftruncate in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - ftruncate
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of ftruncate in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_ftruncate
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for ftruncate EPERM for x86_64 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - ftruncate
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of ftruncate in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - ftruncate
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of ftruncate in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_ftruncate
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Unsuccessful Access Attempts to Files - openrule

At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F arch=b32 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S open -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S open -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access

warning  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:

Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL="open"
KEY="access"
SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EACCES"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EPERM"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_open
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit open tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_open
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for open EACCES for x86 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - open
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of open in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - open
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of open in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_open
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for open EACCES for x86_64 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - open
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of open in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - open
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of open in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_open
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for open EPERM for x86 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - open
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of open in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - open
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of open in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_open
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for open EPERM for x86_64 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - open
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of open in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - open
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of open in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_open
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Unsuccessful Access Attempts to Files - open_by_handle_atrule

At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F arch=b32 -S open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S open_by_handle_at -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S open_by_handle_at -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S open_by_handle_at,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S open_by_handle_at,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access

warning  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:

Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL="open_by_handle_at"
KEY="access"
SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EACCES"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EPERM"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_open_by_handle_at
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit open_by_handle_at tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_open_by_handle_at
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for open_by_handle_at EACCES for x86 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - open_by_handle_at
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of open_by_handle_at in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - open_by_handle_at
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of open_by_handle_at in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_open_by_handle_at
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for open_by_handle_at EACCES for x86_64
    platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - open_by_handle_at
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of open_by_handle_at in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - open_by_handle_at
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of open_by_handle_at in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_open_by_handle_at
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for open_by_handle_at EPERM for x86 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - open_by_handle_at
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of open_by_handle_at in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - open_by_handle_at
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of open_by_handle_at in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_open_by_handle_at
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for open_by_handle_at EPERM for x86_64
    platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - open_by_handle_at
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of open_by_handle_at in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - open_by_handle_at
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of open_by_handle_at in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_open_by_handle_at
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Unsuccessful Access Attempts to Files - openatrule

At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F arch=b32 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access

warning  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:

Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL="openat"
KEY="access"
SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EACCES"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EPERM"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_openat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit openat tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_openat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for openat EACCES for x86 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - openat
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of openat in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - openat
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of openat in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_openat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for openat EACCES for x86_64 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - openat
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of openat in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - openat
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of openat in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_openat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for openat EPERM for x86 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - openat
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of openat in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - openat
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of openat in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_openat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for openat EPERM for x86_64 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - openat
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of openat in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - openat
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of openat in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_openat
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Unsuccessful Access Attempts to Files - truncaterule

At a minimum, the audit system should collect unauthorized file accesses for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F arch=b32 -S truncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S truncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S truncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S truncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file:
-a always,exit -F arch=b32 -S truncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b32 -S truncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access
If the system is 64 bit then also add the following lines:
-a always,exit -F arch=b64 -S truncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -F key=access
-a always,exit -F arch=b64 -S truncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -F key=access

warning  Note that these rules can be configured in a number of ways while still achieving the desired effect. Here the system calls have been placed independent of other system calls. Grouping these system calls with others as identifying earlier in this guide is more efficient.
Rationale:

Unsuccessful attempts to access files could be an indicator of malicious activity on a system. Auditing these events could serve as evidence of potential system compromise.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL="truncate"
KEY="access"
SYSCALL_GROUPING="creat ftruncate truncate open openat open_by_handle_at"

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EACCES"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group the syscall in the rule
        sed -i -e "\#${rule_to_edit}#s#${rule_syscalls_to_edit}#${new_grouped_syscalls}#" "$file_to_edit"
    fi
fi
done

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS="-F exit=-EPERM"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_truncate
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit truncate tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_truncate
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for truncate EACCES for x86 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - truncate
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of truncate in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - truncate
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of truncate in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_truncate
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for truncate EACCES for x86_64 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - truncate
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of truncate in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - truncate
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of truncate in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EACCES -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EACCES -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EACCES
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_truncate
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for truncate EPERM for x86 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - truncate
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of truncate in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - truncate
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of truncate in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_truncate
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for truncate EPERM for x86_64 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - truncate
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of truncate in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/access.rules
      set_fact: audit_file="/etc/audit/rules.d/access.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - truncate
        syscall_grouping:
          - creat
          - ftruncate
          - truncate
          - open
          - openat
          - open_by_handle_at

    - name: Check existence of truncate in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F exit=-EPERM -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F exit=-EPERM -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F exit=-EPERM
          -F auid>=1000 -F auid!=unset -F key=access
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.1
    - PCI-DSS-Req-10.2.4
    - audit_rules_unsuccessful_file_modification_truncate
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Information on Kernel Modules Loading and Unloadinggroup

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.

contains 3 rules

Ensure auditd Collects Information on Kernel Module Unloading - delete_modulerule

To capture kernel module unloading events, use following line, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit:

-a always,exit -F arch=ARCH -S delete_module -F key=modules
Place to add the line depends on a way auditd daemon is configured. If it is configured to use the augenrules program (the default), add the line to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility, add the line to file /etc/audit/audit.rules.

Rationale:

The removal of kernel modules can be used to alter the behavior of the kernel and potentially introduce malicious code into kernel space. It is important to have an audit trail of modules that have been introduced into the kernel.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
# Note: 32-bit and 64-bit kernel syscall numbers not always line up =>
#       it's required on a 64-bit system to check also for the presence
#       of 32-bit's equivalent of the corresponding rule.
#       (See `man 7 audit.rules` for details )
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS=""
	SYSCALL="delete_module"
	KEY="modules"
	SYSCALL_GROUPING="delete_module"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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.7
    - audit_rules_kernel_module_loading_delete
    - configure_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

- name: Set architecture for audit delete_module tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_kernel_module_loading_delete
    - configure_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

- name: Perform remediation of Audit rules for delete_module for x86 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - delete_module
        syscall_grouping: []

    - name: Check existence of delete_module in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/module-change.rules
      set_fact: audit_file="/etc/audit/rules.d/module-change.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=module-change
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - delete_module
        syscall_grouping: []

    - name: Check existence of delete_module in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=module-change
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_kernel_module_loading_delete
    - configure_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

- name: Perform remediation of Audit rules for delete_module for x86_64 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - delete_module
        syscall_grouping: []

    - name: Check existence of delete_module in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/module-change.rules
      set_fact: audit_file="/etc/audit/rules.d/module-change.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=module-change
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - delete_module
        syscall_grouping: []

    - name: Check existence of delete_module in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=module-change
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_kernel_module_loading_delete
    - configure_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

Ensure auditd Collects Information on Kernel Module Loading and Unloading - finit_modulerule

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 to capture kernel module loading and unloading events, setting ARCH to either b32 or b64 as appropriate for your system:

-a always,exit -F arch=ARCH -S finit_module -F key=modules
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 kernel module loading and unloading events, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S finit_module -F key=modules

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.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
# Note: 32-bit and 64-bit kernel syscall numbers not always line up =>
#       it's required on a 64-bit system to check also for the presence
#       of 32-bit's equivalent of the corresponding rule.
#       (See `man 7 audit.rules` for details )
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS=""
	SYSCALL="finit_module"
	KEY="modules"
	SYSCALL_GROUPING="init_module finit_module"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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.7
    - audit_rules_kernel_module_loading_finit
    - configure_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

- name: Set architecture for audit finit_module tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_kernel_module_loading_finit
    - configure_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

- name: Perform remediation of Audit rules for finit_module for x86 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - finit_module
        syscall_grouping:
          - init_module
          - finit_module

    - name: Check existence of finit_module in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/module-change.rules
      set_fact: audit_file="/etc/audit/rules.d/module-change.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=module-change
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - finit_module
        syscall_grouping:
          - init_module
          - finit_module

    - name: Check existence of finit_module in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=module-change
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_kernel_module_loading_finit
    - configure_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

- name: Perform remediation of Audit rules for finit_module for x86_64 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - finit_module
        syscall_grouping:
          - init_module
          - finit_module

    - name: Check existence of finit_module in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/module-change.rules
      set_fact: audit_file="/etc/audit/rules.d/module-change.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=module-change
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - finit_module
        syscall_grouping:
          - init_module
          - finit_module

    - name: Check existence of finit_module in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=module-change
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  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.7
    - audit_rules_kernel_module_loading_finit
    - configure_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

Ensure auditd Collects Information on Kernel Module Loading - init_modulerule

To capture kernel module loading events, use following line, setting ARCH to either b32 for 32-bit system, or having two lines for both b32 and b64 in case your system is 64-bit:

-a always,exit -F arch=ARCH -S init_module -F key=modules
Place to add the line depends on a way auditd daemon is configured. If it is configured to use the augenrules program (the default), add the line to a file with suffix .rules in the directory /etc/audit/rules.d. If the auditd daemon is configured to use the auditctl utility, add the line to file /etc/audit/audit.rules.

Rationale:

The addition of kernel modules can be used to alter the behavior of the kernel and potentially introduce malicious code into kernel space. It is important to have an audit trail of modules that have been introduced into the kernel.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
# Note: 32-bit and 64-bit kernel syscall numbers not always line up =>
#       it's required on a 64-bit system to check also for the presence
#       of 32-bit's equivalent of the corresponding rule.
#       (See `man 7 audit.rules` for details )
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS=""
	SYSCALL="init_module"
	KEY="modules"
	SYSCALL_GROUPING="init_module finit_module"
	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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.7
    - audit_rules_kernel_module_loading_init
    - configure_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

- name: Set architecture for audit init_module tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_kernel_module_loading_init
    - configure_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

- name: Perform remediation of Audit rules for init_module for x86 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - init_module
        syscall_grouping:
          - init_module
          - finit_module

    - name: Check existence of init_module in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/module-change.rules
      set_fact: audit_file="/etc/audit/rules.d/module-change.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=module-change
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - init_module
        syscall_grouping:
          - init_module
          - finit_module

    - name: Check existence of init_module in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F key=module-change
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_kernel_module_loading_init
    - configure_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

- name: Perform remediation of Audit rules for init_module for x86_64 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - init_module
        syscall_grouping:
          - init_module
          - finit_module

    - name: Check existence of init_module in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/module-change.rules
      set_fact: audit_file="/etc/audit/rules.d/module-change.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=module-change
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - init_module
        syscall_grouping:
          - init_module
          - finit_module

    - name: Check existence of init_module in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F key=module-change
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_kernel_module_loading_init
    - configure_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
contains 3 rules

Record Information on the Use of Privileged Commandsgroup

At a minimum, the audit system should collect the execution of privileged commands for all users and root.

contains 17 rules

Ensure auditd Collects Information on the Use of Privileged Commandsrule

The audit system should collect information about usage of privileged commands for all users and root. To find the relevant setuid / setgid programs, run the following command for each local partition PART:

$ sudo find PART -xdev -type f -perm -4000 -o -type f -perm -2000 2>/dev/null
If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d for each setuid / setgid program on the system, replacing the SETUID_PROG_PATH part with the full path of that setuid / setgid program in the list:
-a always,exit -F path=SETUID_PROG_PATH -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules for each setuid / setgid program on the system, replacing the SETUID_PROG_PATH part with the full path of that setuid / setgid program in the list:
-a always,exit -F path=SETUID_PROG_PATH -F auid>=1000 -F auid!=unset -F key=privileged

warning  This rule checks for multiple syscalls related to privileged commands; it was written with DISA STIG in mind. Other policies should use a separate rule for each syscall that needs to be checked. For example:
  • audit_rules_privileged_commands_su
  • audit_rules_privileged_commands_umount
  • audit_rules_privileged_commands_passwd
Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
files_to_inspect=()

# If the audit tool is 'auditctl', then:
# * add '/etc/audit/audit.rules'to the list of files to be inspected,
# * specify '/etc/audit/audit.rules' as the output audit file, where
#   missing rules should be inserted
files_to_inspect=("/etc/audit/audit.rules")
output_audit_file="/etc/audit/audit.rules"

# Obtain the list of SUID/SGID binaries on the particular system (split by newline)
# into privileged_binaries array
privileged_binaries=()
readarray -t privileged_binaries < <(find / -not \( -fstype afs -o -fstype ceph -o -fstype cifs -o -fstype smb3 -o -fstype smbfs -o -fstype sshfs -o -fstype ncpfs -o -fstype ncp -o -fstype nfs -o -fstype nfs4 -o -fstype gfs -o -fstype gfs2 -o -fstype glusterfs -o -fstype gpfs -o -fstype pvfs2 -o -fstype ocfs2 -o -fstype lustre -o -fstype davfs -o -fstype fuse.sshfs \) -type f \( -perm -4000 -o -perm -2000 \) 2> /dev/null)

# Keep list of SUID/SGID binaries that have been already handled within some previous iteration
sbinaries_to_skip=()

# For each found sbinary in privileged_binaries list
for sbinary in "${privileged_binaries[@]}"
do

    # Check if this sbinary wasn't already handled in some of the previous sbinary iterations
    # Return match only if whole sbinary definition matched (not in the case just prefix matched!!!)
    if [[ $(sed -ne "\|${sbinary}|p" <<< "${sbinaries_to_skip[*]}") ]]
    then
        # If so, don't process it second time & go to process next sbinary
        continue
    fi

    # Reset the counter of inspected files when starting to check
    # presence of existing audit rule for new sbinary
    count_of_inspected_files=0

    # Define expected rule form for this binary
    expected_rule="-a always,exit -F path=${sbinary} -F auid>=1000 -F auid!=unset -F key=privileged"

    # If list of audit rules files to be inspected is empty, just add new rule and move on to next binary
    if [[ ${#files_to_inspect[@]} -eq 0 ]]; then
        echo "$expected_rule" >> "$output_audit_file"
        continue
    fi

    # Replace possible slash '/' character in sbinary definition so we could use it in sed expressions below
    sbinary_esc=${sbinary//$'/'/$'\/'}

    # For each audit rules file from the list of files to be inspected
    for afile in "${files_to_inspect[@]}"
    do
        # Search current audit rules file's content for match. Match criteria:
        # * existing rule is for the same SUID/SGID binary we are currently processing (but
        #   can contain multiple -F path= elements covering multiple SUID/SGID binaries)
        # * existing rule contains all arguments from expected rule form (though can contain
        #   them in arbitrary order)

        base_search=$(sed -e '/-a always,exit/!d' -e '/-F path='"${sbinary_esc}"'[^[:graph:]]/!d'		\
                -e '/-F path=[^[:space:]]\+/!d'						\
                -e '/-F auid>='"1000"'/!d' -e '/-F auid!=\(4294967295\|unset\)/!d'	\
                -e '/-k \|-F key=/!d' "$afile")

        # Increase the count of inspected files for this sbinary
        count_of_inspected_files=$((count_of_inspected_files + 1))

        # Search current audit rules file's content for presence of rule pattern for this sbinary
        if [[ $base_search ]]
        then

            # Current audit rules file already contains rule for this binary =>
            # Store the exact form of found rule for this binary for further processing
            concrete_rule=$base_search

            # Select all other SUID/SGID binaries possibly also present in the found rule

            readarray -t handled_sbinaries < <(grep -o -e "-F path=[^[:space:]]\+" <<< "$concrete_rule")
            handled_sbinaries=("${handled_sbinaries[@]//-F path=/}")

            # Merge the list of such SUID/SGID binaries found in this iteration with global list ignoring duplicates
            readarray -t sbinaries_to_skip < <(for i in "${sbinaries_to_skip[@]}" "${handled_sbinaries[@]}"; do echo "$i"; done | sort -du)

            # if there is a -F perm flag, remove it
            if grep -q '.*-F\s\+perm=[rwxa]\+.*' <<< "$concrete_rule"; then

                # Separate concrete_rule into three sections using hash '#'
                # sign as a delimiter around rule's permission section borders
                # note that the trailing space after perm flag is captured because there would be
                # two consecutive spaces after joining remaining parts of the rule together
                concrete_rule="$(echo "$concrete_rule" | sed -n "s/\(.*\)\+\(-F perm=[rwax]\+\ \?\)\+/\1#\2#/p")"

                # Split concrete_rule into head and tail sections using hash '#' delimiter
                # The second column contains the permission section, which we don't need to extract
                rule_head=$(cut -d '#' -f 1 <<< "$concrete_rule")
                rule_tail=$(cut -d '#' -f 3 <<< "$concrete_rule")

                # Remove permissions section from existing rule in the file
                sed -i "s#${rule_head}\(.*\)${rule_tail}#${rule_head}${rule_tail}#" "$afile"
            fi
        # If the required audit rule for particular sbinary wasn't found yet, insert it under following conditions:
        #
        # * in the "auditctl" mode of operation insert particular rule each time
        #   (because in this mode there's only one file -- /etc/audit/audit.rules to be inspected for presence of this rule),
        #
        # * in the "augenrules" mode of operation insert particular rule only once and only in case we have already
        #   searched all of the files from /etc/audit/rules.d/*.rules location (since that audit rule can be defined
        #   in any of those files and if not, we want it to be inserted only once into /etc/audit/rules.d/privileged.rules file)
        #
        elif [ "auditctl" == "auditctl" ] || [[ "auditctl" == "augenrules" && $count_of_inspected_files -eq "${#files_to_inspect[@]}" ]]
        then
            # Check if this sbinary wasn't already handled in some of the previous afile iterations
            # Return match only if whole sbinary definition matched (not in the case just prefix matched!!!)
            if [[ ! $(sed -ne "\|${sbinary}|p" <<< "${sbinaries_to_skip[*]}") ]]
            then
                # Current audit rules file's content doesn't contain expected rule for this
                # SUID/SGID binary yet => append it
                echo "$expected_rule" >> "$output_audit_file"
            fi
            continue
        fi
    done
done
files_to_inspect=()
# If the audit tool is 'augenrules', then:
# * add '/etc/audit/rules.d/*.rules' to the list of files to be inspected
#   (split by newline),
# * specify /etc/audit/rules.d/privileged.rules' as the output file, where
#   missing rules should be inserted
readarray -t files_to_inspect < <(find /etc/audit/rules.d -maxdepth 1 -type f -name '*.rules' -print)
output_audit_file="/etc/audit/rules.d/privileged.rules"

# Obtain the list of SUID/SGID binaries on the particular system (split by newline)
# into privileged_binaries array
privileged_binaries=()
readarray -t privileged_binaries < <(find / -not \( -fstype afs -o -fstype ceph -o -fstype cifs -o -fstype smb3 -o -fstype smbfs -o -fstype sshfs -o -fstype ncpfs -o -fstype ncp -o -fstype nfs -o -fstype nfs4 -o -fstype gfs -o -fstype gfs2 -o -fstype glusterfs -o -fstype gpfs -o -fstype pvfs2 -o -fstype ocfs2 -o -fstype lustre -o -fstype davfs -o -fstype fuse.sshfs \) -type f \( -perm -4000 -o -perm -2000 \) 2> /dev/null)

# Keep list of SUID/SGID binaries that have been already handled within some previous iteration
sbinaries_to_skip=()

# For each found sbinary in privileged_binaries list
for sbinary in "${privileged_binaries[@]}"
do

    # Check if this sbinary wasn't already handled in some of the previous sbinary iterations
    # Return match only if whole sbinary definition matched (not in the case just prefix matched!!!)
    if [[ $(sed -ne "\|${sbinary}|p" <<< "${sbinaries_to_skip[*]}") ]]
    then
        # If so, don't process it second time & go to process next sbinary
        continue
    fi

    # Reset the counter of inspected files when starting to check
    # presence of existing audit rule for new sbinary
    count_of_inspected_files=0

    # Define expected rule form for this binary
    expected_rule="-a always,exit -F path=${sbinary} -F auid>=1000 -F auid!=unset -F key=privileged"

    # If list of audit rules files to be inspected is empty, just add new rule and move on to next binary
    if [[ ${#files_to_inspect[@]} -eq 0 ]]; then
        echo "$expected_rule" >> "$output_audit_file"
        continue
    fi

    # Replace possible slash '/' character in sbinary definition so we could use it in sed expressions below
    sbinary_esc=${sbinary//$'/'/$'\/'}

    # For each audit rules file from the list of files to be inspected
    for afile in "${files_to_inspect[@]}"
    do
        # Search current audit rules file's content for match. Match criteria:
        # * existing rule is for the same SUID/SGID binary we are currently processing (but
        #   can contain multiple -F path= elements covering multiple SUID/SGID binaries)
        # * existing rule contains all arguments from expected rule form (though can contain
        #   them in arbitrary order)

        base_search=$(sed -e '/-a always,exit/!d' -e '/-F path='"${sbinary_esc}"'[^[:graph:]]/!d'		\
                -e '/-F path=[^[:space:]]\+/!d'						\
                -e '/-F auid>='"1000"'/!d' -e '/-F auid!=\(4294967295\|unset\)/!d'	\
                -e '/-k \|-F key=/!d' "$afile")

        # Increase the count of inspected files for this sbinary
        count_of_inspected_files=$((count_of_inspected_files + 1))

        # Search current audit rules file's content for presence of rule pattern for this sbinary
        if [[ $base_search ]]
        then

            # Current audit rules file already contains rule for this binary =>
            # Store the exact form of found rule for this binary for further processing
            concrete_rule=$base_search

            # Select all other SUID/SGID binaries possibly also present in the found rule

            readarray -t handled_sbinaries < <(grep -o -e "-F path=[^[:space:]]\+" <<< "$concrete_rule")
            handled_sbinaries=("${handled_sbinaries[@]//-F path=/}")

            # Merge the list of such SUID/SGID binaries found in this iteration with global list ignoring duplicates
            readarray -t sbinaries_to_skip < <(for i in "${sbinaries_to_skip[@]}" "${handled_sbinaries[@]}"; do echo "$i"; done | sort -du)

            # if there is a -F perm flag, remove it
            if grep -q '.*-F\s\+perm=[rwxa]\+.*' <<< "$concrete_rule"; then

                # Separate concrete_rule into three sections using hash '#'
                # sign as a delimiter around rule's permission section borders
                # note that the trailing space after perm flag is captured because there would be
                # two consecutive spaces after joining remaining parts of the rule together
                concrete_rule="$(echo "$concrete_rule" | sed -n "s/\(.*\)\+\(-F perm=[rwax]\+\ \?\)\+/\1#\2#/p")"

                # Split concrete_rule into head and tail sections using hash '#' delimiter
                # The second column contains the permission section, which we don't need to extract
                rule_head=$(cut -d '#' -f 1 <<< "$concrete_rule")
                rule_tail=$(cut -d '#' -f 3 <<< "$concrete_rule")

                # Remove permissions section from existing rule in the file
                sed -i "s#${rule_head}\(.*\)${rule_tail}#${rule_head}${rule_tail}#" "$afile"
            fi
        # If the required audit rule for particular sbinary wasn't found yet, insert it under following conditions:
        #
        # * in the "auditctl" mode of operation insert particular rule each time
        #   (because in this mode there's only one file -- /etc/audit/audit.rules to be inspected for presence of this rule),
        #
        # * in the "augenrules" mode of operation insert particular rule only once and only in case we have already
        #   searched all of the files from /etc/audit/rules.d/*.rules location (since that audit rule can be defined
        #   in any of those files and if not, we want it to be inserted only once into /etc/audit/rules.d/privileged.rules file)
        #
        elif [ "augenrules" == "auditctl" ] || [[ "augenrules" == "augenrules" && $count_of_inspected_files -eq "${#files_to_inspect[@]}" ]]
        then
            # Check if this sbinary wasn't already handled in some of the previous afile iterations
            # Return match only if whole sbinary definition matched (not in the case just prefix matched!!!)
            if [[ ! $(sed -ne "\|${sbinary}|p" <<< "${sbinaries_to_skip[*]}") ]]
            then
                # Current audit rules file's content doesn't contain expected rule for this
                # SUID/SGID binary yet => append it
                echo "$expected_rule" >> "$output_audit_file"
            fi
            continue
        fi
    done
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.2
    - audit_rules_privileged_commands
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Search for privileged commands
  shell: |
    set -o pipefail
    find / -not \( -fstype afs -o -fstype ceph -o -fstype cifs -o -fstype smb3 -o -fstype smbfs -o -fstype sshfs -o -fstype ncpfs -o -fstype ncp -o -fstype nfs -o -fstype nfs4 -o -fstype gfs -o -fstype gfs2 -o -fstype glusterfs -o -fstype gpfs -o -fstype pvfs2 -o -fstype ocfs2 -o -fstype lustre -o -fstype davfs -o -fstype fuse.sshfs \) -type f \( -perm -4000 -o -perm -2000 \) 2> /dev/null
  args:
    warn: false
    executable: /bin/bash
  check_mode: false
  register: find_result
  changed_when: false
  failed_when: false
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.2
    - audit_rules_privileged_commands
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Search /etc/audit/rules.d for audit rule entries
  find:
    paths: /etc/audit/rules.d
    recurse: false
    contains: ^.*path={{ item }} .*$
    patterns: '*.rules'
  with_items:
    - '{{ find_result.stdout_lines }}'
  register: files_result
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.2
    - audit_rules_privileged_commands
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Overwrites the rule in rules.d
  lineinfile:
    path: '{{ item.1.path }}'
    line: -a always,exit -F path={{ item.0.item }} -F auid>=1000 -F auid!=unset -F
      key=privileged
    create: false
    regexp: ^.*path={{ item.0.item }} .*$
  with_subelements:
    - '{{ files_result.results }}'
    - files
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.2
    - audit_rules_privileged_commands
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Adds the rule in rules.d
  lineinfile:
    path: /etc/audit/rules.d/privileged.rules
    line: -a always,exit -F path={{ item.item }} -F auid>=1000 -F auid!=unset -F key=privileged
    create: true
  with_items:
    - '{{ files_result.results }}'
  when:
    - '"audit" in ansible_facts.packages'
    - files_result.results is defined and item.matched == 0
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.2
    - audit_rules_privileged_commands
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Inserts/replaces the rule in audit.rules
  lineinfile:
    path: /etc/audit/audit.rules
    line: -a always,exit -F path={{ item.item }} -F auid>=1000 -F auid!=unset -F key=privileged
    create: true
    regexp: ^.*path={{ item.item }} .*$
  with_items:
    - '{{ files_result.results }}'
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.2
    - audit_rules_privileged_commands
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on the Use of Privileged Commands - chagerule

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F path=/usr/bin/chage -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/chage -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/chage"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_chage
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/chage
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/chage -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
      set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chage -F auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chage -F auid>=1000
          -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/chage -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
          }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chage -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chage -F auid>=1000
          -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_chage
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on the Use of Privileged Commands - chshrule

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F path=/usr/bin/chsh -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/chsh -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/chsh"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_chsh
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/chsh
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/chsh -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
      set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chsh -F auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chsh -F auid>=1000
          -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/chsh -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
          }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/chsh -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/chsh -F auid>=1000
          -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_chsh
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on the Use of Privileged Commands - crontabrule

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F path=/usr/bin/crontab -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/crontab -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/crontab"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_crontab
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/crontab
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/crontab -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
      set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/crontab -F auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/crontab -F
          auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/crontab -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
          }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/crontab -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/crontab -F
          auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_crontab
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on the Use of Privileged Commands - gpasswdrule

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F path=/usr/bin/gpasswd -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/gpasswd -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/gpasswd"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_gpasswd
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/gpasswd
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/gpasswd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
      set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/gpasswd -F auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/gpasswd -F
          auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/gpasswd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
          }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/gpasswd -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/gpasswd -F
          auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_gpasswd
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on the Use of Privileged Commands - newgrprule

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F path=/usr/bin/newgrp -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/newgrp -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/newgrp"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_newgrp
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/newgrp
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/newgrp -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
      set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/newgrp -F auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/newgrp -F
          auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/newgrp -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
          }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/newgrp -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/newgrp -F
          auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_newgrp
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on the Use of Privileged Commands - pam_timestamp_checkrule

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F path=/usr/sbin/pam_timestamp_check
-F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/sbin/pam_timestamp_check
-F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/pam_timestamp_check"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_pam_timestamp_check
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Perform remediation of Audit rules for /usr/sbin/pam_timestamp_check
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/sbin/pam_timestamp_check -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
      set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/pam_timestamp_check
          -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/pam_timestamp_check
          -F auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/sbin/pam_timestamp_check -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
          }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/pam_timestamp_check -F auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/pam_timestamp_check
          -F auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_pam_timestamp_check
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on the Use of Privileged Commands - passwdrule

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F path=/usr/bin/passwd -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/passwd -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/passwd"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_passwd
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/passwd
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/passwd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
      set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/passwd -F auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/passwd -F
          auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/passwd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
          }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/passwd -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/passwd -F
          auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_passwd
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on the Use of Privileged Commands - postdroprule

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F path=/usr/sbin/postdrop -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/sbin/postdrop -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/postdrop"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_postdrop
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Perform remediation of Audit rules for /usr/sbin/postdrop
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/sbin/postdrop -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
      set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/postdrop -F auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/postdrop
          -F auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/sbin/postdrop -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
          }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/postdrop -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/postdrop
          -F auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_postdrop
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on the Use of Privileged Commands - postqueuerule

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F path=/usr/sbin/postqueue -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/sbin/postqueue -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/postqueue"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_postqueue
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Perform remediation of Audit rules for /usr/sbin/postqueue
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/sbin/postqueue -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
      set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/postqueue -F auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/postqueue
          -F auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/sbin/postqueue -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
          }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/postqueue -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/postqueue
          -F auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_postqueue
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on the Use of Privileged Commands - ssh-keysignrule

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F path=/usr/libexec/openssh/ssh-keysign -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/libexec/openssh/ssh-keysign -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/libexec/openssh/ssh-keysign"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_ssh_keysign
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Perform remediation of Audit rules for /usr/libexec/openssh/ssh-keysign
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/libexec/openssh/ssh-keysign -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
      set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/libexec/openssh/ssh-keysign
          -F auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/libexec/openssh/ssh-keysign
          -F auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/libexec/openssh/ssh-keysign -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
          }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/libexec/openssh/ssh-keysign -F auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/libexec/openssh/ssh-keysign
          -F auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_ssh_keysign
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on the Use of Privileged Commands - surule

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F path=/usr/bin/su -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/su -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/su"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_su
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/su
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/su -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
      set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/su -F auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/su -F auid>=1000
          -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/su -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
          }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/su -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/su -F auid>=1000
          -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_su
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on the Use of Privileged Commands - sudorule

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F path=/usr/bin/sudo -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/sudo -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/sudo"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_sudo
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/sudo
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/sudo -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
      set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/sudo -F auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/sudo -F auid>=1000
          -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/sudo -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
          }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/sudo -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/sudo -F auid>=1000
          -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_sudo
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on the Use of Privileged Commands - sudoeditrule

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F path=/usr/bin/sudoedit -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/sudoedit -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/sudoedit"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_sudoedit
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/sudoedit
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/sudoedit -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
      set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/sudoedit -F auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/sudoedit -F
          auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/sudoedit -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
          }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/sudoedit -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/sudoedit -F
          auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_sudoedit
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on the Use of Privileged Commands - umountrule

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F path=/usr/bin/umount -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/bin/umount -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/bin/umount"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_umount
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Perform remediation of Audit rules for /usr/bin/umount
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/umount -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
      set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/umount -F auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/umount -F
          auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/bin/umount -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
          }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/bin/umount -F auid>=1000 -F auid!=unset
          (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/bin/umount -F
          auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_umount
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on the Use of Privileged Commands - unix_chkpwdrule

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F path=/usr/sbin/unix_chkpwd -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/sbin/unix_chkpwd -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/unix_chkpwd"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(a)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-12.1(ii)
    - NIST-800-53-AU-12.1(iv)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-AU-3
    - NIST-800-53-AU-3.1
    - NIST-800-53-CM-6(a)
    - NIST-800-53-MA-4(1)(a)
    - audit_rules_privileged_commands_unix_chkpwd
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Perform remediation of Audit rules for /usr/sbin/unix_chkpwd
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/sbin/unix_chkpwd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
      set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/unix_chkpwd -F
          auid>=1000 -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/unix_chkpwd
          -F auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/sbin/unix_chkpwd -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
          }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/unix_chkpwd -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/unix_chkpwd
          -F auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(a)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-12.1(ii)
    - NIST-800-53-AU-12.1(iv)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-AU-3
    - NIST-800-53-AU-3.1
    - NIST-800-53-CM-6(a)
    - NIST-800-53-MA-4(1)(a)
    - audit_rules_privileged_commands_unix_chkpwd
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on the Use of Privileged Commands - userhelperrule

At a minimum, the audit system should collect the execution of privileged commands for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add a line of the following form to a file with suffix .rules in the directory /etc/audit/rules.d:

-a always,exit -F path=/usr/sbin/userhelper -F auid>=1000 -F auid!=unset -F key=privileged
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add a line of the following form to /etc/audit/audit.rules:
-a always,exit -F path=/usr/sbin/userhelper -F auid>=1000 -F auid!=unset -F key=privileged

Rationale:

Misuse of privileged functions, either intentionally or unintentionally by authorized users, or by unauthorized external entities that have compromised system accounts, is a serious and ongoing concern and can have significant adverse impacts on organizations. Auditing the use of privileged functions is one way to detect such misuse and identify the risk from insider and advanced persistent threats.

Privileged programs are subject to escalation-of-privilege attacks, which attempt to subvert their normal role of providing some necessary but limited capability. As such, motivation exists to monitor these programs for unusual activity.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

ACTION_ARCH_FILTERS="-a always,exit"
OTHER_FILTERS="-F path=/usr/sbin/userhelper"
AUID_FILTERS="-F auid>=1000 -F auid!=unset"
SYSCALL=""
KEY="privileged"
SYSCALL_GROUPING=""
# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_userhelper
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Perform remediation of Audit rules for /usr/sbin/userhelper
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/sbin/userhelper -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/privileged.rules
      set_fact: audit_file="/etc/audit/rules.d/privileged.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/userhelper -F auid>=1000
          -F auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/userhelper
          -F auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls: []
        syscall_grouping: []

    - name: Check existence of  in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit(( -S |,)\w+)*(( -S |,){{ item }})+(( -S |,)\w+)*
          -F path=/usr/sbin/userhelper -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit)(?=.*(?:(?:-S |,)(?:{{ syscalls_found | join("|")
          }}))\b)((?:( -S |,)\w+)+)( -F path=/usr/sbin/userhelper -F auid>=1000 -F
          auid!=unset (?:-k |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit{{ syscalls | join(',') }} -F path=/usr/sbin/userhelper
          -F auid>=1000 -F auid!=unset -F key=privileged
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - audit_rules_privileged_commands_userhelper
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure auditd Collects Information on Exporting to Media (successful)rule

At a minimum, the audit system should collect media exportation events for all users and root. If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d, setting ARCH to either b32 or b64 as appropriate for your system:

-a always,exit -F arch=ARCH -S mount -F auid>=1000 -F auid!=unset -F key=export
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to /etc/audit/audit.rules file, setting ARCH to either b32 or b64 as appropriate for your system:
-a always,exit -F arch=ARCH -S mount -F auid>=1000 -F auid!=unset -F key=export

Rationale:

The unauthorized exportation of data to external media could result in an information leak where classified information, Privacy Act information, and intellectual property could be lost. An audit trail should be created each time a filesystem is mounted to help identify and guard against information loss.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# First perform the remediation of the syscall rule
# Retrieve hardware architecture of the underlying system
[ "$(getconf LONG_BIT)" = "32" ] && RULE_ARCHS=("b32") || RULE_ARCHS=("b32" "b64")

for ARCH in "${RULE_ARCHS[@]}"
do
	ACTION_ARCH_FILTERS="-a always,exit -F arch=$ARCH"
	OTHER_FILTERS=""
	AUID_FILTERS="-F auid>=1000 -F auid!=unset"
	SYSCALL="mount"
	KEY="perm_mod"
	SYSCALL_GROUPING=""

	# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
	unset syscall_a
unset syscall_grouping
unset syscall_string
unset syscall
unset file_to_edit
unset rule_to_edit
unset rule_syscalls_to_edit
unset other_string
unset auid_string
unset full_rule

# Load macro arguments into arrays
read -a syscall_a <<< $SYSCALL
read -a syscall_grouping <<< $SYSCALL_GROUPING

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
# 
# -----------------------------------------------------------------------------------------
#  Tool used to load audit rules | Rule already defined  |  Audit rules file to inspect    |
# -----------------------------------------------------------------------------------------
#        auditctl                |     Doesn't matter    |  /etc/audit/audit.rules         |
# -----------------------------------------------------------------------------------------
#        augenrules              |          Yes          |  /etc/audit/rules.d/*.rules     |
#        augenrules              |          No           |  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
#
files_to_inspect=()


# If audit tool is 'augenrules', then check if the audit rule is defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to the list for inspection
# If rule isn't defined yet, add '/etc/audit/rules.d/$key.rules' to the list for inspection
default_file="/etc/audit/rules.d/$KEY.rules"
# As other_filters may include paths, lets use a different delimiter for it
# The "F" script expression tells sed to print the filenames where the expressions matched
readarray -t files_to_inspect < <(sed -s -n -e "/$ACTION_ARCH_FILTERS/!d" -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" -e "F" /etc/audit/rules.d/*.rules)
# Case when particular rule isn't defined in /etc/audit/rules.d/*.rules yet
if [ ${#files_to_inspect[@]} -eq "0" ]
then
    file_to_inspect="/etc/audit/rules.d/$KEY.rules"
    files_to_inspect=("$file_to_inspect")
    if [ ! -e "$file_to_inspect" ]
    then
        touch "$file_to_inspect"
        chmod 0640 "$file_to_inspect"
    fi
fi

# After converting to jinja, we cannot return; therefore we skip the rest of the macro if needed instead
skip=1

for audit_file in "${files_to_inspect[@]}"
do
    # Filter existing $audit_file rules' definitions to select those that satisfy the rule pattern,
    # i.e, collect rules that match:
    # * the action, list and arch, (2-nd argument)
    # * the other filters, (3-rd argument)
    # * the auid filters, (4-rd argument)
    readarray -t similar_rules < <(sed -e "/$ACTION_ARCH_FILTERS/!d"  -e "\#$OTHER_FILTERS#!d" -e "/$AUID_FILTERS/!d" "$audit_file")

    candidate_rules=()
    # Filter out rules that have more fields then required. This will remove rules more specific than the required scope
    for s_rule in "${similar_rules[@]}"
    do
        # Strip all the options and fields we know of,
        # than check if there was any field left over
        extra_fields=$(sed -E -e "s/$ACTION_ARCH_FILTERS//"  -e "s#$OTHER_FILTERS##" -e "s/$AUID_FILTERS//" -e "s/((:?-S [[:alnum:],]+)+)//g" -e "s/-F key=\w+|-k \w+//"<<< "$s_rule")
        grep -q -- "-F" <<< "$extra_fields"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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"
        if [ $? -ne 0 ]
        then
            candidate_rules+=("$s_rule")
        fi
    done

    if [[ ${#syscall_a[@]} -ge 1 ]]
    then
        # Check if the syscall we want is present in any of the similar existing rules
        for rule in "${candidate_rules[@]}"
        do
            rule_syscalls=$(echo "$rule" | grep -o -P '(-S [\w,]+)+' | xargs)
            all_syscalls_found=0
            for syscall in "${syscall_a[@]}"
            do
                grep -q -- "\b${syscall}\b" <<< "$rule_syscalls"
                if [ $? -eq 1 ]
                then
                    # A syscall was not found in the candidate rule
                    all_syscalls_found=1
                fi
            done
            if [[ $all_syscalls_found -eq 0 ]]
            then
                # We found a rule with all the syscall(s) we want; skip rest of macro
                skip=0
                break
            fi

            # Check if this rule can be grouped with our target syscall and keep track of it
            for syscall_g in "${syscall_grouping[@]}"
            do
                if grep -q -- "\b${syscall_g}\b" <<< "$rule_syscalls"
                then
                    file_to_edit=${audit_file}
                    rule_to_edit=${rule}
                    rule_syscalls_to_edit=${rule_syscalls}
                fi
            done
        done
    else
        # If there is any candidate rule, it is compliant; skip rest of macro
        if [ "${#candidate_rules[@]}" -gt 0 ]
        then
            skip=0
        fi
    fi

    if [ "$skip" -eq 0 ]; then
        break
    fi
done

if [ "$skip" -ne 0 ]; then
    # We checked all rules that matched the expected resemblance pattern (action, arch & auid)
    # At this point we know if we need to either append the $full_rule or group
    # the syscall together with an exsiting rule

    # Append the full_rule if it cannot be grouped to any other rule
    if [ -z ${rule_to_edit+x} ]
    then
        # Build full_rule while avoid adding double spaces when other_filters is empty
        if [ "${#syscall_a[@]}" -gt 0 ]
        then
            syscall_string=""
            for syscall in "${syscall_a[@]}"
            do
                syscall_string+=" -S $syscall"
            done
        fi
        other_string=$([[ $OTHER_FILTERS ]] && echo " $OTHER_FILTERS")
        auid_string=$([[ $AUID_FILTERS ]] && echo " $AUID_FILTERS")
        full_rule="$ACTION_ARCH_FILTERS${syscall_string}${other_string}${auid_string} -F key=$KEY"
        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}"
            if [ $? -eq 1 ]
            then
                # A syscall was not found in the candidate rule
                new_grouped_syscalls+="${delimiter}${syscall}"
            fi
        done

        # Group 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 script:
- 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_media_export
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit mount tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_media_export
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for mount for x86 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - mount
        syscall_grouping: []

    - name: Check existence of mount in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
          |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - mount
        syscall_grouping: []

    - name: Check existence of mount in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b32(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b32)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
          |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b32 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_media_export
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Perform remediation of Audit rules for mount for x86_64 platform
  block:

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - mount
        syscall_grouping: []

    - name: Check existence of mount in /etc/audit/rules.d/
      find:
        paths: /etc/audit/rules.d
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: '*.rules'
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Reset syscalls found per file
      set_fact:
        syscalls_per_file: {}
        found_paths_dict: {}

    - name: Declare syscalls found per file
      set_fact: syscalls_per_file="{{ syscalls_per_file | combine( {item.files[0].path
        :[item.item] + syscalls_per_file.get(item.files[0].path, []) } ) }}"
      loop: '{{ find_command.results | selectattr(''matched'') | list }}'

    - name: Declare files where syscalls were found
      set_fact: found_paths="{{ find_command.results | map(attribute='files') | flatten
        | map(attribute='path') | list }}"

    - name: Count occurrences of syscalls in paths
      set_fact: found_paths_dict="{{ found_paths_dict | combine({ item:1+found_paths_dict.get(item,
        0) }) }}"
      loop: '{{ find_command.results | map(attribute=''files'') | flatten | map(attribute=''path'')
        | list }}'

    - name: Get path with most syscalls
      set_fact: audit_file="{{ (found_paths_dict | dict2items() | sort(attribute='value')
        | last).key }}"
      when: found_paths | length >= 1

    - name: No file with syscall found, set path to /etc/audit/rules.d/perm_mod.rules
      set_fact: audit_file="/etc/audit/rules.d/perm_mod.rules"
      when: found_paths | length == 0

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_per_file[audit_file]
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
          |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0

    - name: Declare list of syscalls
      set_fact:
        syscalls:
          - mount
        syscall_grouping: []

    - name: Check existence of mount in /etc/audit/audit.rules
      find:
        paths: /etc/audit
        contains: -a always,exit -F arch=b64(( -S |,)\w+)*(( -S |,){{ item }})+((
          -S |,)\w+)* -F auid>=1000 -F auid!=unset (-k\s+|-F\s+key=)\S+\s*$
        patterns: audit.rules
      register: find_command
      loop: '{{ (syscall_grouping + syscalls) | unique }}'

    - name: Set path to /etc/audit/audit.rules
      set_fact: audit_file="/etc/audit/audit.rules"

    - name: Declare found syscalls
      set_fact: syscalls_found="{{ find_command.results | selectattr('matched') |
        map(attribute='item') | list }}"

    - name: Declare missing syscalls
      set_fact: missing_syscalls="{{ syscalls | difference(syscalls_found) }}"

    - name: Replace the audit rule in {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        regexp: (-a always,exit -F arch=b64)(?=.*(?:(?:-S |,)(?:{{ syscalls_found
          | join("|") }}))\b)((?:( -S |,)\w+)+)( -F auid>=1000 -F auid!=unset (?:-k
          |-F key=)\w+)
        line: \1\2\3{{ missing_syscalls | join("\3") }}\4
        backrefs: true
        state: present
      when: syscalls_found | length > 0 and missing_syscalls | length > 0

    - name: Add the audit rule to {{ audit_file }}
      lineinfile:
        path: '{{ audit_file }}'
        line: -a always,exit -F arch=b64 -S {{ syscalls | join(',') }} -F auid>=1000
          -F auid!=unset -F key=perm_mod
        create: true
        mode: o-rwx
        state: present
      when: syscalls_found | length == 0
  when:
    - '"audit" in ansible_facts.packages'
    - audit_arch == "b64"
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.7
    - audit_rules_media_export
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Ensure auditd Collects System Administrator Actionsrule

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.

references:  CCI-000126, CCI-000130, CCI-000135, CCI-000169, CCI-000172, CCI-002884, SRG-OS-000462-VMM-001840, SRG-OS-000471-VMM-001910, 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, 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, 5.4.1.1, Req-10.2.2, Req-10.2.5.b, SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000304-GPOS-00121, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221, 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), 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, 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, 3.1.7, 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9, FAU_GEN.1.1.c

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'
# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()


# If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# into the list of files to be inspected
files_to_inspect+=('/etc/audit/audit.rules')

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "[\s]*-w[\s]+/etc/sudoers" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/sudoers$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/sudoers -p wa -k actions" >> "$audit_rules_file"
    fi
done
# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()

# If the audit is 'augenrules', then check if rule is already defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
# If rule isn't defined, add '/etc/audit/rules.d/actions.rules' to list of files for inspection.
readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/sudoers" /etc/audit/rules.d/*.rules)

# For each of the matched entries
for match in "${matches[@]}"
do
    # Extract filepath from the match
    rulesd_audit_file=$(echo $match | cut -f1 -d ':')
    # Append that path into list of files for inspection
    files_to_inspect+=("$rulesd_audit_file")
done
# Case when particular audit rule isn't defined yet
if [ "${#files_to_inspect[@]}" -eq "0" ]
then
    # Append '/etc/audit/rules.d/actions.rules' into list of files for inspection
    key_rule_file="/etc/audit/rules.d/actions.rules"
    # If the actions.rules file doesn't exist yet, create it with correct permissions
    if [ ! -e "$key_rule_file" ]
    then
        touch "$key_rule_file"
        chmod 0640 "$key_rule_file"
    fi
    files_to_inspect+=("$key_rule_file")
fi

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "[\s]*-w[\s]+/etc/sudoers" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/sudoers$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/sudoers -p wa -k actions" >> "$audit_rules_file"
    fi
done

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()


# If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# into the list of files to be inspected
files_to_inspect+=('/etc/audit/audit.rules')

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "[\s]*-w[\s]+/etc/sudoers.d/" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers.d/ $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/sudoers.d/$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/sudoers.d/ -p wa -k actions" >> "$audit_rules_file"
    fi
done
# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()

# If the audit is 'augenrules', then check if rule is already defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
# If rule isn't defined, add '/etc/audit/rules.d/actions.rules' to list of files for inspection.
readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/sudoers.d/" /etc/audit/rules.d/*.rules)

# For each of the matched entries
for match in "${matches[@]}"
do
    # Extract filepath from the match
    rulesd_audit_file=$(echo $match | cut -f1 -d ':')
    # Append that path into list of files for inspection
    files_to_inspect+=("$rulesd_audit_file")
done
# Case when particular audit rule isn't defined yet
if [ "${#files_to_inspect[@]}" -eq "0" ]
then
    # Append '/etc/audit/rules.d/actions.rules' into list of files for inspection
    key_rule_file="/etc/audit/rules.d/actions.rules"
    # If the actions.rules file doesn't exist yet, create it with correct permissions
    if [ ! -e "$key_rule_file" ]
    then
        touch "$key_rule_file"
        chmod 0640 "$key_rule_file"
    fi
    files_to_inspect+=("$key_rule_file")
fi

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "[\s]*-w[\s]+/etc/sudoers.d/" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/sudoers.d/ $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/sudoers.d/$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/sudoers.d/ -p wa -k actions" >> "$audit_rules_file"
    fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- 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
    - audit_rules_sysadmin_actions
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Search /etc/audit/rules.d for audit rule entries for sysadmin actions
  find:
    paths: /etc/audit/rules.d
    recurse: false
    contains: ^.*/etc/sudoers.*$
    patterns: '*.rules'
  register: find_audit_sysadmin_actions
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(7)(b)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.2
    - PCI-DSS-Req-10.2.5.b
    - audit_rules_sysadmin_actions
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Use /etc/audit/rules.d/actions.rules as the recipient for the rule
  set_fact:
    all_sysadmin_actions_files:
      - /etc/audit/rules.d/actions.rules
  when:
    - '"audit" in ansible_facts.packages'
    - find_audit_sysadmin_actions.matched is defined and find_audit_sysadmin_actions.matched
      == 0
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(7)(b)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.2
    - PCI-DSS-Req-10.2.5.b
    - audit_rules_sysadmin_actions
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Use matched file as the recipient for the rule
  set_fact:
    all_sysadmin_actions_files:
      - '{{ find_audit_sysadmin_actions.files | map(attribute=''path'') | list | first
        }}'
  when:
    - '"audit" in ansible_facts.packages'
    - find_audit_sysadmin_actions.matched is defined and find_audit_sysadmin_actions.matched
      > 0
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(7)(b)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.2
    - PCI-DSS-Req-10.2.5.b
    - audit_rules_sysadmin_actions
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Inserts/replaces audit rule for /etc/sudoers rule in rules.d
  lineinfile:
    path: '{{ all_sysadmin_actions_files[0] }}'
    line: -w /etc/sudoers -p wa -k actions
    create: true
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(7)(b)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.2
    - PCI-DSS-Req-10.2.5.b
    - audit_rules_sysadmin_actions
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Inserts/replaces audit rule for /etc/sudoers.d rule in rules.d
  lineinfile:
    path: '{{ all_sysadmin_actions_files[0] }}'
    line: -w /etc/sudoers.d/ -p wa -k actions
    create: true
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(7)(b)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.2
    - PCI-DSS-Req-10.2.5.b
    - audit_rules_sysadmin_actions
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Inserts/replaces audit rule for /etc/sudoers in audit.rules
  lineinfile:
    path: /etc/audit/audit.rules
    line: -w /etc/sudoers -p wa -k actions
    create: true
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(7)(b)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.2
    - PCI-DSS-Req-10.2.5.b
    - audit_rules_sysadmin_actions
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Inserts/replaces audit rule for /etc/sudoers.d in audit.rules
  lineinfile:
    path: /etc/audit/audit.rules
    line: -w /etc/sudoers.d/ -p wa -k actions
    create: true
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(7)(b)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.2
    - PCI-DSS-Req-10.2.5.b
    - audit_rules_sysadmin_actions
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Shutdown System When Auditing Failures Occurrule

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following line to a file with suffix .rules in the directory /etc/audit/rules.d:

-f 2
If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following line to the top of the /etc/audit/audit.rules file:
-f 2

Rationale:

It is critical for the appropriate personnel to be aware if a system is at risk of failing to process audit logs as required. Without this notification, the security personnel may be unaware of an impending failure of the audit capability, and system operation may be adversely affected.

Audit processing failures include software/hardware errors, failures in the audit capturing mechanisms, and audit storage capacity being reached or exceeded.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# Traverse all of:
#
# /etc/audit/audit.rules,			(for auditctl case)
# /etc/audit/rules.d/*.rules			(for augenrules case)
find /etc/audit /etc/audit/rules.d -maxdepth 1 -type f -name '*.rules' -exec sed -i '/-f[[:space:]]\+.*/d' {} ';'

for AUDIT_FILE in "/etc/audit/audit.rules" "/etc/audit/rules.d/immutable.rules"
do
	echo '' >> $AUDIT_FILE
	echo '# Set the audit.rules configuration to halt system upon audit failure per security requirements' >> $AUDIT_FILE
	echo '-f 2' >> $AUDIT_FILE
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - NIST-800-171-3.3.1
    - NIST-800-171-3.3.4
    - NIST-800-53-AU-5(b)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SC-24
    - audit_rules_system_shutdown
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Collect all files from /etc/audit/rules.d with .rules extension
  find:
    paths: /etc/audit/rules.d/
    patterns: '*.rules'
  register: find_rules_d
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.3.1
    - NIST-800-171-3.3.4
    - NIST-800-53-AU-5(b)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SC-24
    - audit_rules_system_shutdown
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Remove the -f option from all Audit config files
  lineinfile:
    path: '{{ item }}'
    regexp: ^\s*(?:-f)\s+.*$
    state: absent
  loop: '{{ find_rules_d.files | map(attribute=''path'') | list + [''/etc/audit/audit.rules'']
    }}'
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.3.1
    - NIST-800-171-3.3.4
    - NIST-800-53-AU-5(b)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SC-24
    - audit_rules_system_shutdown
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Add Audit -f option into /etc/audit/rules.d/immutable.rules and /etc/audit/audit.rules
  lineinfile:
    path: '{{ item }}'
    create: true
    line: -f 2
  loop:
    - /etc/audit/audit.rules
    - /etc/audit/rules.d/immutable.rules
  when: '"audit" in ansible_facts.packages'
  tags:
    - NIST-800-171-3.3.1
    - NIST-800-171-3.3.4
    - NIST-800-53-AU-5(b)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SC-24
    - audit_rules_system_shutdown
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify User/Group Information - /etc/grouprule

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d, in order to capture events that modify account changes:

-w /etc/group -p wa -k audit_rules_usergroup_modification


If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file, in order to capture events that modify account changes:

-w /etc/group -p wa -k audit_rules_usergroup_modification

Rationale:

In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy.

references:  AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), 5.4.1.1, 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), 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, 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, FAU_GEN.1.1.c, 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, CCI-000018, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-001405, CCI-001683, CCI-001684, CCI-001685, CCI-001686, CCI-002130, CCI-002132, CCI-002884, SRG-OS-000004-VMM-000040, SRG-OS-000239-VMM-000810, SRG-OS-000240-VMM-000820, SRG-OS-000241-VMM-000830, SRG-OS-000274-VMM-000960, SRG-OS-000275-VMM-000970, SRG-OS-000276-VMM-000980, SRG-OS-000277-VMM-000990, SRG-OS-000303-VMM-001090, SRG-OS-000304-VMM-001100, SRG-OS-000476-VMM-001960, 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, 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, Req-10.2.5, SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000304-GPOS-00121, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221, 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, 3.1.7, 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()


# If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# into the list of files to be inspected
files_to_inspect+=('/etc/audit/audit.rules')

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "[\s]*-w[\s]+/etc/group" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/group $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/group$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/group -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"
    fi
done
# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()

# If the audit is 'augenrules', then check if rule is already defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
# If rule isn't defined, add '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' to list of files for inspection.
readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/group" /etc/audit/rules.d/*.rules)

# For each of the matched entries
for match in "${matches[@]}"
do
    # Extract filepath from the match
    rulesd_audit_file=$(echo $match | cut -f1 -d ':')
    # Append that path into list of files for inspection
    files_to_inspect+=("$rulesd_audit_file")
done
# Case when particular audit rule isn't defined yet
if [ "${#files_to_inspect[@]}" -eq "0" ]
then
    # Append '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' into list of files for inspection
    key_rule_file="/etc/audit/rules.d/audit_rules_usergroup_modification.rules"
    # If the audit_rules_usergroup_modification.rules file doesn't exist yet, create it with correct permissions
    if [ ! -e "$key_rule_file" ]
    then
        touch "$key_rule_file"
        chmod 0640 "$key_rule_file"
    fi
    files_to_inspect+=("$key_rule_file")
fi

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "[\s]*-w[\s]+/etc/group" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/group $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/group$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/group -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"
    fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- 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.5
    - audit_rules_usergroup_modification_group
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit group tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_group
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Search /etc/audit/rules.d for other user/group modification audit rules
  find:
    paths: /etc/audit/rules.d
    recurse: false
    contains: -k audit_rules_usergroup_modification$
    patterns: '*.rules'
  register: find_group
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_group
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: If existing user/group modification ruleset not found, use /etc/audit/rules.d/privileged.rules
    as the recipient for the rule
  set_fact:
    all_files:
      - /etc/audit/rules.d/privileged.rules
  when:
    - '"audit" in ansible_facts.packages'
    - find_group.matched is defined and find_group.matched == 0
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_group
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Use matched file as the recipient for the rule
  set_fact:
    all_files:
      - '{{ find_group.files | map(attribute=''path'') | list | first }}'
  when:
    - '"audit" in ansible_facts.packages'
    - find_group.matched is defined and find_group.matched > 0
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_group
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Inserts/replaces the group rule in rules.d when on x86
  lineinfile:
    path: '{{ all_files[0] }}'
    line: -w /etc/group -p wa -k audit_rules_usergroup_modification
    create: true
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_group
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Inserts/replaces the group rule in /etc/audit/audit.rules
  lineinfile:
    line: -w /etc/group -p wa -k audit_rules_usergroup_modification
    state: present
    dest: /etc/audit/audit.rules
    create: true
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_group
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify User/Group Information - /etc/gshadowrule

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d, in order to capture events that modify account changes:

-w /etc/gshadow -p wa -k audit_rules_usergroup_modification


If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file, in order to capture events that modify account changes:

-w /etc/gshadow -p wa -k audit_rules_usergroup_modification

Rationale:

In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy.

references:  AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), 5.4.1.1, 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), 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, 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, FAU_GEN.1.1.c, 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, CCI-000018, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-001405, CCI-001683, CCI-001684, CCI-001685, CCI-001686, CCI-002130, CCI-002132, SRG-OS-000004-VMM-000040, SRG-OS-000239-VMM-000810, SRG-OS-000240-VMM-000820, SRG-OS-000241-VMM-000830, SRG-OS-000274-VMM-000960, SRG-OS-000275-VMM-000970, SRG-OS-000276-VMM-000980, SRG-OS-000277-VMM-000990, SRG-OS-000303-VMM-001090, SRG-OS-000304-VMM-001100, SRG-OS-000476-VMM-001960, 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, 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, Req-10.2.5, SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000304-GPOS-00121, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221, 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, 3.1.7, 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()


# If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# into the list of files to be inspected
files_to_inspect+=('/etc/audit/audit.rules')

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "[\s]*-w[\s]+/etc/gshadow" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/gshadow $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/gshadow$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/gshadow -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"
    fi
done
# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()

# If the audit is 'augenrules', then check if rule is already defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
# If rule isn't defined, add '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' to list of files for inspection.
readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/gshadow" /etc/audit/rules.d/*.rules)

# For each of the matched entries
for match in "${matches[@]}"
do
    # Extract filepath from the match
    rulesd_audit_file=$(echo $match | cut -f1 -d ':')
    # Append that path into list of files for inspection
    files_to_inspect+=("$rulesd_audit_file")
done
# Case when particular audit rule isn't defined yet
if [ "${#files_to_inspect[@]}" -eq "0" ]
then
    # Append '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' into list of files for inspection
    key_rule_file="/etc/audit/rules.d/audit_rules_usergroup_modification.rules"
    # If the audit_rules_usergroup_modification.rules file doesn't exist yet, create it with correct permissions
    if [ ! -e "$key_rule_file" ]
    then
        touch "$key_rule_file"
        chmod 0640 "$key_rule_file"
    fi
    files_to_inspect+=("$key_rule_file")
fi

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "[\s]*-w[\s]+/etc/gshadow" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/gshadow $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/gshadow$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/gshadow -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"
    fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- 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.5
    - audit_rules_usergroup_modification_gshadow
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit gshadow tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_gshadow
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Search /etc/audit/rules.d for other user/group modification audit rules
  find:
    paths: /etc/audit/rules.d
    recurse: false
    contains: -k audit_rules_usergroup_modification$
    patterns: '*.rules'
  register: find_gshadow
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_gshadow
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: If existing user/group modification ruleset not found, use /etc/audit/rules.d/privileged.rules
    as the recipient for the rule
  set_fact:
    all_files:
      - /etc/audit/rules.d/privileged.rules
  when:
    - '"audit" in ansible_facts.packages'
    - find_gshadow.matched is defined and find_gshadow.matched == 0
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_gshadow
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Use matched file as the recipient for the rule
  set_fact:
    all_files:
      - '{{ find_gshadow.files | map(attribute=''path'') | list | first }}'
  when:
    - '"audit" in ansible_facts.packages'
    - find_gshadow.matched is defined and find_gshadow.matched > 0
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_gshadow
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Inserts/replaces the gshadow rule in rules.d when on x86
  lineinfile:
    path: '{{ all_files[0] }}'
    line: -w /etc/gshadow -p wa -k audit_rules_usergroup_modification
    create: true
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_gshadow
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Inserts/replaces the gshadow rule in /etc/audit/audit.rules
  lineinfile:
    line: -w /etc/gshadow -p wa -k audit_rules_usergroup_modification
    state: present
    dest: /etc/audit/audit.rules
    create: true
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_gshadow
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify User/Group Information - /etc/security/opasswdrule

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d, in order to capture events that modify account changes:

-w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification


If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file, in order to capture events that modify account changes:

-w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification

Rationale:

In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy.

references:  AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), 5.4.1.1, 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), 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, 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, FAU_GEN.1.1.c, 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, CCI-000018, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-001405, CCI-001683, CCI-001684, CCI-001685, CCI-001686, CCI-002130, CCI-002132, SRG-OS-000004-VMM-000040, SRG-OS-000239-VMM-000810, SRG-OS-000240-VMM-000820, SRG-OS-000241-VMM-000830, SRG-OS-000274-VMM-000960, SRG-OS-000275-VMM-000970, SRG-OS-000276-VMM-000980, SRG-OS-000277-VMM-000990, SRG-OS-000303-VMM-001090, SRG-OS-000304-VMM-001100, SRG-OS-000476-VMM-001960, 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, 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, Req-10.2.5, SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000304-GPOS-00121, SRG-OS-000476-GPOS-00221, SRG-OS-000463-GPOS-00207, 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, 3.1.7, 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()


# If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# into the list of files to be inspected
files_to_inspect+=('/etc/audit/audit.rules')

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "[\s]*-w[\s]+/etc/security/opasswd" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/security/opasswd $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/security/opasswd$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"
    fi
done
# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()

# If the audit is 'augenrules', then check if rule is already defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
# If rule isn't defined, add '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' to list of files for inspection.
readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/security/opasswd" /etc/audit/rules.d/*.rules)

# For each of the matched entries
for match in "${matches[@]}"
do
    # Extract filepath from the match
    rulesd_audit_file=$(echo $match | cut -f1 -d ':')
    # Append that path into list of files for inspection
    files_to_inspect+=("$rulesd_audit_file")
done
# Case when particular audit rule isn't defined yet
if [ "${#files_to_inspect[@]}" -eq "0" ]
then
    # Append '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' into list of files for inspection
    key_rule_file="/etc/audit/rules.d/audit_rules_usergroup_modification.rules"
    # If the audit_rules_usergroup_modification.rules file doesn't exist yet, create it with correct permissions
    if [ ! -e "$key_rule_file" ]
    then
        touch "$key_rule_file"
        chmod 0640 "$key_rule_file"
    fi
    files_to_inspect+=("$key_rule_file")
fi

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "[\s]*-w[\s]+/etc/security/opasswd" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/security/opasswd $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/security/opasswd$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"
    fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- 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.5
    - audit_rules_usergroup_modification_opasswd
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit opasswd tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_opasswd
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Search /etc/audit/rules.d for other user/group modification audit rules
  find:
    paths: /etc/audit/rules.d
    recurse: false
    contains: -k audit_rules_usergroup_modification$
    patterns: '*.rules'
  register: find_opasswd
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_opasswd
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: If existing user/group modification ruleset not found, use /etc/audit/rules.d/privileged.rules
    as the recipient for the rule
  set_fact:
    all_files:
      - /etc/audit/rules.d/privileged.rules
  when:
    - '"audit" in ansible_facts.packages'
    - find_opasswd.matched is defined and find_opasswd.matched == 0
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_opasswd
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Use matched file as the recipient for the rule
  set_fact:
    all_files:
      - '{{ find_opasswd.files | map(attribute=''path'') | list | first }}'
  when:
    - '"audit" in ansible_facts.packages'
    - find_opasswd.matched is defined and find_opasswd.matched > 0
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_opasswd
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Inserts/replaces the opasswd rule in rules.d when on x86
  lineinfile:
    path: '{{ all_files[0] }}'
    line: -w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification
    create: true
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_opasswd
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Inserts/replaces the opasswd rule in /etc/audit/audit.rules
  lineinfile:
    line: -w /etc/security/opasswd -p wa -k audit_rules_usergroup_modification
    state: present
    dest: /etc/audit/audit.rules
    create: true
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_opasswd
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify User/Group Information - /etc/passwdrule

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d, in order to capture events that modify account changes:

-w /etc/passwd -p wa -k audit_rules_usergroup_modification


If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file, in order to capture events that modify account changes:

-w /etc/passwd -p wa -k audit_rules_usergroup_modification

Rationale:

In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy.

references:  AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), 5.4.1.1, 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), 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, 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, FAU_GEN.1.1.c, 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, CCI-000018, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-001405, CCI-001683, CCI-001684, CCI-001685, CCI-001686, CCI-002130, CCI-002132, SRG-OS-000004-VMM-000040, SRG-OS-000239-VMM-000810, SRG-OS-000240-VMM-000820, SRG-OS-000241-VMM-000830, SRG-OS-000274-VMM-000960, SRG-OS-000275-VMM-000970, SRG-OS-000276-VMM-000980, SRG-OS-000277-VMM-000990, SRG-OS-000303-VMM-001090, SRG-OS-000304-VMM-001100, SRG-OS-000476-VMM-001960, 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, 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, Req-10.2.5, SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000304-GPOS-00121, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221, SRG-OS-000274-GPOS-00104, SRG-OS-000275-GPOS-00105, SRG-OS-000276-GPOS-00106, SRG-OS-000277-GPOS-00107, 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, 3.1.7, 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()


# If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# into the list of files to be inspected
files_to_inspect+=('/etc/audit/audit.rules')

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "[\s]*-w[\s]+/etc/passwd" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/passwd $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/passwd$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/passwd -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"
    fi
done
# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()

# If the audit is 'augenrules', then check if rule is already defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
# If rule isn't defined, add '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' to list of files for inspection.
readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/passwd" /etc/audit/rules.d/*.rules)

# For each of the matched entries
for match in "${matches[@]}"
do
    # Extract filepath from the match
    rulesd_audit_file=$(echo $match | cut -f1 -d ':')
    # Append that path into list of files for inspection
    files_to_inspect+=("$rulesd_audit_file")
done
# Case when particular audit rule isn't defined yet
if [ "${#files_to_inspect[@]}" -eq "0" ]
then
    # Append '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' into list of files for inspection
    key_rule_file="/etc/audit/rules.d/audit_rules_usergroup_modification.rules"
    # If the audit_rules_usergroup_modification.rules file doesn't exist yet, create it with correct permissions
    if [ ! -e "$key_rule_file" ]
    then
        touch "$key_rule_file"
        chmod 0640 "$key_rule_file"
    fi
    files_to_inspect+=("$key_rule_file")
fi

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "[\s]*-w[\s]+/etc/passwd" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/passwd $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/passwd$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/passwd -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"
    fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- 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.5
    - audit_rules_usergroup_modification_passwd
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit passwd tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_passwd
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Search /etc/audit/rules.d for other user/group modification audit rules
  find:
    paths: /etc/audit/rules.d
    recurse: false
    contains: -k audit_rules_usergroup_modification$
    patterns: '*.rules'
  register: find_passwd
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_passwd
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: If existing user/group modification ruleset not found, use /etc/audit/rules.d/privileged.rules
    as the recipient for the rule
  set_fact:
    all_files:
      - /etc/audit/rules.d/privileged.rules
  when:
    - '"audit" in ansible_facts.packages'
    - find_passwd.matched is defined and find_passwd.matched == 0
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_passwd
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Use matched file as the recipient for the rule
  set_fact:
    all_files:
      - '{{ find_passwd.files | map(attribute=''path'') | list | first }}'
  when:
    - '"audit" in ansible_facts.packages'
    - find_passwd.matched is defined and find_passwd.matched > 0
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_passwd
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Inserts/replaces the passwd rule in rules.d when on x86
  lineinfile:
    path: '{{ all_files[0] }}'
    line: -w /etc/passwd -p wa -k audit_rules_usergroup_modification
    create: true
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_passwd
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Inserts/replaces the passwd rule in /etc/audit/audit.rules
  lineinfile:
    line: -w /etc/passwd -p wa -k audit_rules_usergroup_modification
    state: present
    dest: /etc/audit/audit.rules
    create: true
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_passwd
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

Record Events that Modify User/Group Information - /etc/shadowrule

If the auditd daemon is configured to use the augenrules program to read audit rules during daemon startup (the default), add the following lines to a file with suffix .rules in the directory /etc/audit/rules.d, in order to capture events that modify account changes:

-w /etc/shadow -p wa -k audit_rules_usergroup_modification


If the auditd daemon is configured to use the auditctl utility to read audit rules during daemon startup, add the following lines to /etc/audit/audit.rules file, in order to capture events that modify account changes:

-w /etc/shadow -p wa -k audit_rules_usergroup_modification

Rationale:

In addition to auditing new user and group accounts, these watches will alert the system administrator(s) to any modifications. Any unexpected users, groups, or modifications should be investigated for legitimacy.

references:  AC-2(4), AU-2(d), AU-12(c), AC-6(9), CM-6(a), 5.4.1.1, 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), 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, 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, FAU_GEN.1.1.c, 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, CCI-000018, CCI-000169, CCI-000172, CCI-001403, CCI-001404, CCI-001405, CCI-001683, CCI-001684, CCI-001685, CCI-001686, CCI-002130, CCI-002132, SRG-OS-000004-VMM-000040, SRG-OS-000239-VMM-000810, SRG-OS-000240-VMM-000820, SRG-OS-000241-VMM-000830, SRG-OS-000274-VMM-000960, SRG-OS-000275-VMM-000970, SRG-OS-000276-VMM-000980, SRG-OS-000277-VMM-000990, SRG-OS-000303-VMM-001090, SRG-OS-000304-VMM-001100, SRG-OS-000476-VMM-001960, 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, 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, Req-10.2.5, SRG-OS-000004-GPOS-00004, SRG-OS-000037-GPOS-00015, SRG-OS-000042-GPOS-00020, SRG-OS-000062-GPOS-00031, SRG-OS-000304-GPOS-00121, SRG-OS-000392-GPOS-00172, SRG-OS-000462-GPOS-00206, SRG-OS-000470-GPOS-00214, SRG-OS-000471-GPOS-00215, SRG-OS-000239-GPOS-00089, SRG-OS-000240-GPOS-00090, SRG-OS-000241-GPOS-00091, SRG-OS-000303-GPOS-00120, SRG-OS-000304-GPOS-00121, SRG-OS-000466-GPOS-00210, SRG-OS-000476-GPOS-00221, 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, 3.1.7, 1, 11, 12, 13, 14, 15, 16, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

# Perform the remediation for both possible tools: 'auditctl' and 'augenrules'

# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()


# If the audit tool is 'auditctl', then add '/etc/audit/audit.rules'
# into the list of files to be inspected
files_to_inspect+=('/etc/audit/audit.rules')

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "[\s]*-w[\s]+/etc/shadow" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/shadow $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/shadow$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/shadow -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"
    fi
done
# Create a list of audit *.rules files that should be inspected for presence and correctness
# of a particular audit rule. The scheme is as follows:
#
# -----------------------------------------------------------------------------------------
# Tool used to load audit rules	| Rule already defined	|  Audit rules file to inspect	  |
# -----------------------------------------------------------------------------------------
#	auditctl		|     Doesn't matter	|  /etc/audit/audit.rules	  |
# -----------------------------------------------------------------------------------------
# 	augenrules		|          Yes		|  /etc/audit/rules.d/*.rules	  |
# 	augenrules		|          No		|  /etc/audit/rules.d/$key.rules  |
# -----------------------------------------------------------------------------------------
files_to_inspect=()

# If the audit is 'augenrules', then check if rule is already defined
# If rule is defined, add '/etc/audit/rules.d/*.rules' to list of files for inspection.
# If rule isn't defined, add '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' to list of files for inspection.
readarray -t matches < <(grep -HP "[\s]*-w[\s]+/etc/shadow" /etc/audit/rules.d/*.rules)

# For each of the matched entries
for match in "${matches[@]}"
do
    # Extract filepath from the match
    rulesd_audit_file=$(echo $match | cut -f1 -d ':')
    # Append that path into list of files for inspection
    files_to_inspect+=("$rulesd_audit_file")
done
# Case when particular audit rule isn't defined yet
if [ "${#files_to_inspect[@]}" -eq "0" ]
then
    # Append '/etc/audit/rules.d/audit_rules_usergroup_modification.rules' into list of files for inspection
    key_rule_file="/etc/audit/rules.d/audit_rules_usergroup_modification.rules"
    # If the audit_rules_usergroup_modification.rules file doesn't exist yet, create it with correct permissions
    if [ ! -e "$key_rule_file" ]
    then
        touch "$key_rule_file"
        chmod 0640 "$key_rule_file"
    fi
    files_to_inspect+=("$key_rule_file")
fi

# Finally perform the inspection and possible subsequent audit rule
# correction for each of the files previously identified for inspection
for audit_rules_file in "${files_to_inspect[@]}"
do
    # Check if audit watch file system object rule for given path already present
    if grep -q -P -- "[\s]*-w[\s]+/etc/shadow" "$audit_rules_file"
    then
        # Rule is found => verify yet if existing rule definition contains
        # all of the required access type bits

        # Define BRE whitespace class shortcut
        sp="[[:space:]]"
        # Extract current permission access types (e.g. -p [r|w|x|a] values) from audit rule
        current_access_bits=$(sed -ne "s#$sp*-w$sp\+/etc/shadow $sp\+-p$sp\+\([rxwa]\{1,4\}\).*#\1#p" "$audit_rules_file")
        # Split required access bits string into characters array
        # (to check bit's presence for one bit at a time)
        for access_bit in $(echo "wa" | grep -o .)
        do
            # For each from the required access bits (e.g. 'w', 'a') check
            # if they are already present in current access bits for rule.
            # If not, append that bit at the end
            if ! grep -q "$access_bit" <<< "$current_access_bits"
            then
                # Concatenate the existing mask with the missing bit
                current_access_bits="$current_access_bits$access_bit"
            fi
        done
        # Propagate the updated rule's access bits (original + the required
        # ones) back into the /etc/audit/audit.rules file for that rule
        sed -i "s#\($sp*-w$sp\+/etc/shadow$sp\+-p$sp\+\)\([rxwa]\{1,4\}\)\(.*\)#\1$current_access_bits\3#" "$audit_rules_file"
    else
        # Rule isn't present yet. Append it at the end of $audit_rules_file file
        # with proper key

        echo "-w /etc/shadow -p wa -k audit_rules_usergroup_modification" >> "$audit_rules_file"
    fi
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- 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.5
    - audit_rules_usergroup_modification_shadow
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Set architecture for audit shadow tasks
  set_fact:
    audit_arch: b{{ ansible_architecture | regex_replace('.*(\d\d$)','\1') }}
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_shadow
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Search /etc/audit/rules.d for other user/group modification audit rules
  find:
    paths: /etc/audit/rules.d
    recurse: false
    contains: -k audit_rules_usergroup_modification$
    patterns: '*.rules'
  register: find_shadow
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_shadow
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: If existing user/group modification ruleset not found, use /etc/audit/rules.d/privileged.rules
    as the recipient for the rule
  set_fact:
    all_files:
      - /etc/audit/rules.d/privileged.rules
  when:
    - '"audit" in ansible_facts.packages'
    - find_shadow.matched is defined and find_shadow.matched == 0
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_shadow
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Use matched file as the recipient for the rule
  set_fact:
    all_files:
      - '{{ find_shadow.files | map(attribute=''path'') | list | first }}'
  when:
    - '"audit" in ansible_facts.packages'
    - find_shadow.matched is defined and find_shadow.matched > 0
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_shadow
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Inserts/replaces the shadow rule in rules.d when on x86
  lineinfile:
    path: '{{ all_files[0] }}'
    line: -w /etc/shadow -p wa -k audit_rules_usergroup_modification
    create: true
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_shadow
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Inserts/replaces the shadow rule in /etc/audit/audit.rules
  lineinfile:
    line: -w /etc/shadow -p wa -k audit_rules_usergroup_modification
    state: present
    dest: /etc/audit/audit.rules
    create: true
  when: '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.1.7
    - NIST-800-53-AC-2(4)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-CM-6(a)
    - PCI-DSS-Req-10.2.5
    - audit_rules_usergroup_modification_shadow
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy

System Audit Logs Must Be Owned By Rootrule

All audit logs must be owned by root user and group. By default, the path for audit log is

/var/log/audit/
. To properly set the owner of /var/log/audit, run the command:
$ sudo chown root /var/log/audit 
To properly set the owner of /var/log/audit/*, run the command:
$ sudo chown root /var/log/audit/* 

Rationale:

Unauthorized disclosure of audit records can reveal system and configuration data to attackers, thus compromising its confidentiality.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

if LC_ALL=C grep -m 1 -q ^log_group /etc/audit/auditd.conf; then
  GROUP=$(awk -F "=" '/log_group/ {print $2}' /etc/audit/auditd.conf | tr -d ' ')
  if ! [ "${GROUP}" == 'root' ] ; then
    chown root.${GROUP} /var/log/audit
    chown root.${GROUP} /var/log/audit/audit.log*
  else
    chown root.root /var/log/audit
    chown root.root /var/log/audit/audit.log*
  fi
else
  chown root.root /var/log/audit
  chown root.root /var/log/audit/audit.log*
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

System Audit Logs Must Have Mode 0640 or Less Permissiverule

If log_group in /etc/audit/auditd.conf is set to a group other than the root group account, change the mode of the audit log files with the following command:

$ sudo chmod 0640 audit_file

Otherwise, change the mode of the audit log files with the following command:
$ sudo chmod 0600 audit_file

Rationale:

If users can write to audit logs, audit trails can be modified or destroyed.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q audit; then

if LC_ALL=C grep -iw log_file /etc/audit/auditd.conf; then
    FILE=$(awk -F "=" '/^log_file/ {print $2}' /etc/audit/auditd.conf | tr -d ' ')
else
    FILE="/var/log/audit/audit.log"
fi

if LC_ALL=C grep -m 1 -q ^log_group /etc/audit/auditd.conf; then
  GROUP=$(awk -F "=" '/log_group/ {print $2}' /etc/audit/auditd.conf | tr -d ' ')
  if ! [ "${GROUP}" == 'root' ] ; then
    chmod 0640 $FILE
    chmod 0440 $FILE.*
  else
    chmod 0600 $FILE
    chmod 0400 $FILE.*
  fi
else
  chmod 0600 $FILE
  chmod 0400 $FILE.*
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Enable auditd Servicerule

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.

references:  CIP-004-6 R3.3, CIP-007-3 R6.5, CCI-000126, CCI-000130, CCI-000131, CCI-000132, CCI-000133, CCI-000134, CCI-000135, CCI-000154, CCI-000158, CCI-000366, CCI-001464, CCI-001487, CCI-001814, CCI-001876, CCI-002884, CCI-000169, 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, 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, 5.4.1.1, Req-10.1, SRG-OS-000037-GPOS-00015, SRG-OS-000038-GPOS-00016, SRG-OS-000039-GPOS-00017, SRG-OS-000040-GPOS-00018, SRG-OS-000041-GPOS-00019, SRG-OS-000042-GPOS-00021, SRG-OS-000051-GPOS-00024, SRG-OS-000054-GPOS-00025, SRG-OS-000122-GPOS-00063, SRG-OS-000254-GPOS-00095, SRG-OS-000255-GPOS-00096, SRG-OS-000365-GPOS-00152, SRG-OS-000392-GPOS-00172, SRG-OS-000480-GPOS-00227, SRG-OS-000062-GPOS-00031, 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), 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, 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, 3.3.1, 3.3.2, 3.3.6, 1, 11, 12, 13, 14, 15, 16, 19, 2, 3, 4, 5, 6, 7, 8, 9, SRG-OS-000037-VMM-000150, SRG-OS-000063-VMM-000310, SRG-OS-000038-VMM-000160, SRG-OS-000039-VMM-000170, SRG-OS-000040-VMM-000180, SRG-OS-000041-VMM-000190

Remediation script:
# 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 script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.3.1
    - NIST-800-171-3.3.2
    - NIST-800-171-3.3.6
    - NIST-800-53-AC-2(g)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-10
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-14(1)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-AU-3
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SI-4(23)
    - PCI-DSS-Req-10.1
    - enable_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - service_auditd_enabled

- name: Enable service auditd
  block:

    - name: Gather the package facts
      package_facts:
        manager: auto

    - name: Enable service auditd
      service:
        name: auditd
        enabled: 'yes'
        state: started
        masked: 'no'
      when:
        - '"audit" in ansible_facts.packages'
  when:
    - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    - '"audit" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.3.1
    - NIST-800-171-3.3.2
    - NIST-800-171-3.3.6
    - NIST-800-53-AC-2(g)
    - NIST-800-53-AC-6(9)
    - NIST-800-53-AU-10
    - NIST-800-53-AU-12(c)
    - NIST-800-53-AU-14(1)
    - NIST-800-53-AU-2(d)
    - NIST-800-53-AU-3
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SI-4(23)
    - PCI-DSS-Req-10.1
    - enable_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - service_auditd_enabled
Remediation script:
include enable_auditd

class enable_auditd {
  service {'auditd':
    enable => true,
    ensure => 'running',
  }
}
Remediation script:
---
apiVersion: machineconfiguration.openshift.io/v1
kind: MachineConfig
spec:
  config:
    ignition:
      version: 3.1.0
    systemd:
      units:
      - name: auditd.service
        enabled: true
Remediation script:

[customizations.services]
enabled = ["auditd"]

Enable Auditing for Processes Which Start Prior to the Audit Daemonrule

To ensure all processes can be audited, even those which start prior to the audit daemon, add the argument audit=1 to the default GRUB 2 command line for the Linux operating system in /boot/grub2/grubenv, in the manner below:

# grub2-editenv - set "$(grub2-editenv - list | grep kernelopts) audit=1"

warning  The GRUB 2 configuration file, grub.cfg, is automatically updated each time a new kernel is installed. Note that any changes to /etc/default/grub require rebuilding the grub.cfg file. To update the GRUB 2 configuration file manually, use the
grub2-mkconfig -o
command as follows:
  • On BIOS-based machines, issue the following command as root:
    ~]# grub2-mkconfig -o /boot/grub2/grub.cfg
  • On UEFI-based machines, issue the following command as root:
    ~]# grub2-mkconfig -o /boot/efi/EFI/fedora/grub.cfg
Rationale:

Each process on the system carries an "auditable" flag which indicates whether its activities can be audited. Although auditd takes care of enabling this for all processes which launch after it does, adding the kernel argument ensures it is set for every process during boot.

Remediation script:
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ] && { rpm --quiet -q grub2-common; }; then

# Correct grub2 kernelopts value using grub2-editenv
existing_kernelopts="$(grub2-editenv - list | grep kernelopts)"
if ! printf '%s' "$existing_kernelopts" | grep -qE '^kernelopts=(.*\s)?audit=1(\s.*)?$'; then
  if test -n "$existing_kernelopts"; then
    grub2-editenv - set "$existing_kernelopts audit=1"
  else
    grub2-editenv - set "kernelopts=audit=1"
  fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.3.1
    - NIST-800-53-AC-17(1)
    - NIST-800-53-AU-10
    - NIST-800-53-AU-14(1)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IR-5(1)
    - PCI-DSS-Req-10.3
    - grub2_audit_argument
    - low_disruption
    - medium_complexity
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Get current kernel parameters
  ansible.builtin.shell:
    cmd: /usr/bin/grub2-editenv - list | grep "kernelopts="
  register: kernelopts
  ignore_errors: true
  changed_when: false
  when:
    - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    - '"grub2-common" in ansible_facts.packages'
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.3.1
    - NIST-800-53-AC-17(1)
    - NIST-800-53-AU-10
    - NIST-800-53-AU-14(1)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IR-5(1)
    - PCI-DSS-Req-10.3
    - grub2_audit_argument
    - low_disruption
    - medium_complexity
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Update the bootloader menu
  command: /usr/bin/grub2-editenv - set "{{ item }} audit=1"
  with_items: '{{ kernelopts.stdout_lines | select(''match'', ''^kernelopts.*'') |
    list }}'
  when:
    - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    - '"grub2-common" in ansible_facts.packages'
    - kernelopts.rc == 0
    - kernelopts.stdout_lines is defined
    - kernelopts.stdout_lines | length > 0
    - kernelopts.stdout | regex_search('^kernelopts=(?:.*\s)?audit=1(?:\s.*)?$', multiline=True)
      is none
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.3.1
    - NIST-800-53-AC-17(1)
    - NIST-800-53-AU-10
    - NIST-800-53-AU-14(1)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IR-5(1)
    - PCI-DSS-Req-10.3
    - grub2_audit_argument
    - low_disruption
    - medium_complexity
    - medium_severity
    - reboot_required
    - restrict_strategy

- name: Update the bootloader menu when there are no entries previously set
  command: /usr/bin/grub2-editenv - set "kernelopts=audit=1"
  when:
    - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    - '"grub2-common" in ansible_facts.packages'
    - kernelopts.rc != 0
  tags:
    - CJIS-5.4.1.1
    - NIST-800-171-3.3.1
    - NIST-800-53-AC-17(1)
    - NIST-800-53-AU-10
    - NIST-800-53-AU-14(1)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-IR-5(1)
    - PCI-DSS-Req-10.3
    - grub2_audit_argument
    - low_disruption
    - medium_complexity
    - medium_severity
    - reboot_required
    - restrict_strategy
Remediation script:

[customizations.kernel]
append = "audit=1"

GRUB2 bootloader configurationgroup

During the boot process, the boot loader is responsible for starting the execution of the kernel and passing options to it. The boot loader allows for the selection of different kernels - possibly on different partitions or media. The default Red Hat Virtualization 4 boot loader for x86 systems is called GRUB2. Options it can pass to the kernel include single-user mode, which provides root access without any authentication, and the ability to disable SELinux. To prevent local users from modifying the boot parameters and endangering security, protect the boot loader configuration with a password and ensure its configuration file's permissions are set properly.

contains 2 rules

Non-UEFI GRUB2 bootloader configurationgroup

Non-UEFI GRUB2 bootloader configuration

contains 1 rule

Set Boot Loader Password in grub2rule

The grub2 boot loader should have a superuser account and password protection enabled to protect boot-time settings.

Since plaintext passwords are a security risk, generate a hash for the password by running the following command:

# grub2-setpassword
When prompted, enter the password that was selected.

warning  To prevent hard-coded passwords, automatic remediation of this control is not available. Remediation must be automated as a component of machine provisioning, or followed manually as outlined above. Also, do NOT manually add the superuser account and password to the grub.cfg file as the grub2-mkconfig command overwrites this file.
Rationale:

Password protection on the boot loader configuration ensures users with physical access cannot trivially alter important bootloader settings. These include which kernel to use, and whether to enter single-user mode.

UEFI GRUB2 bootloader configurationgroup

UEFI GRUB2 bootloader configuration

contains 1 rule

Set the UEFI Boot Loader Passwordrule

The grub2 boot loader should have a superuser account and password protection enabled to protect boot-time settings.

Since plaintext passwords are a security risk, generate a hash for the password by running the following command:

# grub2-setpassword
When prompted, enter the password that was selected.

warning  To prevent hard-coded passwords, automatic remediation of this control is not available. Remediation must be automated as a component of machine provisioning, or followed manually as outlined above. Also, do NOT manually add the superuser account and password to the grub.cfg file as the grub2-mkconfig command overwrites this file.
Rationale:

Password protection on the boot loader configuration ensures users with physical access cannot trivially alter important bootloader settings. These include which kernel to use, and whether to enter single-user mode.

Network Configuration and Firewallsgroup

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.

contains 2 rules

Wireless Networkinggroup

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.

contains 2 rules

Disable Wireless Through Software Configurationgroup

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.

contains 2 rules

Disable Bluetooth Servicerule

The bluetooth service can be disabled with the following command:

$ sudo systemctl mask --now bluetooth.service
$ sudo service bluetooth stop

Rationale:

Disabling the bluetooth service prevents the system from attempting connections to Bluetooth devices, which entails some security risk. Nevertheless, variation in this risk decision may be expected due to the utility of Bluetooth connectivity and its limited range.

Remediation script:
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

SYSTEMCTL_EXEC='/usr/bin/systemctl'
"$SYSTEMCTL_EXEC" stop 'bluetooth.service'
"$SYSTEMCTL_EXEC" disable 'bluetooth.service'
"$SYSTEMCTL_EXEC" mask 'bluetooth.service'
# Disable socket activation if we have a unit file for it
if "$SYSTEMCTL_EXEC" list-unit-files | grep -q '^bluetooth.socket'; then
    "$SYSTEMCTL_EXEC" stop 'bluetooth.socket'
    "$SYSTEMCTL_EXEC" mask 'bluetooth.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 'bluetooth.service' || true

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Disable service bluetooth
  block:

    - name: Gather the service facts
      service_facts: null

    - name: Disable service bluetooth
      systemd:
        name: bluetooth.service
        enabled: 'no'
        state: stopped
        masked: 'yes'
      when: '"bluetooth.service" in ansible_facts.services'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - 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
    - disable_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - service_bluetooth_disabled

- name: Unit Socket Exists - bluetooth.socket
  command: systemctl list-unit-files bluetooth.socket
  args:
    warn: false
  register: socket_file_exists
  changed_when: false
  ignore_errors: true
  check_mode: false
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-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
    - disable_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - service_bluetooth_disabled

- name: Disable socket bluetooth
  systemd:
    name: bluetooth.socket
    enabled: 'no'
    state: stopped
    masked: 'yes'
  when:
    - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    - '"bluetooth.socket" in socket_file_exists.stdout_lines[1]'
  tags:
    - 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
    - disable_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - service_bluetooth_disabled
Remediation script:
include disable_bluetooth

class disable_bluetooth {
  service {'bluetooth':
    enable => false,
    ensure => 'stopped',
  }
}
Remediation script:

[customizations.services]
disabled = ["bluetooth"]

Disable Bluetooth Kernel Modulerule

The kernel's module loading system can be configured to prevent loading of the Bluetooth module. Add the following to the appropriate /etc/modprobe.d configuration file to prevent the loading of the Bluetooth module:

install bluetooth /bin/true

Rationale:

If Bluetooth functionality must be disabled, preventing the kernel from loading the kernel module provides an additional safeguard against its activation.

Remediation script:
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if LC_ALL=C grep -q -m 1 "^install bluetooth" /etc/modprobe.d/bluetooth.conf ; then
	
	sed -i 's#^install bluetooth.*#install bluetooth /bin/true#g' /etc/modprobe.d/bluetooth.conf
else
	echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/bluetooth.conf
	echo "install bluetooth /bin/true" >> /etc/modprobe.d/bluetooth.conf
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Ensure kernel module 'bluetooth' is disabled
  lineinfile:
    create: true
    dest: /etc/modprobe.d/bluetooth.conf
    regexp: bluetooth
    line: install bluetooth /bin/true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - CJIS-5.13.1.3
    - 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
    - disable_strategy
    - kernel_module_bluetooth_disabled
    - low_complexity
    - medium_disruption
    - medium_severity
    - reboot_required

File Permissions and Masksgroup

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 Virtualization 4 installations:

$ mount -t xfs | awk '{print $3}'
For any systems that use a different local filesystem type, modify this command as appropriate.

contains 4 rules

Restrict Dynamic Mounting and Unmounting of Filesystemsgroup

Linux includes a number of facilities for the automated addition and removal of filesystems on a running system. These facilities may be necessary in many environments, but this capability also carries some risk -- whether direct risk from allowing users to introduce arbitrary filesystems, or risk that software flaws in the automated mount facility itself could allow an attacker to compromise the system.

This command can be used to list the types of filesystems that are available to the currently executing kernel:

$ find /lib/modules/`uname -r`/kernel/fs -type f -name '*.ko'
If these filesystems are not required then they can be explicitly disabled in a configuratio file in /etc/modprobe.d.

contains 1 rule

Disable Modprobe Loading of USB Storage Driverrule

To prevent USB storage devices from being used, configure the kernel module loading system to prevent automatic loading of the USB storage driver. To configure the system to prevent the usb-storage kernel module from being loaded, add the following line to a file in the directory /etc/modprobe.d:

install usb-storage /bin/true
This will prevent the modprobe program from loading the usb-storage module, but will not prevent an administrator (or another program) from using the insmod program to load the module manually.

Rationale:

USB storage devices such as thumb drives can be used to introduce malicious software.

Remediation script:
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if LC_ALL=C grep -q -m 1 "^install usb-storage" /etc/modprobe.d/usb-storage.conf ; then
	
	sed -i 's#^install usb-storage.*#install usb-storage /bin/true#g' /etc/modprobe.d/usb-storage.conf
else
	echo -e "\n# Disable per security requirements" >> /etc/modprobe.d/usb-storage.conf
	echo "install usb-storage /bin/true" >> /etc/modprobe.d/usb-storage.conf
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Ensure kernel module 'usb-storage' is disabled
  lineinfile:
    create: true
    dest: /etc/modprobe.d/usb-storage.conf
    regexp: usb-storage
    line: install usb-storage /bin/true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-171-3.1.21
    - NIST-800-53-CM-6(a)
    - NIST-800-53-CM-7(a)
    - NIST-800-53-CM-7(b)
    - NIST-800-53-MP-7
    - disable_strategy
    - kernel_module_usb-storage_disabled
    - low_complexity
    - medium_disruption
    - medium_severity
    - reboot_required

Restrict Programs from Dangerous Execution Patternsgroup

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.

contains 3 rules

Enable ExecShieldgroup

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.

contains 3 rules

Enable ExecShield via sysctlrule

By default on Red Hat Enterprise Linux 7 64-bit systems, ExecShield is enabled and can only be disabled if the hardware does not support ExecShield or is disabled in /etc/default/grub. For Red Hat Enterprise Linux 7 32-bit systems, sysctl can be used to enable ExecShield.

Rationale:

ExecShield uses the segmentation feature on all x86 systems to prevent execution in memory higher than a certain address. It writes an address as a limit in the code segment descriptor, to control where code can be executed, on a per-process basis. When the kernel places a process's memory regions such as the stack and heap higher than this address, the hardware prevents execution in that address range. This is enabled by default on the latest Red Hat and Fedora systems if supported by the hardware.

Remediation script:
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

if [ "$(getconf LONG_BIT)" = "32" ] ; then
  #
  # Set runtime for kernel.exec-shield
  #
  sysctl -q -n -w kernel.exec-shield=1

  #
  # If kernel.exec-shield present in /etc/sysctl.conf, change value to "1"
  #	else, add "kernel.exec-shield = 1" to /etc/sysctl.conf
  #
  # Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
# Otherwise, regular sed command will do.
sed_command=('sed' '-i')
if test -L "/etc/sysctl.conf"; then
    sed_command+=('--follow-symlinks')
fi

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.exec-shield")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "1"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^kernel.exec-shield\\>" "/etc/sysctl.conf"; then
    "${sed_command[@]}" "s/^kernel.exec-shield\\>.*/$formatted_output/gi" "/etc/sysctl.conf"
else
    # \n is precaution for case where file ends without trailing newline
    
    printf '%s\n' "$formatted_output" >> "/etc/sysctl.conf"
fi
fi

if [ "$(getconf LONG_BIT)" = "64" ] ; then
  if grep --silent noexec /boot/grub2/grub*.cfg ; then
        sed -i "s/noexec.*//g" /etc/default/grub
        sed -i "s/noexec.*//g" /etc/grub.d/*
        grub2-mkconfig -o "/boot/grub2"/grub*.cfg
  fi
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi

Restrict Exposed Kernel Pointer Addresses Accessrule

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 that can contain functions pointers. If a write vulnereability occurs in the kernel allowing a write access to any of this structure, the kernel can be compromise. This option disallow any program withtout the CAP_SYSLOG capability from getting the kernel pointers addresses, replacing them with 0.

Remediation script:
# 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 ; do
  matching_list=$(grep -P '^(?!#).*[\s]+kernel.kptr_restrict.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      # comment out "kernel.kptr_restrict" matches to preserve user data
      sed -i "s/^${entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set runtime for kernel.kptr_restrict
#
/sbin/sysctl -q -n -w kernel.kptr_restrict="1"

#
# If kernel.kptr_restrict present in /etc/sysctl.conf, change value to "1"
#	else, add "kernel.kptr_restrict = 1" to /etc/sysctl.conf
#
# Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
# Otherwise, regular sed command will do.
sed_command=('sed' '-i')
if test -L "/etc/sysctl.conf"; then
    sed_command+=('--follow-symlinks')
fi

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.kptr_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.kptr_restrict\\>" "/etc/sysctl.conf"; then
    "${sed_command[@]}" "s/^kernel.kptr_restrict\\>.*/$formatted_output/gi" "/etc/sysctl.conf"
else
    # \n is precaution for case where file ends without trailing newline
    
    printf '%s\n' "$formatted_output" >> "/etc/sysctl.conf"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: List /etc/sysctl.d/*.conf files
  find:
    paths: /etc/sysctl.d/
    contains: ^[\s]*kernel.kptr_restrict.*$
    patterns: '*.conf'
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - 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 /etc/sysctl.d/*.conf
    files
  replace:
    path: '{{ item }}'
    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:
    - 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: Ensure sysctl kernel.kptr_restrict is set to 1
  sysctl:
    name: kernel.kptr_restrict
    value: '1'
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - 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

Enable Randomized Layout of Virtual Address Spacerule

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.

Remediation script:
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# Comment out any occurrences of kernel.randomize_va_space from /etc/sysctl.d/*.conf files
for f in /etc/sysctl.d/*.conf ; do
  matching_list=$(grep -P '^(?!#).*[\s]+kernel.randomize_va_space.*$' $f | uniq )
  if ! test -z "$matching_list"; then
    while IFS= read -r entry; do
      # comment out "kernel.randomize_va_space" matches to preserve user data
      sed -i "s/^${entry}$/# &/g" $f
    done <<< "$matching_list"
  fi
done

#
# Set runtime for kernel.randomize_va_space
#
/sbin/sysctl -q -n -w kernel.randomize_va_space="2"

#
# If kernel.randomize_va_space present in /etc/sysctl.conf, change value to "2"
#	else, add "kernel.randomize_va_space = 2" to /etc/sysctl.conf
#
# Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
# Otherwise, regular sed command will do.
sed_command=('sed' '-i')
if test -L "/etc/sysctl.conf"; then
    sed_command+=('--follow-symlinks')
fi

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^kernel.randomize_va_space")

# shellcheck disable=SC2059
printf -v formatted_output "%s = %s" "$stripped_key" "2"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^kernel.randomize_va_space\\>" "/etc/sysctl.conf"; then
    "${sed_command[@]}" "s/^kernel.randomize_va_space\\>.*/$formatted_output/gi" "/etc/sysctl.conf"
else
    # \n is precaution for case where file ends without trailing newline
    
    printf '%s\n' "$formatted_output" >> "/etc/sysctl.conf"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: List /etc/sysctl.d/*.conf files
  find:
    paths: /etc/sysctl.d/
    contains: ^[\s]*kernel.randomize_va_space.*$
    patterns: '*.conf'
  register: find_sysctl_d
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SC-30
    - NIST-800-53-SC-30(2)
    - disable_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - reboot_required
    - sysctl_kernel_randomize_va_space

- name: Comment out any occurrences of kernel.randomize_va_space from /etc/sysctl.d/*.conf
    files
  replace:
    path: '{{ item }}'
    regexp: ^[\s]*kernel.randomize_va_space
    replace: '#kernel.randomize_va_space'
  loop: '{{ find_sysctl_d.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SC-30
    - NIST-800-53-SC-30(2)
    - disable_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - reboot_required
    - sysctl_kernel_randomize_va_space

- name: Ensure sysctl kernel.randomize_va_space is set to 2
  sysctl:
    name: kernel.randomize_va_space
    value: '2'
    state: present
    reload: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-171-3.1.7
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SC-30
    - NIST-800-53-SC-30(2)
    - disable_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - reboot_required
    - sysctl_kernel_randomize_va_space

SELinuxgroup

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 Virtualization 4, 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 Virtualization 4 system, unless that system has unusual requirements which make a stronger policy appropriate.

contains 5 rules

SELinux - Booleansgroup

Enable or Disable runtime customization of SELinux system policies without having to reload or recompile the SELinux policy.

contains 1 rule

Enable the fips_mode SELinux Booleanrule

By default, the SELinux boolean fips_mode is enabled. This allows all SELinux domains to execute in fips_mode. If this setting is disabled, it should be enabled. To enable the fips_mode SELinux boolean, run the following command:

$ sudo setsebool -P fips_mode on

Rationale:

Remediation script:
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

var_fips_mode='true'


setsebool -P fips_mode $var_fips_mode

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: XCCDF Value var_fips_mode # promote to variable
  set_fact:
    var_fips_mode: !!str true
  tags:
    - always

- name: Ensure libsemanage-python installed
  package:
    name: libsemanage-python
    state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-171-3.13.11
    - 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_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - sebool_fips_mode

- name: Set SELinux boolean fips_mode accordingly
  seboolean:
    name: fips_mode
    state: '{{ var_fips_mode }}'
    persistent: true
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-171-3.13.11
    - 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_strategy
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - sebool_fips_mode

Ensure SELinux Not Disabled in /etc/default/grubrule

SELinux can be disabled at boot time by an argument in /etc/default/grub. Remove any instances of selinux=0 from the kernel arguments in that file to prevent SELinux from being disabled at boot.

Rationale:

Disabling a major host protection feature, such as SELinux, at boot time prevents it from confining system services at boot time. Further, it increases the chances that it will remain off during system operation.

references:  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, CCI-000022, CCI-000032, AC-3, AC-3(3)(a), DE.AE-1, ID.AM-3, PR.AC-4, PR.AC-5, PR.AC-6, PR.DS-5, PR.PT-1, PR.PT-3, PR.PT-4, 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, 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 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, 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, 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, 3.1.2, 3.7.2, 1, 11, 12, 13, 14, 15, 16, 18, 3, 4, 5, 6, 8, 9, SRG-OS-000445-VMM-001780

Remediation script:
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

sed -i --follow-symlinks "s/selinux=0//gI" /etc/default/grub /etc/grub2.cfg /etc/grub.d/*
sed -i --follow-symlinks "s/enforcing=0//gI" /etc/default/grub /etc/grub2.cfg /etc/grub.d/*

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Find /etc/grub.d/ files
  find:
    paths:
      - /etc/grub.d/
    follow: true
  register: grub
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-171-3.1.2
    - NIST-800-171-3.7.2
    - NIST-800-53-AC-3
    - NIST-800-53-AC-3(3)(a)
    - grub2_enable_selinux
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

- name: Ensure SELinux Not Disabled in grub files
  replace:
    dest: '{{ item.path }}'
    regexp: (selinux|enforcing)=0
  with_items:
    - '{{ grub.files }}'
    - path: /etc/grub2.cfg
    - path: /etc/default/grub
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-171-3.1.2
    - NIST-800-171-3.7.2
    - NIST-800-53-AC-3
    - NIST-800-53-AC-3(3)(a)
    - grub2_enable_selinux
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Ensure No Daemons are Unconfined by SELinuxrule

Daemons for which the SELinux policy does not contain rules will inherit the context of the parent process. Because daemons are launched during startup and descend from the init process, they inherit the unconfined_service_t context.

To check for unconfined daemons, run the following command:

$ sudo ps -eZ | grep "unconfined_service_t"
It should produce no output in a well-configured system.

warning  Automatic remediation of this control is not available. Remediation can be achieved by amending SELinux policy or stopping the unconfined daemons as outlined above.
Rationale:

Daemons which run with the unconfined_service_t context may cause AVC denials, or allow privileges that the daemon does not require.

Configure SELinux Policyrule

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.

references:  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, CCI-002165, CCI-002696, 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, 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, SRG-OS-000445-GPOS-00199, BP28(R66), 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, 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, 3.1.2, 3.7.2, 1, 11, 12, 13, 14, 15, 16, 18, 3, 4, 5, 6, 8, 9, SRG-OS-000445-VMM-001780, 164.308(a)(1)(ii)(D), 164.308(a)(3), 164.308(a)(4), 164.310(b), 164.310(c), 164.312(a), 164.312(e)

Remediation script:
# 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 script:
- name: XCCDF Value var_selinux_policy_name # promote to variable
  set_fact:
    var_selinux_policy_name: !!str targeted
  tags:
    - always

- name: Configure SELinux Policy
  block:

    - name: Check for duplicate values
      lineinfile:
        path: /etc/selinux/config
        create: false
        regexp: ^SELINUXTYPE=
        state: absent
      check_mode: true
      changed_when: false
      register: dupes

    - name: Deduplicate values from /etc/selinux/config
      lineinfile:
        path: /etc/selinux/config
        create: false
        regexp: ^SELINUXTYPE=
        state: absent
      when: dupes.found is defined and dupes.found > 1

    - name: Insert correct line to /etc/selinux/config
      lineinfile:
        path: /etc/selinux/config
        create: true
        regexp: ^SELINUXTYPE=
        line: SELINUXTYPE={{ var_selinux_policy_name }}
        state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-171-3.1.2
    - NIST-800-171-3.7.2
    - NIST-800-53-AC-3
    - NIST-800-53-AC-3(3)(a)
    - NIST-800-53-AU-9
    - NIST-800-53-SC-7(21)
    - low_complexity
    - low_disruption
    - medium_severity
    - reboot_required
    - restrict_strategy
    - selinux_policytype

Ensure SELinux State is Enforcingrule

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.

references:  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, CCI-002165, CCI-002696, AC-3, AC-3(3)(a), AU-9, SC-7(21), SRG-OS-000445-VMM-001780, 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, 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, SRG-OS-000445-GPOS-00199, BP28(R4), BP28(R66), 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, 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, 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, 3.1.2, 3.7.2, 1, 11, 12, 13, 14, 15, 16, 18, 3, 4, 5, 6, 8, 9, 164.308(a)(1)(ii)(D), 164.308(a)(3), 164.308(a)(4), 164.310(b), 164.310(c), 164.312(a), 164.312(e)

Remediation script:
# 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 script:
- name: XCCDF Value var_selinux_state # promote to variable
  set_fact:
    var_selinux_state: !!str enforcing
  tags:
    - always

- name: Ensure SELinux State is Enforcing
  block:

    - name: Check for duplicate values
      lineinfile:
        path: /etc/selinux/config
        create: false
        regexp: ^SELINUX=
        state: absent
      check_mode: true
      changed_when: false
      register: dupes

    - name: Deduplicate values from /etc/selinux/config
      lineinfile:
        path: /etc/selinux/config
        create: false
        regexp: ^SELINUX=
        state: absent
      when: dupes.found is defined and dupes.found > 1

    - name: Insert correct line to /etc/selinux/config
      lineinfile:
        path: /etc/selinux/config
        create: true
        regexp: ^SELINUX=
        line: SELINUX={{ var_selinux_state }}
        state: present
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-171-3.1.2
    - NIST-800-171-3.7.2
    - NIST-800-53-AC-3
    - NIST-800-53-AC-3(3)(a)
    - NIST-800-53-AU-9
    - NIST-800-53-SC-7(21)
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
    - selinux_state

Servicesgroup

The best protection against vulnerable software is running less software. This section describes how to review the software which Red Hat Virtualization 4 installs on a system and disable software which is not needed. It then enumerates the software packages installed on a default Red Hat Virtualization 4 system and provides guidance about which ones can be safely disabled.

Red Hat Virtualization 4 provides a convenient minimal install option that essentially installs the bare necessities for a functional system. When building Red Hat Virtualization 4 systems, it is highly recommended to select the minimal packages and then build up the system from there.

contains 17 rules

SSH Servergroup

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.

contains 16 rules

Configure OpenSSH Server if Necessarygroup

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.

contains 14 rules

Disable Host-Based Authenticationrule

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.

Remediation script:
# 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 before the line matching the regex '^Match'.
line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.bak" | LC_ALL=C sed 's/:.*//g')"
if [ -z "$line_number" ]; then
    # There was no match of '^Match', insert at
    # the end of the file.
    printf '%s\n' "HostbasedAuthentication no" >> "/etc/ssh/sshd_config"
else
    head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.bak" > "/etc/ssh/sshd_config"
    printf '%s\n' "HostbasedAuthentication no" >> "/etc/ssh/sshd_config"
    tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
fi
# Clean up after ourselves.
rm "/etc/ssh/sshd_config.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Disable Host-Based Authentication
  block:

    - name: Check for duplicate values
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        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: false
        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: ^[#\s]*Match
        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)
    - disable_host_auth
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy

Disable Compression Or Set Compression to delayedrule

Compression is useful for slow network connections over long distances but can cause performance issues on local LANs. If use of compression is required, it should be enabled only after a user has authenticated; otherwise, it should be disabled. To disable compression or delay compression until after a user has successfully authenticated, add or correct the following line in the /etc/ssh/sshd_config file:

Compression no

Rationale:

If compression is allowed in an SSH connection prior to authentication, vulnerabilities in the compression software could result in compromise of the system from an unauthenticated connection, potentially with root privileges.

Remediation script:
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

var_sshd_disable_compression='no'


# Test if the config_file is a symbolic link. If so, use --follow-symlinks with sed.
# Otherwise, regular sed command will do.
sed_command=('sed' '-i')
if test -L "/etc/ssh/sshd_config"; then
    sed_command+=('--follow-symlinks')
fi

# Strip any search characters in the key arg so that the key can be replaced without
# adding any search characters to the config file.
stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^Compression")

# shellcheck disable=SC2059
printf -v formatted_output "%s %s" "$stripped_key" "$var_sshd_disable_compression"

# If the key exists, change it. Otherwise, add it to the config_file.
# We search for the key string followed by a word boundary (matched by \>),
# so if we search for 'setting', 'setting2' won't match.
if LC_ALL=C grep -q -m 1 -i -e "^Compression\\>" "/etc/ssh/sshd_config"; then
    "${sed_command[@]}" "s/^Compression\\>.*/$formatted_output/gi" "/etc/ssh/sshd_config"
else
    # \n is precaution for case where file ends without trailing newline
    
    printf '%s\n' "$formatted_output" >> "/etc/ssh/sshd_config"
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: XCCDF Value var_sshd_disable_compression # promote to variable
  set_fact:
    var_sshd_disable_compression: !!str no
  tags:
    - always

- name: Disable Compression Or Set Compression to delayed
  block:

    - name: Check for duplicate values
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        regexp: (?i)^\s*Compression\s+
        state: absent
      check_mode: true
      changed_when: false
      register: dupes

    - name: Deduplicate values from /etc/ssh/sshd_config
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        regexp: (?i)^\s*Compression\s+
        state: absent
      when: dupes.found is defined and dupes.found > 1

    - name: Insert correct line to /etc/ssh/sshd_config
      lineinfile:
        path: /etc/ssh/sshd_config
        create: true
        regexp: (?i)^\s*Compression\s+
        line: Compression {{ var_sshd_disable_compression }}
        state: present
        insertbefore: ^[#\s]*Match
        validate: /usr/sbin/sshd -t -f %s
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - NIST-800-171-3.1.12
    - NIST-800-53-AC-17(a)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-CM-7(a)
    - NIST-800-53-CM-7(b)
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
    - sshd_disable_compression

Disable SSH Access via Empty Passwordsrule

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.

Remediation script:
# 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 before the line matching the regex '^Match'.
line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.bak" | LC_ALL=C sed 's/:.*//g')"
if [ -z "$line_number" ]; then
    # There was no match of '^Match', insert at
    # the end of the file.
    printf '%s\n' "PermitEmptyPasswords no" >> "/etc/ssh/sshd_config"
else
    head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.bak" > "/etc/ssh/sshd_config"
    printf '%s\n' "PermitEmptyPasswords no" >> "/etc/ssh/sshd_config"
    tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
fi
# Clean up after ourselves.
rm "/etc/ssh/sshd_config.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Disable SSH Access via Empty Passwords
  block:

    - name: Check for duplicate values
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        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: false
        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: ^[#\s]*Match
        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.1
    - NIST-800-171-3.1.5
    - NIST-800-53-AC-17(a)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-CM-7(a)
    - NIST-800-53-CM-7(b)
    - high_severity
    - low_complexity
    - low_disruption
    - no_reboot_needed
    - restrict_strategy
    - sshd_disable_empty_passwords

Disable GSSAPI Authenticationrule

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.

Remediation script:
# 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 before the line matching the regex '^Match'.
line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.bak" | LC_ALL=C sed 's/:.*//g')"
if [ -z "$line_number" ]; then
    # There was no match of '^Match', insert at
    # the end of the file.
    printf '%s\n' "GSSAPIAuthentication no" >> "/etc/ssh/sshd_config"
else
    head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.bak" > "/etc/ssh/sshd_config"
    printf '%s\n' "GSSAPIAuthentication no" >> "/etc/ssh/sshd_config"
    tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
fi
# Clean up after ourselves.
rm "/etc/ssh/sshd_config.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Disable GSSAPI Authentication
  block:

    - name: Check for duplicate values
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        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: false
        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: ^[#\s]*Match
        validate: /usr/sbin/sshd -t -f %s
  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(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

Disable Kerberos Authenticationrule

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. Vulnerabilities in the system's Kerberos implementations may be subject to exploitation.

Remediation script:
# 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 before the line matching the regex '^Match'.
line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.bak" | LC_ALL=C sed 's/:.*//g')"
if [ -z "$line_number" ]; then
    # There was no match of '^Match', insert at
    # the end of the file.
    printf '%s\n' "KerberosAuthentication no" >> "/etc/ssh/sshd_config"
else
    head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.bak" > "/etc/ssh/sshd_config"
    printf '%s\n' "KerberosAuthentication no" >> "/etc/ssh/sshd_config"
    tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
fi
# Clean up after ourselves.
rm "/etc/ssh/sshd_config.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Disable Kerberos Authentication
  block:

    - name: Check for duplicate values
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        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: false
        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: ^[#\s]*Match
        validate: /usr/sbin/sshd -t -f %s
  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(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

Do Not Allow SSH Environment Optionsrule

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.

Remediation script:
# 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 before the line matching the regex '^Match'.
line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.bak" | LC_ALL=C sed 's/:.*//g')"
if [ -z "$line_number" ]; then
    # There was no match of '^Match', insert at
    # the end of the file.
    printf '%s\n' "PermitUserEnvironment no" >> "/etc/ssh/sshd_config"
else
    head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.bak" > "/etc/ssh/sshd_config"
    printf '%s\n' "PermitUserEnvironment no" >> "/etc/ssh/sshd_config"
    tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
fi
# Clean up after ourselves.
rm "/etc/ssh/sshd_config.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Do Not Allow SSH Environment Options
  block:

    - name: Check for duplicate values
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        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: false
        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: ^[#\s]*Match
        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)
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
    - sshd_do_not_permit_user_env

Enable Use of Strict Mode Checkingrule

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.

Remediation script:
# 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 before the line matching the regex '^Match'.
line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.bak" | LC_ALL=C sed 's/:.*//g')"
if [ -z "$line_number" ]; then
    # There was no match of '^Match', insert at
    # the end of the file.
    printf '%s\n' "StrictModes yes" >> "/etc/ssh/sshd_config"
else
    head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.bak" > "/etc/ssh/sshd_config"
    printf '%s\n' "StrictModes yes" >> "/etc/ssh/sshd_config"
    tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
fi
# Clean up after ourselves.
rm "/etc/ssh/sshd_config.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Enable Use of Strict Mode Checking
  block:

    - name: Check for duplicate values
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        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: false
        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: ^[#\s]*Match
        validate: /usr/sbin/sshd -t -f %s
  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-AC-6
    - NIST-800-53-CM-6(a)
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
    - sshd_enable_strictmodes

Enable SSH Warning Bannerrule

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.

Remediation script:
# 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 before the line matching the regex '^Match'.
line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.bak" | LC_ALL=C sed 's/:.*//g')"
if [ -z "$line_number" ]; then
    # There was no match of '^Match', insert at
    # the end of the file.
    printf '%s\n' "Banner /etc/issue" >> "/etc/ssh/sshd_config"
else
    head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.bak" > "/etc/ssh/sshd_config"
    printf '%s\n' "Banner /etc/issue" >> "/etc/ssh/sshd_config"
    tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
fi
# Clean up after ourselves.
rm "/etc/ssh/sshd_config.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Enable SSH Warning Banner
  block:

    - name: Check for duplicate values
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        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: false
        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: ^[#\s]*Match
        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.9
    - NIST-800-53-AC-17(a)
    - NIST-800-53-AC-8(a)
    - NIST-800-53-AC-8(c)
    - NIST-800-53-CM-6(a)
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
    - sshd_enable_warning_banner

Enable SSH Print Last Logrule

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.

Remediation script:
# 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 before the line matching the regex '^Match'.
line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.bak" | LC_ALL=C sed 's/:.*//g')"
if [ -z "$line_number" ]; then
    # There was no match of '^Match', insert at
    # the end of the file.
    printf '%s\n' "PrintLastLog yes" >> "/etc/ssh/sshd_config"
else
    head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.bak" > "/etc/ssh/sshd_config"
    printf '%s\n' "PrintLastLog yes" >> "/etc/ssh/sshd_config"
    tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
fi
# Clean up after ourselves.
rm "/etc/ssh/sshd_config.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Enable SSH Print Last Log
  block:

    - name: Check for duplicate values
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        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: false
        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: ^[#\s]*Match
        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-AC-9
    - NIST-800-53-CM-6(a)
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
    - sshd_print_last_log

Set SSH Idle Timeout Intervalrule

SSH allows administrators to set an idle timeout interval. After this interval has passed, the idle user will be automatically logged out.

To set an idle timeout interval, edit the following line in /etc/ssh/sshd_config as follows:

ClientAliveInterval 300


The timeout interval is given in seconds. For example, have a timeout of 10 minutes, set interval to 600.

If a shorter timeout has already been set for the login shell, that value will preempt any SSH setting made in /etc/ssh/sshd_config. Keep in mind that some processes may stop SSH from correctly detecting that the user is idle.

warning  SSH disconnecting idle clients will not have desired effect without also configuring ClientAliveCountMax in the SSH service configuration.
warning  Following conditions may prevent the SSH session to time out:
  • Remote processes on the remote machine generates output. As the output has to be transferred over the network to the client, the timeout is reset every time such transfer happens.
  • Any scp or sftp activity by the same user to the host resets the timeout.
Rationale:

Terminating an idle ssh session within a short time period reduces the window of opportunity for unauthorized personnel to take control of a management session enabled on the console or console port that has been let unattended.

Remediation script:
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

sshd_idle_timeout_value='300'


if [ -e "/etc/ssh/sshd_config" ] ; then
    
    LC_ALL=C sed -i "/^\s*ClientAliveInterval\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 before the line matching the regex '^Match'.
line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.bak" | LC_ALL=C sed 's/:.*//g')"
if [ -z "$line_number" ]; then
    # There was no match of '^Match', insert at
    # the end of the file.
    printf '%s\n' "ClientAliveInterval $sshd_idle_timeout_value" >> "/etc/ssh/sshd_config"
else
    head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.bak" > "/etc/ssh/sshd_config"
    printf '%s\n' "ClientAliveInterval $sshd_idle_timeout_value" >> "/etc/ssh/sshd_config"
    tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
fi
# Clean up after ourselves.
rm "/etc/ssh/sshd_config.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: XCCDF Value sshd_idle_timeout_value # promote to variable
  set_fact:
    sshd_idle_timeout_value: !!str 300
  tags:
    - always

- name: Set SSH Idle Timeout Interval
  block:

    - name: Check for duplicate values
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        regexp: (?i)^\s*ClientAliveInterval\s+
        state: absent
      check_mode: true
      changed_when: false
      register: dupes

    - name: Deduplicate values from /etc/ssh/sshd_config
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        regexp: (?i)^\s*ClientAliveInterval\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*ClientAliveInterval\s+
        line: ClientAliveInterval {{ sshd_idle_timeout_value }}
        state: present
        insertbefore: ^[#\s]*Match
        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.11
    - NIST-800-53-AC-12
    - NIST-800-53-AC-17(a)
    - NIST-800-53-AC-17(a)
    - NIST-800-53-AC-2(5)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SC-10
    - PCI-DSS-Req-8.1.8
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
    - sshd_set_idle_timeout

Set SSH Client Alive Count Max to zerorule

The SSH server sends at most ClientAliveCountMax messages during a SSH session and waits for a response from the SSH client. The option ClientAliveInterval configures timeout after each ClientAliveCountMax message. If the SSH server does not receive a response from the client, then the connection is considered idle and terminated. To ensure the SSH idle timeout occurs precisely when the ClientAliveInterval is set, set the ClientAliveCountMax to value of 0 in /etc/ssh/sshd_config:

Rationale:

This ensures a user login will be terminated as soon as the ClientAliveInterval is reached.

Remediation script:
# 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*ClientAliveCountMax\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 before the line matching the regex '^Match'.
line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.bak" | LC_ALL=C sed 's/:.*//g')"
if [ -z "$line_number" ]; then
    # There was no match of '^Match', insert at
    # the end of the file.
    printf '%s\n' "ClientAliveCountMax 0" >> "/etc/ssh/sshd_config"
else
    head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.bak" > "/etc/ssh/sshd_config"
    printf '%s\n' "ClientAliveCountMax 0" >> "/etc/ssh/sshd_config"
    tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
fi
# Clean up after ourselves.
rm "/etc/ssh/sshd_config.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Set SSH Client Alive Count Max to zero
  block:

    - name: Check for duplicate values
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        regexp: (?i)^\s*ClientAliveCountMax\s+
        state: absent
      check_mode: true
      changed_when: false
      register: dupes

    - name: Deduplicate values from /etc/ssh/sshd_config
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        regexp: (?i)^\s*ClientAliveCountMax\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*ClientAliveCountMax\s+
        line: ClientAliveCountMax 0
        state: present
        insertbefore: ^[#\s]*Match
        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.11
    - NIST-800-53-AC-12
    - NIST-800-53-AC-17(a)
    - NIST-800-53-AC-2(5)
    - NIST-800-53-CM-6(a)
    - NIST-800-53-SC-10
    - PCI-DSS-Req-8.1.8
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
    - sshd_set_keepalive_0

Set LogLevel to INFOrule

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.

references:  AC-17(a), CM-6(a)

Remediation script:
# 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 before the line matching the regex '^Match'.
line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.bak" | LC_ALL=C sed 's/:.*//g')"
if [ -z "$line_number" ]; then
    # There was no match of '^Match', insert at
    # the end of the file.
    printf '%s\n' "LogLevel INFO" >> "/etc/ssh/sshd_config"
else
    head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.bak" > "/etc/ssh/sshd_config"
    printf '%s\n' "LogLevel INFO" >> "/etc/ssh/sshd_config"
    tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
fi
# Clean up after ourselves.
rm "/etc/ssh/sshd_config.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Set LogLevel to INFO
  block:

    - name: Check for duplicate values
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        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: false
        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: ^[#\s]*Match
        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

Enable Use of Privilege Separationrule

When enabled, SSH will create an unprivileged child process that has the privilege of the authenticated user. To enable privilege separation in SSH, add or correct the following line in the /etc/ssh/sshd_config file:

UsePrivilegeSeparation sandbox

Rationale:

SSH daemon privilege separation causes the SSH process to drop root privileges when not needed which would decrease the impact of software vulnerabilities in the unprivileged section.

Remediation script:
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

var_sshd_priv_separation='sandbox'


if [ -e "/etc/ssh/sshd_config" ] ; then
    
    LC_ALL=C sed -i "/^\s*UsePrivilegeSeparation\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 before the line matching the regex '^Match'.
line_number="$(LC_ALL=C grep -n "^Match" "/etc/ssh/sshd_config.bak" | LC_ALL=C sed 's/:.*//g')"
if [ -z "$line_number" ]; then
    # There was no match of '^Match', insert at
    # the end of the file.
    printf '%s\n' "UsePrivilegeSeparation $var_sshd_priv_separation" >> "/etc/ssh/sshd_config"
else
    head -n "$(( line_number - 1 ))" "/etc/ssh/sshd_config.bak" > "/etc/ssh/sshd_config"
    printf '%s\n' "UsePrivilegeSeparation $var_sshd_priv_separation" >> "/etc/ssh/sshd_config"
    tail -n "+$(( line_number ))" "/etc/ssh/sshd_config.bak" >> "/etc/ssh/sshd_config"
fi
# Clean up after ourselves.
rm "/etc/ssh/sshd_config.bak"

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: XCCDF Value var_sshd_priv_separation # promote to variable
  set_fact:
    var_sshd_priv_separation: !!str sandbox
  tags:
    - always

- name: Enable Use of Privilege Separation
  block:

    - name: Check for duplicate values
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        regexp: (?i)^\s*UsePrivilegeSeparation\s+
        state: absent
      check_mode: true
      changed_when: false
      register: dupes

    - name: Deduplicate values from /etc/ssh/sshd_config
      lineinfile:
        path: /etc/ssh/sshd_config
        create: false
        regexp: (?i)^\s*UsePrivilegeSeparation\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*UsePrivilegeSeparation\s+
        line: UsePrivilegeSeparation {{ var_sshd_priv_separation }}
        state: present
        insertbefore: ^[#\s]*Match
        validate: /usr/sbin/sshd -t -f %s
  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-AC-6
    - NIST-800-53-CM-6(a)
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
    - restrict_strategy
    - sshd_use_priv_separation

Verify Permissions on SSH Server Private *_key Key Filesrule

To properly set the permissions of /etc/ssh/*_key, run the command:

$ sudo chmod 0600 /etc/ssh/*_key

Rationale:

If an unauthorized user obtains the private SSH host key file, the host could be impersonated.

Remediation script:
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

readarray -t files < <(find /etc/ssh/)
for file in "${files[@]}"; do
    if basename $file | grep -qE '^.*_key$'; then
        chmod 0600 $file
    fi    
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Find /etc/ssh/ file(s)
  find:
    paths: /etc/ssh/
    patterns: ^.*_key$
    use_regex: true
  register: files_found
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - 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)
    - configure_strategy
    - file_permissions_sshd_private_key
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

- name: Set permissions for /etc/ssh/ file(s)
  file:
    path: '{{ item.path }}'
    mode: '0600'
  with_items:
    - '{{ files_found.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - 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)
    - configure_strategy
    - file_permissions_sshd_private_key
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
Remediation script:
include ssh_private_key_perms

class ssh_private_key_perms {
  exec { 'sshd_priv_key':
    command => "chmod 0640 /etc/ssh/*_key",
    path    => '/bin:/usr/bin'
  }
}

Verify Permissions on SSH Server Public *.pub Key Filesrule

To properly set the permissions of /etc/ssh/*.pub, run the command:

$ sudo chmod 0644 /etc/ssh/*.pub

Rationale:

If a public host key file is modified by an unauthorized user, the SSH service may be compromised.

Remediation script:
# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

readarray -t files < <(find /etc/ssh/)
for file in "${files[@]}"; do
    if basename $file | grep -qE '^.*.pub$'; then
        chmod 0644 $file
    fi    
done

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Find /etc/ssh/ file(s)
  find:
    paths: /etc/ssh/
    patterns: ^.*.pub$
    use_regex: true
  register: files_found
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - 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)
    - configure_strategy
    - file_permissions_sshd_pub_key
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed

- name: Set permissions for /etc/ssh/ file(s)
  file:
    path: '{{ item.path }}'
    mode: '0644'
  with_items:
    - '{{ files_found.files }}'
  when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - 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)
    - configure_strategy
    - file_permissions_sshd_pub_key
    - low_complexity
    - low_disruption
    - medium_severity
    - no_reboot_needed
Remediation script:
include ssh_public_key_perms

class ssh_public_key_perms {
  exec { 'sshd_pub_key':
    command => "chmod 0644 /etc/ssh/*.pub",
    path    => '/bin:/usr/bin'
  }
}

System Security Services Daemongroup

The System Security Services Daemon (SSSD) is a system daemon that provides access to different identity and authentication providers such as Red Hat's IdM, Microsoft's AD, openLDAP, MIT Kerberos, etc. It uses a common framework that can provide caching and offline support to systems utilizing SSSD. SSSD using caching to reduce load on authentication servers permit offline authentication as well as store extended user data.

For more information, see

contains 1 rule

Enable Smartcards in SSSDrule

SSSD should be configured to authenticate access to the system using smart cards. To enable smart cards in SSSD, set pam_cert_auth to true under the [pam] section in /etc/sssd/sssd.conf. For example:

[pam]
pam_cert_auth = true

Rationale:

Using an authentication device, such as a CAC or token that is separate from the information system, ensures that even if the information system is compromised, that compromise will not affect credentials stored on the authentication device.

Multifactor solutions that require devices separate from information systems gaining access include, for example, hardware tokens providing time-based or challenge-response authenticators and smart cards such as the U.S. Government Personal Identity Verification card and the DoD Common Access Card.

Remediation script:
# Remediation is applicable only in certain platforms
if rpm --quiet -q sssd-common && { [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; }; then

SSSD_CONF="/etc/sssd/sssd.conf"
SSSD_OPT="pam_cert_auth"
SSSD_OPT_VAL=true
PAM_REGEX="[[:space:]]*\[pam]"
PAM_OPT_REGEX="${PAM_REGEX}([^\n\[]*\n+)+?[[:space:]]*${SSSD_OPT}"

if grep -qzosP $PAM_OPT_REGEX $SSSD_CONF; then
	sed -i "s/${SSSD_OPT}[^(\n)]*/${SSSD_OPT} = ${SSSD_OPT_VAL}/" $SSSD_CONF
elif grep -qs $PAM_REGEX $SSSD_CONF; then
	sed -i "/$PAM_REGEX/a ${SSSD_OPT} = ${SSSD_OPT_VAL}" $SSSD_CONF
else
	mkdir -p /etc/sssd
	touch $SSSD_CONF
	echo -e "[pam]\n${SSSD_OPT} = ${SSSD_OPT_VAL}" >> $SSSD_CONF
fi

else
    >&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation script:
- name: Gather the package facts
  package_facts:
    manager: auto
  tags:
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed
    - sssd_enable_smartcards

- name: Test for domain group
  command: grep '^\s*\[domain\/[^]]*]' /etc/sssd/sssd.conf
  register: test_grep_domain
  ignore_errors: true
  changed_when: false
  check_mode: false
  when:
    - '"sssd-common" in ansible_facts.packages'
    - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed
    - sssd_enable_smartcards

- name: Add default domain group (if no domain there)
  ini_file:
    path: /etc/sssd/sssd.conf
    section: '{{ item.section }}'
    option: '{{ item.option }}'
    value: '{{ item.value }}'
    create: true
    mode: 384
  with_items:
    - section: sssd
      option: domains
      value: default
    - section: domain/default
      option: id_provider
      value: files
  when:
    - '"sssd-common" in ansible_facts.packages'
    - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
    - test_grep_domain.stdout is defined
    - test_grep_domain.stdout | length < 1
  tags:
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed
    - sssd_enable_smartcards

- name: Enable Smartcards in SSSD
  ini_file:
    dest: /etc/sssd/sssd.conf
    section: pam
    option: pam_cert_auth
    value: 'true'
    create: true
    mode: 384
  when:
    - '"sssd-common" in ansible_facts.packages'
    - ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
  tags:
    - configure_strategy
    - low_complexity
    - medium_disruption
    - medium_severity
    - no_reboot_needed
    - sssd_enable_smartcards
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.